From ce4ebbc3b538bccacf6604a0c2f302942e13fc92 Mon Sep 17 00:00:00 2001 From: Maksim Kotlyar Date: Fri, 3 Mar 2017 16:17:51 +0200 Subject: [PATCH] Partial update. --- composer.json | 5 +- docker-compose.yml | 2 + src/ChangesCollector.php | 86 ++++++++++++ src/MongodbStorage.php | 55 +++++--- tests/ChangesCollectorTest.php | 168 ++++++++++++++++++++++++ tests/Functional/MongodbStorageTest.php | 2 +- tests/Model/Object.php | 22 ++++ 7 files changed, 321 insertions(+), 19 deletions(-) create mode 100644 src/ChangesCollector.php create mode 100644 tests/ChangesCollectorTest.php create mode 100644 tests/Model/Object.php diff --git a/composer.json b/composer.json index b81c690..f20dc54 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "require": { "php": "^7.0", "mongodb/mongodb": "^1", - "makasim/values": "^0.2" + "makasim/values": "^0.2", + "mikemccabe/json-patch-php": "^0.1" }, "require-dev": { "phpunit/phpunit": "~6.0" @@ -25,7 +26,7 @@ "psr-4": { "Makasim\\Yadm\\": "src" }, "files": ["src/functions_include.php"] }, - "autoload0-dev": { + "autoload-dev": { "psr-4": { "Makasim\\Yadm\\Tests\\": "tests" } }, "extra": { diff --git a/docker-compose.yml b/docker-compose.yml index fb34137..84a6a42 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,3 +9,5 @@ services: mongo: image: mongo:3 + ports: + - "27017:27017" \ No newline at end of file diff --git a/src/ChangesCollector.php b/src/ChangesCollector.php new file mode 100644 index 0000000..cb54afb --- /dev/null +++ b/src/ChangesCollector.php @@ -0,0 +1,86 @@ +originalValues[$id] = get_values($object); + } + } + + public function unregister($object) + { + if ($id = get_object_id($object)) { + unset($this->originalValues[$id]); + } + } + + public function changes($object) + { + if (false == $id = get_object_id($object)) { + throw new \LogicException(sprintf('Object does not have an id set.')); + } + + if (false == array_key_exists($id, $this->originalValues)) { + throw new \LogicException(sprintf('Changes has not been collected. The object with id "%s" original data is missing.')); + } + + $diff = JsonPatch::diff($this->originalValues[$id], get_values($object)); + + $update = ['$set' => [], '$unset' => []]; + foreach ($diff as $op) { + switch ($op['op']) { + case 'add': + if (is_array($op['value'])) { + foreach ($op['value'] as $key => $value) { + $update['$set'][$this->pathToDot($op['path']).'.'.$key] = $value; + } + } else { + $update['$set'][$this->pathToDot($op['path'])] = $op['value']; + } + + break; + case 'remove': + $update['$unset'][$this->pathToDot($op['path'])] = ''; + + break; + case 'replace': + $update['$set'][$this->pathToDot($op['path'])] = $op['value']; + + break; + default: + throw new \LogicException('JSON Patch operation "'.$op['op'].'"" is not supported.'); + } + + + } + + if (empty($update['$set'])) { + unset($update['$set']); + } + if (empty($update['$unset'])) { + unset($update['$unset']); + } + + return $update; + } + + /** + * @param string $path + * + * @return string + */ + private function pathToDot($path) + { + $path = ltrim($path, '/'); + + return str_replace('/', '.', $path); + } +} diff --git a/src/MongodbStorage.php b/src/MongodbStorage.php index 4b1166e..49df0f1 100644 --- a/src/MongodbStorage.php +++ b/src/MongodbStorage.php @@ -17,6 +17,11 @@ class MongodbStorage */ private $hydrator; + /** + * @var ChangesCollector + */ + private $changesCollector; + /** * @var PessimisticLock */ @@ -25,12 +30,18 @@ class MongodbStorage /** * @param Collection $collection * @param Hydrator $hydrator + * @param ChangesCollector $changesCollector * @param PessimisticLock|null $pessimisticLock */ - public function __construct(Collection $collection, Hydrator $hydrator, PessimisticLock $pessimisticLock = null) - { + public function __construct( + Collection $collection, + Hydrator $hydrator, + ChangesCollector $changesCollector = null, + PessimisticLock $pessimisticLock = null + ) { $this->collection = $collection; $this->hydrator = $hydrator; + $this->changesCollector = $changesCollector ?: new ChangesCollector(); $this->pessimisticLock = $pessimisticLock; } @@ -50,15 +61,14 @@ public function create() */ public function insert($model, array $options = []) { - $values = get_values($model); - $result = $this->collection->insertOne($values, $options); + $result = $this->collection->insertOne(get_values($model), $options); if (false == $result->isAcknowledged()) { throw new \LogicException('Operation is not acknowledged'); } - $this->hydrator->hydrate($values, $model); set_object_id($model, $result->getInsertedId()); + $this->changesCollector->register($model); return $result; } @@ -67,13 +77,13 @@ public function insert($model, array $options = []) * @param object[] $models * @param array $options * - * @return \MongoDB\InsertOneResult + * @return \MongoDB\InsertManyResult */ public function insertMany(array $models, array $options = []) { $data = []; foreach ($models as $key => $model) { - $data[$key] = get_values($model); + $data[$key] =get_values($model); } $result = $this->collection->insertMany($data, $options); @@ -84,6 +94,8 @@ public function insertMany(array $models, array $options = []) foreach ($result->getInsertedIds() as $key => $modelId) { $this->hydrator->hydrate($data[$key], $models[$key]); set_object_id($models[$key], $modelId); + + $this->changesCollector->register($models[$key]); } return $result; @@ -102,14 +114,15 @@ public function update($model, $filter = null, array $options = []) $filter = ['_id' => new ObjectID(get_object_id($model))]; } - $values = get_values($model); - unset($values['_id']); + $update = $this->changesCollector->changes($model); - $result = $this->collection->updateOne($filter, ['$set' => $values], $options); + $result = $this->collection->updateOne($filter, $update, $options); if (false == $result->isAcknowledged()) { throw new \LogicException('Operation is not acknowledged'); } + $this->changesCollector->register($model); + return $result; } @@ -121,15 +134,17 @@ public function update($model, $filter = null, array $options = []) */ public function delete($model, array $options = []) { - $modelId = get_object_id($model); - $values = get_values($model); - unset($values['_id']); + $modelId = new ObjectID(get_object_id($model)); - $result = $this->collection->deleteOne(['_id' => new ObjectID($modelId)], $options); + $result = $this->collection->deleteOne(['_id' => $modelId], $options); if (false == $result->isAcknowledged()) { throw new \LogicException('Operation is not acknowledged'); } + // TODO remove id??? + + $this->changesCollector->unregister($model); + return $result; } @@ -144,7 +159,11 @@ public function findOne(array $filter = [], array $options = []) $options['typeMap'] = ['root' => 'array', 'document' => 'array', 'array' => 'array']; if ($values = $this->collection->findOne($filter, $options)) { - return $this->hydrator->hydrate($values); + $object = $this->hydrator->hydrate($values); + + $this->changesCollector->register($object); + + return $object; } } @@ -160,7 +179,11 @@ public function find(array $filter = [], array $options = []) $cursor->setTypeMap(['root' => 'array', 'document' => 'array', 'array' => 'array']); foreach ($cursor as $values) { - yield $this->hydrator->hydrate($values); + $object = $this->hydrator->hydrate($values); + + $this->changesCollector->register($object); + + yield $object; } } diff --git a/tests/ChangesCollectorTest.php b/tests/ChangesCollectorTest.php new file mode 100644 index 0000000..3aa77e9 --- /dev/null +++ b/tests/ChangesCollectorTest.php @@ -0,0 +1,168 @@ +createPersistedObject(); + + $collector = new ChangesCollector(); + $collector->register($obj); + + set_value($obj, 'aKey', 'aVal'); +var_dump($collector->changes($obj)); + self::assertEquals([ + '$set' => [ + 'aKey' => 'aVal', + ], + ], $collector->changes($obj)); + } + + public function testShouldTrackAddedValue() + { + $obj = $this->createPersistedObject(); + + $collector = new ChangesCollector(); + $collector->register($obj); + + add_value($obj, 'aKey', 'aVal'); + + self::assertEquals([ + '$set' => [ + 'aKey.0' => 'aVal', + ], + ], $collector->changes($obj)); + } + + public function testShouldNotTrackSetValueAndUnsetLater() + { + $obj = $this->createPersistedObject(); + + $collector = new ChangesCollector(); + $collector->register($obj); + + set_value($obj, 'aKey', 'aVal'); + set_value($obj, 'aKey', null); + + self::assertEquals([], $collector->changes($obj)); + } + + public function testShouldTrackUnsetValue() + { + $obj = $this->createPersistedObject(['aKey' => 'aVal']); + $collector = new ChangesCollector(); + $collector->register($obj); + + set_value($obj, 'aKey', null); + + self::assertEquals([ + '$unset' => [ + 'aKey' => '', + ] + ], $collector->changes($obj)); + } + + public function testShouldTrackChangedValue() + { + $obj = $this->createPersistedObject(['aKey' => 'aVal']); + + $collector = new ChangesCollector(); + $collector->register($obj); + + set_value($obj, 'aKey', 'aNewVal'); + + self::assertEquals([ + '$set' => [ + 'aKey' => 'aNewVal', + ], + ], $collector->changes($obj)); + } + + public function testShouldTrackStringValueChangedToArrayValue() + { + $obj = $this->createPersistedObject(['aKey' => 'aVal']); + + $collector = new ChangesCollector(); + $collector->register($obj); + + set_value($obj, 'aKey.fooKey', 'aFooVal'); + set_value($obj, 'aKey.barKey', 'aBarVal'); + + self::assertEquals([ + '$set' => [ + 'aKey' => [ + 'fooKey' => 'aFooVal', + 'barKey' => 'aBarVal', + ], + ], + ], $collector->changes($obj)); + } + + public function testShouldTrackArrayValueChangedToStringValue() + { + $obj = $this->createPersistedObject([ + 'aKey' => [ + 'fooKey' => 'aFooVal', + 'barKey' => 'aBarVal', + ] + ]); + + $collector = new ChangesCollector(); + $collector->register($obj); + + set_value($obj, 'aKey', 'aVal'); + + self::assertEquals([ + '$set' => [ + 'aKey' => 'aVal', + ], + ], $collector->changes($obj)); + } + + public function testShouldFoo() + { + $obj = $this->createPersistedObject([ + 'aKey' => 'aVal', + ]); + + $collector = new ChangesCollector(); + $collector->register($obj); + + set_value($obj, 'aKey', null); + set_value($obj, 'anotherKey', 'aVal'); + + self::assertEquals([ + '$set' => [ + 'anotherKey' => 'aVal', + ], + '$unset' => [ + 'aKey' => '', + ], + ], $collector->changes($obj)); + } + + /** + * @return Object + */ + private function createPersistedObject(array $values = []) + { + $obj = new Object(); + set_values($obj, $values); + set_object_id($obj, new ObjectID()); + + return $obj; + } +} \ No newline at end of file diff --git a/tests/Functional/MongodbStorageTest.php b/tests/Functional/MongodbStorageTest.php index f40f964..bff22df 100644 --- a/tests/Functional/MongodbStorageTest.php +++ b/tests/Functional/MongodbStorageTest.php @@ -108,7 +108,7 @@ public function testUpdateModelPessimisticLock() $collection = $this->database->selectCollection('storage_test'); $hydrator = new Hydrator(Model::class); - $storage = new MongodbStorage($collection, $hydrator, $pessimisticLock); + $storage = new MongodbStorage($collection, $hydrator, null, $pessimisticLock); $model = new Model(); $model->values = ['foo' => 'fooVal', 'bar' => 'barVal']; diff --git a/tests/Model/Object.php b/tests/Model/Object.php new file mode 100644 index 0000000..455babc --- /dev/null +++ b/tests/Model/Object.php @@ -0,0 +1,22 @@ +