From 44526959e27ee02d2c11342e220e049303e57d31 Mon Sep 17 00:00:00 2001 From: Damien Harper Date: Thu, 24 Oct 2024 11:52:47 +0200 Subject: [PATCH] Better JSON support (#225) * Tweak `phpdoc_to_comment` rule (PHP-CS-Fixer ) * auditor now keeps track of changes inside json data (addition/removal/update of fields) --- .php-cs-fixer.dist.php | 3 + .../Auditing/Transaction/AuditTrait.php | 101 ++++++- .../{ => Auditing}/InheritanceTest.php | 14 +- tests/Provider/Doctrine/Auditing/JsonTest.php | 259 ++++++++++++++++++ .../Transaction/TransactionProcessorTest.php | 2 - .../Provider/Doctrine/Issues/Issue119Test.php | 11 +- 6 files changed, 367 insertions(+), 23 deletions(-) rename tests/Provider/Doctrine/{ => Auditing}/InheritanceTest.php (89%) create mode 100644 tests/Provider/Doctrine/Auditing/JsonTest.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 956448d..3d73b3f 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -30,6 +30,9 @@ 'phpdoc_to_param_type' => true, 'phpdoc_to_property_type' => true, 'phpdoc_to_return_type' => true, + 'phpdoc_to_comment' => [ + 'ignored_tags' => ['todo', 'var'] + ], 'regular_callable_call' => true, 'simplified_if_return' => true, 'get_class_to_class_keyword' => true, diff --git a/src/Provider/Doctrine/Auditing/Transaction/AuditTrait.php b/src/Provider/Doctrine/Auditing/Transaction/AuditTrait.php index 8d77606..b1aa624 100644 --- a/src/Provider/Doctrine/Auditing/Transaction/AuditTrait.php +++ b/src/Provider/Doctrine/Auditing/Transaction/AuditTrait.php @@ -129,7 +129,36 @@ private function value(EntityManagerInterface $entityManager, Type $type, mixed } /** - * Computes a usable diff. + * Computes a usable diff formatted as follow: + * [ + * // field1 value has changed + * 'field1' => [ + * 'old' => $oldValue, // value before change + * 'new' => $newValue // value after change + * ], + * // field2 value has been added + * 'field2' => [ + * 'new' => $newValue + * ], + * ... + * // jsonField1 has changed + * 'jsonField1' => [ + * // field1 value has changed + * 'field1' => [ + * 'old' => $oldValue, + * 'new' => $newValue + * ], + * // field2 value has been added + * 'field2' => [ + * 'new' => $newValue + * ], + * // field3 value has been removed + * 'field3' => [ + * 'old' => $oldValue + * ], + * ... + * ], + * ] * * @throws MappingException * @throws Exception @@ -152,6 +181,7 @@ private function diff(EntityManagerInterface $entityManager, object $entity, arr continue; } + $type = null; if ( !isset($meta->embeddedClasses[$fieldName]) && $meta->hasField($fieldName) @@ -172,10 +202,20 @@ private function diff(EntityManagerInterface $entityManager, object $entity, arr } if ($o !== $n) { - $diff[$fieldName] = [ - 'new' => $n, - 'old' => $o, - ]; + if (isset($type) && Type::getType(Types::JSON) === $type) { + /** + * @var ?array $o + * @var ?array $n + */ + $diff[$fieldName] = $this->deepDiff($o, $n); + } else { + if (null !== $o) { + $diff[$fieldName]['old'] = $o; + } + if (null !== $n) { + $diff[$fieldName]['new'] = $n; + } + } } } @@ -257,4 +297,55 @@ private function blame(): array 'username' => $username, ]; } + + private function deepDiff(?array $old, ?array $new): array + { + $diff = []; + + // Check for differences in $old + if (null !== $old && null !== $new) { + foreach ($old as $key => $value) { + if (!\array_key_exists($key, $new)) { + // $key does not exist in $new, it's been removed + $diff[$key] = \is_array($value) ? $this->formatArray($value, 'old') : ['old' => $value]; + } elseif (\is_array($value) && \is_array($new[$key])) { + // both values are arrays, compare them recursively + $recursiveDiff = $this->deepDiff($value, $new[$key]); + if ([] !== $recursiveDiff) { + $diff[$key] = $recursiveDiff; + } + } elseif ($new[$key] !== $value) { + // values are different + $diff[$key] = ['old' => $value, 'new' => $new[$key]]; + } + } + } + + // Check for new elements in $new + if (null !== $new) { + foreach ($new as $key => $value) { + if (!\array_key_exists($key, $old ?? [])) { + // $key does not exist in $old, it's been added + $diff[$key] = \is_array($value) ? $this->formatArray($value, 'new') : ['new' => $value]; + } + } + } + + return $diff; + } + + private function formatArray(array $array, string $prefix): array + { + $result = []; + + foreach ($array as $key => $value) { + if (\is_array($value)) { + $result[$key] = $this->formatArray($value, $prefix); + } else { + $result[$key][$prefix] = $value; + } + } + + return $result; + } } diff --git a/tests/Provider/Doctrine/InheritanceTest.php b/tests/Provider/Doctrine/Auditing/InheritanceTest.php similarity index 89% rename from tests/Provider/Doctrine/InheritanceTest.php rename to tests/Provider/Doctrine/Auditing/InheritanceTest.php index 6782335..26199e8 100644 --- a/tests/Provider/Doctrine/InheritanceTest.php +++ b/tests/Provider/Doctrine/Auditing/InheritanceTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace DH\Auditor\Tests\Provider\Doctrine; +namespace DH\Auditor\Tests\Provider\Doctrine\Auditing; use DH\Auditor\Provider\Doctrine\DoctrineProvider; use DH\Auditor\Provider\Doctrine\Service\AuditingService; @@ -111,19 +111,11 @@ private function createAndInitDoctrineProvider(): void $this->provider = new DoctrineProvider($this->createProviderConfiguration()); $entityManager = $this->createEntityManager([ - __DIR__.'/../../../src/Provider/Doctrine/Auditing/Annotation', - __DIR__.'/Fixtures/Entity/Inheritance', + __DIR__.'/../../../../src/Provider/Doctrine/Auditing/Annotation', + __DIR__.'/../Fixtures/Entity/Inheritance', ]); $this->provider->registerStorageService(new StorageService('default', $entityManager)); $this->provider->registerAuditingService(new AuditingService('default', $entityManager)); - // $this->provider->registerEntityManager( - // $this->createEntityManager([ - // __DIR__.'/../../../src/Provider/Doctrine/Auditing/Annotation', - // __DIR__.'/Fixtures/Entity/Inheritance', - // ]), - // DoctrineProvider::BOTH, - // 'default' - // ); $auditor->registerProvider($this->provider); } diff --git a/tests/Provider/Doctrine/Auditing/JsonTest.php b/tests/Provider/Doctrine/Auditing/JsonTest.php new file mode 100644 index 0000000..803f34e --- /dev/null +++ b/tests/Provider/Doctrine/Auditing/JsonTest.php @@ -0,0 +1,259 @@ + $this->provider->getStorageServiceForEntity(DummyEntity::class), + ]; + + $reader = $this->createReader(); + + $dummy = new DummyEntity(); + $dummy + ->setLabel('label') + ->setJsonArray([ + 'field1' => 'value1', + 'field2' => 'value2', + 'field3' => [ + 'field3.1' => 'value3.1', + 'field3.2' => 'value3.2', + ], + ]) + ; + + $storageServices[DummyEntity::class]->getEntityManager()->persist($dummy); + $this->flushAll($storageServices); + + $audits = $reader->createQuery(DummyEntity::class)->execute(); + $this->assertCount(1, $audits, 'results count ok.'); + $entry = array_shift($audits); + $this->assertSame([ + 'json_array' => [ + 'field1' => [ + 'new' => 'value1', + ], + 'field2' => [ + 'new' => 'value2', + ], + 'field3' => [ + 'field3.1' => [ + 'new' => 'value3.1', + ], + 'field3.2' => [ + 'new' => 'value3.2', + ], + ], + ], + 'label' => [ + 'new' => 'label', + ], + ], $entry->getDiffs(), 'audit entry diffs is ok.'); + } + + public function testJsonUpdateExistingField(): void + { + $storageServices = [ + DummyEntity::class => $this->provider->getStorageServiceForEntity(DummyEntity::class), + ]; + + $reader = $this->createReader(); + + $dummy = new DummyEntity(); + $dummy + ->setLabel('label') + ->setJsonArray([ + 'field1' => 'value1', + 'field2' => 'value2', + 'field3' => [ + 'field3.1' => 'value3.1', + 'field3.2' => 'value3.2', + ], + ]) + ; + + $storageServices[DummyEntity::class]->getEntityManager()->persist($dummy); + $this->flushAll($storageServices); + + $dummy->setJsonArray([ + 'field1' => 'new value1', + 'field2' => 'value2', + 'field3' => [ + 'field3.1' => 'new value3.1', + 'field3.2' => 'value3.2', + ], + ]); + + $storageServices[DummyEntity::class]->getEntityManager()->persist($dummy); + $this->flushAll($storageServices); + + $audits = $reader->createQuery(DummyEntity::class)->execute(); + $this->assertCount(2, $audits, 'results count ok.'); + $entry = array_shift($audits); + $this->assertSame([ + 'json_array' => [ + 'field1' => [ + 'new' => 'new value1', + 'old' => 'value1', + ], + 'field3' => [ + 'field3.1' => [ + 'new' => 'new value3.1', + 'old' => 'value3.1', + ], + ], + ], + ], $entry->getDiffs(), 'audit entry diffs is ok.'); + } + + public function testJsonAddNewField(): void + { + $storageServices = [ + DummyEntity::class => $this->provider->getStorageServiceForEntity(DummyEntity::class), + ]; + + $reader = $this->createReader(); + + $dummy = new DummyEntity(); + $dummy + ->setLabel('label') + ->setJsonArray([ + 'field1' => 'value1', + 'field2' => 'value2', + 'field3' => [ + 'field3.1' => 'value3.1', + 'field3.2' => 'value3.2', + ], + ]) + ; + + $storageServices[DummyEntity::class]->getEntityManager()->persist($dummy); + $this->flushAll($storageServices); + + $dummy->setJsonArray([ + 'field1' => 'value1', + 'field2' => 'value2', + 'field3' => [ + 'field3.1' => 'value3.1', + 'field3.2' => 'value3.2', + 'field3.3' => 'value3.3', + 'field3.4' => 'value3.4', + ], + 'field4' => 'value4', + 'field5' => 'value5', + ]); + + $storageServices[DummyEntity::class]->getEntityManager()->persist($dummy); + $this->flushAll($storageServices); + + $audits = $reader->createQuery(DummyEntity::class)->execute(); + $this->assertCount(2, $audits, 'results count ok.'); + $entry = array_shift($audits); + $this->assertSame([ + 'json_array' => [ + 'field3' => [ + 'field3.3' => [ + 'new' => 'value3.3', + ], + 'field3.4' => [ + 'new' => 'value3.4', + ], + ], + 'field4' => [ + 'new' => 'value4', + ], + 'field5' => [ + 'new' => 'value5', + ], + ], + ], $entry->getDiffs(), 'audit entry diffs is ok.'); + } + + public function testJsonRemoveField(): void + { + $storageServices = [ + DummyEntity::class => $this->provider->getStorageServiceForEntity(DummyEntity::class), + ]; + + $reader = $this->createReader(); + + $dummy = new DummyEntity(); + $dummy + ->setLabel('label') + ->setJsonArray([ + 'field1' => 'value1', + 'field2' => 'value2', + 'field3' => [ + 'field3.1' => 'value3.1', + 'field3.2' => 'value3.2', + ], + 'field4' => [ + 'field4.1' => 'value4.1', + 'field4.2' => 'value4.2', + ], + ]) + ; + + $storageServices[DummyEntity::class]->getEntityManager()->persist($dummy); + $this->flushAll($storageServices); + + $dummy->setJsonArray([ + 'field1' => 'value1', + 'field3' => [ + 'field3.2' => 'value3.2', + ], + ]); + + $storageServices[DummyEntity::class]->getEntityManager()->persist($dummy); + $this->flushAll($storageServices); + + $audits = $reader->createQuery(DummyEntity::class)->execute(); + $this->assertCount(2, $audits, 'results count ok.'); + $entry = array_shift($audits); + $this->assertSame([ + 'json_array' => [ + 'field2' => [ + 'old' => 'value2', + ], + 'field3' => [ + 'field3.1' => [ + 'old' => 'value3.1', + ], + ], + 'field4' => [ + 'field4.1' => [ + 'old' => 'value4.1', + ], + 'field4.2' => [ + 'old' => 'value4.2', + ], + ], + ], + ], $entry->getDiffs(), 'audit entry diffs is ok.'); + } + + private function configureEntities(): void + { + $this->provider->getConfiguration()->setEntities([ + DummyEntity::class => ['enabled' => true], + ]); + } +} diff --git a/tests/Provider/Doctrine/Auditing/Transaction/TransactionProcessorTest.php b/tests/Provider/Doctrine/Auditing/Transaction/TransactionProcessorTest.php index 5e7f772..3a3fd9f 100644 --- a/tests/Provider/Doctrine/Auditing/Transaction/TransactionProcessorTest.php +++ b/tests/Provider/Doctrine/Auditing/Transaction/TransactionProcessorTest.php @@ -64,11 +64,9 @@ public function testInsert(): void $this->assertSame([ 'email' => [ 'new' => 'john.doe@gmail.com', - 'old' => null, ], 'fullname' => [ 'new' => 'John Doe', - 'old' => null, ], ], $entry->getDiffs(), 'audit entry diffs is ok.'); } diff --git a/tests/Provider/Doctrine/Issues/Issue119Test.php b/tests/Provider/Doctrine/Issues/Issue119Test.php index fff6e4f..0a2641a 100644 --- a/tests/Provider/Doctrine/Issues/Issue119Test.php +++ b/tests/Provider/Doctrine/Issues/Issue119Test.php @@ -29,9 +29,10 @@ public function testIssue119(): void $transaction = new Transaction($entityManager); $entity = new DummyEntity(); $transaction->insert($entity, [ - 'json_array' => [null, [ - 'example' => '例', - ]], + 'json_array' => [ + null, + ['example' => '例'], + ], ]); $processor->process($transaction); $audits = $reader->createQuery(DummyEntity::class)->execute(); @@ -41,8 +42,8 @@ public function testIssue119(): void $audit = $audits[0]; $diffs = $audit->getDiffs()['json_array']; $this->assertSame([ - 'example' => '例', - ], $diffs['new']); + 'example' => ['new' => '例'], + ], $diffs); } private function configureEntities(): void