diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index f925e3c..b7896e1 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - php-versions: ['7.3', '7.4', '8.0'] + php-versions: ['8.3', '8.4'] name: PHP ${{ matrix.php-versions }} steps: - uses: actions/checkout@v2 diff --git a/composer.json b/composer.json index 7978623..183437a 100644 --- a/composer.json +++ b/composer.json @@ -4,10 +4,11 @@ "license": "MIT", "minimum-stability": "stable", "require": { - "php": "^7.3|^8.0", + "php": "^8.3", "doctrine/orm": "^2.4.0", - "hostnet/entity-tracker-component": "^1.2.0||^2.0.0", - "psr/log": "^1.0.0|^2.0.0|^3.0.0" + "hostnet/entity-tracker-component": "^2.3.0", + "psr/log": "^1.0.0|^2.0.0|^3.0.0", + "symfony/cache": "^6.4|^7.4" }, "require-dev": { "hostnet/phpcs-tool": "^9.1.0", diff --git a/src/Attributes/Revision.php b/src/Attributes/Revision.php new file mode 100644 index 0000000..b84d3f0 --- /dev/null +++ b/src/Attributes/Revision.php @@ -0,0 +1,14 @@ +resolver = $resolver; - $this->factory = $factory; - $this->logger = $logger ?: new NullLogger(); } /** @@ -58,10 +37,11 @@ public function __construct( * Used to group all entities to the same revision * in the same flush if they use @Revision * - * @deprecated functionality was moved to entityChanged and postFlush + * @deprecated functionality was moved to entityChanged and postFlush, will be removed when removing + * doctrine/annotations. * @param PreFlushEventArgs $event */ - public function preFlush(PreFlushEventArgs $event) + public function preFlush(PreFlushEventArgs $event): void { trigger_error(__METHOD__ . ' is deprecated, please remove it from your event listener.', E_USER_DEPRECATED); $this->revision = $this->factory->createRevision(new \DateTime()); @@ -74,10 +54,8 @@ public function preFlush(PreFlushEventArgs $event) * in the same flush if they use @Revision. This method * can safely be overwritten if you prefer a Revision * per Request. - * - * @param PreFlushEventArgs $event */ - public function postFlush(PostFlushEventArgs $event) + public function postFlush(PostFlushEventArgs $event): void { $this->revision = null; } @@ -85,7 +63,7 @@ public function postFlush(PostFlushEventArgs $event) /** * @param EntityChangedEvent $event */ - public function entityChanged(EntityChangedEvent $event) + public function entityChanged(EntityChangedEvent $event): void { if (!$this->shouldBePersisted($event)) { return; @@ -112,15 +90,12 @@ public function entityChanged(EntityChangedEvent $event) * * @param EntityChangedEvent $event */ - private function shouldBePersisted(EntityChangedEvent $event) + private function shouldBePersisted(EntityChangedEvent $event): bool { - if (!($entity = $event->getCurrentEntity()) instanceof RevisionableInterface) { - return false; - } - - $em = $event->getEntityManager(); + $entity = $event->getCurrentEntity(); + $em = $event->getEntityManager(); - if (null === $this->resolver->getRevisionAnnotation($em, $entity)) { + if (!$this->isRevision($em, $entity)) { return false; } @@ -133,4 +108,36 @@ private function shouldBePersisted(EntityChangedEvent $event) return true; } + + private function isRevision($em, $entity): bool + { + $cache_key = base64_encode('REVISION-' . get_class($entity)); + $cached_item = $this->is_revision_cache->getItem($cache_key); + + if ($cached_item->isHit()) { + return $cached_item->get(); + } + + if (!($entity instanceof RevisionableInterface)) { + return $this->save($cached_item, false); + } + + if (null !== $this->resolver->getRevisionAttribute($em, $entity)) { + return $this->save($cached_item, true); + } + + if (null !== $this->resolver->getRevisionAnnotation($em, $entity)) { + return $this->save($cached_item, true); + } + + return $this->save($cached_item, false); + } + + private function save(CacheItemInterface $item, bool $value): bool + { + $item->set($value); + $this->is_revision_cache->save($item); + + return $value; + } } diff --git a/src/Resolver/RevisionResolver.php b/src/Resolver/RevisionResolver.php index 1a76c2b..d14bcef 100644 --- a/src/Resolver/RevisionResolver.php +++ b/src/Resolver/RevisionResolver.php @@ -7,40 +7,35 @@ namespace Hostnet\Component\EntityRevision\Resolver; use Doctrine\ORM\EntityManagerInterface; -use Hostnet\Component\EntityTracker\Provider\EntityAnnotationMetadataProvider; +use Hostnet\Component\EntityRevision\Attributes\Revision; +use Hostnet\Component\EntityRevision\Revision as RevisionAnnotation; +use Hostnet\Component\EntityTracker\Provider\EntityMetadataProvider; class RevisionResolver implements RevisionResolverInterface { - /** - * @var string - */ - private $annotation = 'Hostnet\Component\EntityRevision\Revision'; - - /** - * @var EntityAnnotationMetadataProvider - */ - private $provider; - - /** - * @param EntityMetadataProvider $provider - */ - public function __construct(EntityAnnotationMetadataProvider $provider) + public function __construct(private EntityMetadataProvider $provider) { - $this->provider = $provider; } /** * @see \Hostnet\Component\EntityRevision\Resolver\RevisionResolverInterface::getRevisionAnnotation() + * + * @deprecated Please use the Revision attribute instead */ - public function getRevisionAnnotation(EntityManagerInterface $em, $entity) + public function getRevisionAnnotation(EntityManagerInterface $em, $entity): ?RevisionAnnotation + { + return $this->provider->getAnnotationFromEntity($em, $entity, RevisionAnnotation::class); + } + + public function getRevisionAttribute(EntityManagerInterface $em, $entity): ?Revision { - return $this->provider->getAnnotationFromEntity($em, $entity, $this->annotation); + return $this->provider->getAttributeFromEntity(Revision::class, $em, $entity); } /** * @see \Hostnet\Component\EntityRevision\Resolver\RevisionResolverInterface::getRevisionableFields() */ - public function getRevisionableFields(EntityManagerInterface $em, $entity) + public function getRevisionableFields(EntityManagerInterface $em, $entity): array { $metadata = $em->getClassMetadata(get_class($entity)); return array_merge($metadata->getFieldNames(), $metadata->getAssociationNames()); diff --git a/src/Resolver/RevisionResolverInterface.php b/src/Resolver/RevisionResolverInterface.php index 3b49a84..2b29250 100644 --- a/src/Resolver/RevisionResolverInterface.php +++ b/src/Resolver/RevisionResolverInterface.php @@ -7,6 +7,8 @@ namespace Hostnet\Component\EntityRevision\Resolver; use Doctrine\ORM\EntityManagerInterface; +use Hostnet\Component\EntityRevision\Attributes\Revision; +use Hostnet\Component\EntityRevision\Revision as RevisionAnnotation; interface RevisionResolverInterface { @@ -15,12 +17,17 @@ interface RevisionResolverInterface * * @return string[] */ - public function getRevisionableFields(EntityManagerInterface $em, $entity); + public function getRevisionableFields(EntityManagerInterface $em, $entity): array; /** - * Return the revision annotation + * Return the revision annotation or null * - * @return Mutation + * @deprecated */ - public function getRevisionAnnotation(EntityManagerInterface $em, $entity); + public function getRevisionAnnotation(EntityManagerInterface $em, $entity): ?RevisionAnnotation; + + /** + * Return the revision attribute or null + */ + public function getRevisionAttribute(EntityManagerInterface $em, $entity): ?Revision; } diff --git a/src/Revision.php b/src/Revision.php index 5b5e50a..7cba5f1 100644 --- a/src/Revision.php +++ b/src/Revision.php @@ -11,6 +11,8 @@ /** * @Annotation * @Target({"CLASS"}) + * + * @deprecated Please use the attribute instead */ class Revision extends Tracked { diff --git a/src/RevisionInterface.php b/src/RevisionInterface.php index ea857c3..b110a10 100644 --- a/src/RevisionInterface.php +++ b/src/RevisionInterface.php @@ -6,6 +6,9 @@ namespace Hostnet\Component\EntityRevision; +/** + * @TODO: add returntypehints on next BC break, when removing doctrine/annotation support + */ interface RevisionInterface { /** diff --git a/src/RevisionableInterface.php b/src/RevisionableInterface.php index 0499a9f..ac0ec86 100644 --- a/src/RevisionableInterface.php +++ b/src/RevisionableInterface.php @@ -6,6 +6,9 @@ namespace Hostnet\Component\EntityRevision; +/** + * @TODO: add returntypehints on next BC break, when removing doctrine/annotation support + */ interface RevisionableInterface { /** diff --git a/test/Attributes/RevisionTest.php b/test/Attributes/RevisionTest.php new file mode 100644 index 0000000..e19a15e --- /dev/null +++ b/test/Attributes/RevisionTest.php @@ -0,0 +1,19 @@ +factory = $this->createMock(RevisionFactoryInterface::class); $this->resolver = $this->createMock(RevisionResolverInterface::class); $this->entity = $this->createMock(RevisionableInterface::class); - $this->revision = $this->createMock(RevisionInterface::class); $this->logger = $this->createMock(LoggerInterface::class); } @@ -80,7 +79,7 @@ public function testOnEntityChangedNoRevisionFields(): void $this->resolver ->expects($this->once()) ->method('getRevisionAnnotation') - ->willReturn(new Revision()); + ->willReturn(new RevisionAnnotation()); $this->resolver ->expects($this->once()) @@ -101,7 +100,7 @@ public function testOnEntityChangedNoTrackedMutations(): void $this->resolver ->expects($this->exactly(2)) ->method('getRevisionAnnotation') - ->willReturn(new Revision()); + ->willReturn(new RevisionAnnotation()); $this->resolver ->expects($this->exactly(2)) @@ -121,9 +120,9 @@ public function testOnEntityChangedNoTrackedMutations(): void $listener->entityChanged($event); } - public function testOnEntityChangedNoRevision(): void + public function testOnEntityChangedNoRevisionPresentOnFlush(): void { - $history = new Revision(); + $history = new RevisionAnnotation(); $this->resolver ->expects($this->once()) @@ -153,7 +152,7 @@ public function testOnEntityChanged(): void $r1 = $this->createMock('Hostnet\Component\EntityRevision\RevisionInterface'); $r2 = $this->createMock('Hostnet\Component\EntityRevision\RevisionInterface'); - $history = new Revision(); + $history = new RevisionAnnotation(); $this->resolver ->expects($this->any()) ->method('getRevisionAnnotation') @@ -191,4 +190,86 @@ public function testOnEntityChanged(): void $listener->entityChanged($event); $listener->entityChanged($event); } + + public function testOnEntityChangedInterfaceOnlyNoAnnotationNoAttribute(): void + { + $this->resolver + ->expects($this->any()) + ->method('getRevisionAnnotation') + ->willReturn(null); + + $this->resolver + ->expects($this->never()) + ->method('getRevisionableFields'); + + $this->factory + ->expects($this->never()) + ->method('createRevision'); + + $this->em + ->expects($this->never()) + ->method('persist'); + + $this->entity + ->expects($this->never()) + ->method('setRevision'); + + $event = new EntityChangedEvent($this->em, $this->entity, $this->entity, ['something']); + $doctrine_event = $this + ->getMockBuilder('Doctrine\ORM\Event\PostFlushEventArgs') + ->disableOriginalConstructor() + ->getMock(); + + $listener = new RevisionListener($this->resolver, $this->factory); + $listener->entityChanged($event); + $listener->postFlush($doctrine_event); + } + + public function testOnEntityChangedAttribute(): void + { + $r1 = $this->createMock('Hostnet\Component\EntityRevision\RevisionInterface'); + $r2 = $this->createMock('Hostnet\Component\EntityRevision\RevisionInterface'); + + $this->resolver + ->expects($this->any()) + ->method('getRevisionAnnotation') + ->willReturn(null); + + $this->resolver + ->expects($this->once()) + ->method('getRevisionAttribute') + ->willReturn(new Revision()); + + $this->resolver + ->expects($this->any()) + ->method('getRevisionableFields') + ->willReturn(['something']); + + $this->factory + ->expects($this->exactly(2)) + ->method('createRevision') + ->willReturnOnConsecutiveCalls($r1, $r2); + + $this->em + ->expects($this->any()) + ->method('persist') + ->withConsecutive([$this->identicalTo($r1)], [$this->identicalTo($r2)]); + + $this->entity + ->expects($this->exactly(3)) + ->method('setRevision') + ->withConsecutive([$this->identicalTo($r1)], [$this->identicalTo($r2)], [$this->identicalTo($r2)]); + + $event = new EntityChangedEvent($this->em, $this->entity, $this->entity, ['something']); + $doctrine_event = $this + ->getMockBuilder('Doctrine\ORM\Event\PostFlushEventArgs') + ->disableOriginalConstructor() + ->getMock(); + + $listener = new RevisionListener($this->resolver, $this->factory); + $listener->entityChanged($event); + $listener->postFlush($doctrine_event); + $listener->entityChanged($event); + $listener->entityChanged($event); + } } diff --git a/test/Resolver/RevisionResolverTest.php b/test/Resolver/RevisionResolverTest.php index 6bab4e0..9c3a04e 100644 --- a/test/Resolver/RevisionResolverTest.php +++ b/test/Resolver/RevisionResolverTest.php @@ -8,7 +8,8 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; -use Hostnet\Component\EntityRevision\Revision; +use Hostnet\Component\EntityRevision\Attributes\Revision; +use Hostnet\Component\EntityRevision\Revision as RevisionAnnotation; use Hostnet\Component\EntityTracker\Provider\EntityAnnotationMetadataProvider; use PHPUnit\Framework\TestCase; @@ -43,7 +44,7 @@ public function testGetRevisionAnnotation(): void $this->provider ->expects($this->once()) ->method('getAnnotationFromEntity') - ->with($this->em, $entity, Revision::class); + ->with($this->em, $entity, RevisionAnnotation::class); $this->resolver->getRevisionAnnotation($this->em, $entity); } @@ -63,4 +64,19 @@ public function testGetRevisionableFields(): void $this->assertEquals(['id', 'test'], $this->resolver->getRevisionableFields($this->em, $entity)); } + + public function testGetRevisionAttribute(): void + { + $entity = new \stdClass(); + + $attribute = new Revision(); + + $this->provider + ->expects($this->once()) + ->method('getAttributeFromEntity') + ->with(Revision::class, $this->em, $entity) + ->willReturn($attribute); + + self::assertSame($attribute, $this->resolver->getRevisionAttribute($this->em, $entity)); + } }