From 10f380b088f4c8a2fb635dc63660d5d238d84844 Mon Sep 17 00:00:00 2001 From: Jan Lam Date: Mon, 14 Sep 2026 10:14:59 +0200 Subject: [PATCH 1/7] Update README for attribute-based Revision setup The docs were still describing the doctrine/annotations-based @Revision annotation and EntityAnnotationMetadataProvider wiring, which no longer exist now that the component only supports the #[Revision] attribute. Co-Authored-By: Claude Sonnet 5 --- README.md | 61 +++++++++++++++++++----------------------------- src/Revision.php | 19 --------------- 2 files changed, 24 insertions(+), 56 deletions(-) delete mode 100644 src/Revision.php diff --git a/README.md b/README.md index f9bc4e0..1878bff 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ This component lets you automatically store revisions for a set of entities per Requirements ------------ -The revision component requires a minimum of php 7.3 and runs on Doctrine2. For specific requirements, please check [composer.json](../master/composer.json). +The revision component requires a minimum of php 8.3 and runs on Doctrine2. For specific requirements, please check [composer.json](../master/composer.json). Installation ------------ @@ -46,7 +46,7 @@ Documentation How does it work? ----------------- -It works by putting the `@Revision` annotation and RevisionableInterface on your Entity and registering the listener on the entityChanged event, assuming you have already configured the [Entity Tracker Component](https://github.com/hostnet/entity-tracker-component/#setup). +It works by putting the `#[Revision]` attribute and RevisionableInterface on your Entity and registering the listener on the entityChanged event, assuming you have already configured the [Entity Tracker Component](https://github.com/hostnet/entity-tracker-component/#setup). For a usage example, follow the setup below. @@ -54,7 +54,7 @@ For a usage example, follow the setup below. Setup ----- - - You have to add `@Revision` to your entity + - You have to add `#[Revision]` to your entity - You have to add the RevisionableInterface to your entity - You need a Revision Entity @@ -72,29 +72,26 @@ It might look a bit complicated to set up, but it's pretty much setting up the t use Acme\Bundle\AcmeBundle\AcmeRevisionFactory; use Hostnet\Component\EntityRevision\Resolver\RevisionResolver; use Hostnet\Component\EntityTracker\Listener\EntityChangedListener; -use Hostnet\Component\EntityTracker\Provider\EntityAnnotationMetadataProvider; +use Hostnet\Component\EntityTracker\Provider\EntityMetadataProvider; use Hostnet\Component\EntityTracker\Provider\EntityMutationMetadataProvider; /* @var $em \Doctrine\ORM\EntityManager */ $event_manager = $em->getEventManager(); -// default doctrine annotation reader -$annotation_reader = new AnnotationReader(); - // setup required providers -$mutation_metadata_provider = new EntityMutationMetadataProvider($annotation_reader); -$annotation_metadata_provider = new EntityAnnotationMetadataProvider($annotation_reader); +$mutation_metadata_provider = new EntityMutationMetadataProvider(); +$metadata_provider = new EntityMetadataProvider(); $logger = ...; // instance of LoggerInterface from the psr/log package, optional argument - -// pre flush event listener that uses the @Revision annotation + +// pre flush event listener that uses the #[Tracked]-derived attributes, such as #[Revision] $entity_changed_listener = new EntityChangedListener( + $metadata_provider, $mutation_metadata_provider, - $annotation_metadata_provider, $logger ); -// the resolver is used to find the correct annotation -$revision_resolver = new RevisionResolver($annotation_metadata_provider); +// the resolver is used to find the correct attribute +$revision_resolver = new RevisionResolver($metadata_provider); // this factory will provide the revision and as author Henk $revision_factory = new AcmeRevisionFactory('Henk'); @@ -118,22 +115,15 @@ Revisions are stored in the database, thus need an Entity. You're free to decide use Doctrine\ORM\Mapping as ORM; use Hostnet\Component\EntityRevision\RevisionInterface; -/** - * @ORM\Entity - */ +#[ORM\Entity] class Revision implements RevisionInterface { - - /** - * @ORM\... - */ + #[ORM\...] private $author; - - /** - * @ORM\... - */ + + #[ORM\...] private $created_at; - + public function __construct($author, \DateTime $created_at) { $this->author = $author; @@ -155,28 +145,25 @@ class Revision implements RevisionInterface #### Configuring the Entity -All we have to do now is put the `@Revision` annotation and RevisionableInterface on our Entity. +All we have to do now is put the `#[Revision]` attribute and RevisionableInterface on our Entity. ```php use Doctrine\ORM\Mapping as ORM; +use Hostnet\Component\EntityRevision\Attributes\Revision as RevisionAttribute; use Hostnet\Component\EntityRevision\RevisionableInterface; -/** - * @ORM\Entity - * @Revision - */ +#[ORM\Entity] +#[RevisionAttribute] class MyEntity implements RevisionableInterface { /** * The current revision of the contact person - * - * @ORM\ManyToOne(targetEntity="Revision") - * @ORM\JoinColumn(name="revision_id", referencedColumnName="id") - * @var Revision */ + #[ORM\ManyToOne(targetEntity: Revision::class)] + #[ORM\JoinColumn(name: 'revision_id', referencedColumnName: 'id')] private $revision; - + public function setRevision(RevisionInterface $revision) { $this->revision = $revision; @@ -217,7 +204,7 @@ class AcmeRevisionFactory implements RevisionFactoryInterface ``` #### What's next? -All you have to do now is change multiple entities with `@Revision` and they will be grouped by a Revision Entity. +All you have to do now is change multiple entities with `#[Revision]` and they will be grouped by a Revision Entity. ```php // imagine all instances to be Revisionable $user->setName('Henk'); diff --git a/src/Revision.php b/src/Revision.php deleted file mode 100644 index 7cba5f1..0000000 --- a/src/Revision.php +++ /dev/null @@ -1,19 +0,0 @@ - Date: Mon, 14 Sep 2026 10:16:03 +0200 Subject: [PATCH 2/7] Remove annotation support --- composer.json | 2 +- src/Listener/RevisionListener.php | 4 -- src/Resolver/RevisionResolver.php | 11 ---- src/Resolver/RevisionResolverInterface.php | 8 --- test/Listener/RevisionListenerTest.php | 70 +++------------------- test/Resolver/RevisionResolverTest.php | 17 +----- 6 files changed, 11 insertions(+), 101 deletions(-) diff --git a/composer.json b/composer.json index 183437a..badfeaa 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "require": { "php": "^8.3", "doctrine/orm": "^2.4.0", - "hostnet/entity-tracker-component": "^2.3.0", + "hostnet/entity-tracker-component": "^3.0.0", "psr/log": "^1.0.0|^2.0.0|^3.0.0", "symfony/cache": "^6.4|^7.4" }, diff --git a/src/Listener/RevisionListener.php b/src/Listener/RevisionListener.php index 003c881..fb98efa 100644 --- a/src/Listener/RevisionListener.php +++ b/src/Listener/RevisionListener.php @@ -126,10 +126,6 @@ private function isRevision($em, $entity): bool 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); } diff --git a/src/Resolver/RevisionResolver.php b/src/Resolver/RevisionResolver.php index d14bcef..f9aad8c 100644 --- a/src/Resolver/RevisionResolver.php +++ b/src/Resolver/RevisionResolver.php @@ -8,7 +8,6 @@ use Doctrine\ORM\EntityManagerInterface; use Hostnet\Component\EntityRevision\Attributes\Revision; -use Hostnet\Component\EntityRevision\Revision as RevisionAnnotation; use Hostnet\Component\EntityTracker\Provider\EntityMetadataProvider; class RevisionResolver implements RevisionResolverInterface @@ -17,16 +16,6 @@ public function __construct(private EntityMetadataProvider $provider) { } - /** - * @see \Hostnet\Component\EntityRevision\Resolver\RevisionResolverInterface::getRevisionAnnotation() - * - * @deprecated Please use the Revision attribute instead - */ - 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->getAttributeFromEntity(Revision::class, $em, $entity); diff --git a/src/Resolver/RevisionResolverInterface.php b/src/Resolver/RevisionResolverInterface.php index 2b29250..9cb5de0 100644 --- a/src/Resolver/RevisionResolverInterface.php +++ b/src/Resolver/RevisionResolverInterface.php @@ -8,7 +8,6 @@ use Doctrine\ORM\EntityManagerInterface; use Hostnet\Component\EntityRevision\Attributes\Revision; -use Hostnet\Component\EntityRevision\Revision as RevisionAnnotation; interface RevisionResolverInterface { @@ -19,13 +18,6 @@ interface RevisionResolverInterface */ public function getRevisionableFields(EntityManagerInterface $em, $entity): array; - /** - * Return the revision annotation or null - * - * @deprecated - */ - public function getRevisionAnnotation(EntityManagerInterface $em, $entity): ?RevisionAnnotation; - /** * Return the revision attribute or null */ diff --git a/test/Listener/RevisionListenerTest.php b/test/Listener/RevisionListenerTest.php index 871a3f4..67ef483 100644 --- a/test/Listener/RevisionListenerTest.php +++ b/test/Listener/RevisionListenerTest.php @@ -78,8 +78,8 @@ public function testOnEntityChangedNoRevisionFields(): void { $this->resolver ->expects($this->once()) - ->method('getRevisionAnnotation') - ->willReturn(new RevisionAnnotation()); + ->method('getRevisionAttribute') + ->willReturn(new Revision()); $this->resolver ->expects($this->once()) @@ -99,8 +99,8 @@ public function testOnEntityChangedNoTrackedMutations(): void { $this->resolver ->expects($this->exactly(2)) - ->method('getRevisionAnnotation') - ->willReturn(new RevisionAnnotation()); + ->method('getRevisionAttribute') + ->willReturn(new Revision()); $this->resolver ->expects($this->exactly(2)) @@ -122,12 +122,12 @@ public function testOnEntityChangedNoTrackedMutations(): void public function testOnEntityChangedNoRevisionPresentOnFlush(): void { - $history = new RevisionAnnotation(); + $revision = new Revision(); $this->resolver ->expects($this->once()) - ->method('getRevisionAnnotation') - ->willReturn($history); + ->method('getRevisionAttribute') + ->willReturn($revision); $this->resolver ->expects($this->once()) @@ -147,57 +147,8 @@ public function testOnEntityChangedNoRevisionPresentOnFlush(): void $listener->entityChanged($event); } - public function testOnEntityChanged(): void + public function testOnEntityChangedInterfaceOnlyNoAttribute(): void { - $r1 = $this->createMock('Hostnet\Component\EntityRevision\RevisionInterface'); - $r2 = $this->createMock('Hostnet\Component\EntityRevision\RevisionInterface'); - - $history = new RevisionAnnotation(); - $this->resolver - ->expects($this->any()) - ->method('getRevisionAnnotation') - ->willReturn($history); - - $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); - } - - public function testOnEntityChangedInterfaceOnlyNoAnnotationNoAttribute(): void - { - $this->resolver - ->expects($this->any()) - ->method('getRevisionAnnotation') - ->willReturn(null); - $this->resolver ->expects($this->never()) ->method('getRevisionableFields'); @@ -230,11 +181,6 @@ 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') diff --git a/test/Resolver/RevisionResolverTest.php b/test/Resolver/RevisionResolverTest.php index 9c3a04e..95bb4d5 100644 --- a/test/Resolver/RevisionResolverTest.php +++ b/test/Resolver/RevisionResolverTest.php @@ -9,8 +9,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Hostnet\Component\EntityRevision\Attributes\Revision; -use Hostnet\Component\EntityRevision\Revision as RevisionAnnotation; -use Hostnet\Component\EntityTracker\Provider\EntityAnnotationMetadataProvider; +use Hostnet\Component\EntityTracker\Provider\EntityMetadataProvider; use PHPUnit\Framework\TestCase; /** @@ -25,7 +24,7 @@ class RevisionResolverTest extends TestCase public function setUp(): void { $this->provider = $this - ->getMockBuilder(EntityAnnotationMetadataProvider::class) + ->getMockBuilder(EntityMetadataProvider::class) ->disableOriginalConstructor() ->getMock(); @@ -37,18 +36,6 @@ public function setUp(): void $this->resolver = new RevisionResolver($this->provider); } - public function testGetRevisionAnnotation(): void - { - $entity = new \stdClass(); - - $this->provider - ->expects($this->once()) - ->method('getAnnotationFromEntity') - ->with($this->em, $entity, RevisionAnnotation::class); - - $this->resolver->getRevisionAnnotation($this->em, $entity); - } - public function testGetRevisionableFields(): void { $entity = new \stdClass(); From 099085db66b6bd55ec161da8743b85baf849525d Mon Sep 17 00:00:00 2001 From: Jan Lam Date: Mon, 14 Sep 2026 10:21:40 +0200 Subject: [PATCH 3/7] Modernize code to PHP 8.3 standards, now that doctrine/annotations is gone Add return type hints to RevisionableInterface, RevisionInterface and RevisionFactoryInterface (fulfilling the @TODO left for this exact BC break), and simplify the code that relied on their previously loose typing. Also removes RevisionListener::preFlush(), deprecated since 1.1 and explicitly slated for removal once doctrine/annotations support was dropped. Along the way: fix a broken sprintf() call that meant the entity class was never actually logged, use $entity::class instead of get_class(), and tighten type hints on private/internal methods and tests. Co-Authored-By: Claude Sonnet 5 --- UPGRADE-2.0.md | 22 +++++++++ src/Factory/RevisionFactoryInterface.php | 7 ++- src/Listener/RevisionListener.php | 41 +++------------- src/Resolver/RevisionResolver.php | 6 +-- src/Resolver/RevisionResolverInterface.php | 4 +- src/RevisionInterface.php | 11 +---- src/RevisionableInterface.php | 11 +---- test/Listener/RevisionListenerTest.php | 57 +++------------------- test/Resolver/RevisionResolverTest.php | 21 +++----- 9 files changed, 56 insertions(+), 124 deletions(-) create mode 100644 UPGRADE-2.0.md diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md new file mode 100644 index 0000000..73f33ec --- /dev/null +++ b/UPGRADE-2.0.md @@ -0,0 +1,22 @@ +UPGRADE FROM 1.x to 2.0 +=============================== + +### General Changes + +- Dropped support for `doctrine/annotations`. Entities must use the `#[Revision]` + attribute instead of the `@Revision` annotation. + +- Removed `RevisionListener::preFlush()`. This method has been deprecated since 1.1 + and its functionality already lives in `RevisionListener::entityChanged()` and + `RevisionListener::postFlush()`. Remove any `preFlush` registration of the + `RevisionListener` from your event manager, if you still have one. + +- Added return type hints to `RevisionableInterface`, `RevisionInterface` and + `RevisionFactoryInterface`. Any implementation of these interfaces must add + matching return types: + - `RevisionableInterface::setRevision()`: `void` + - `RevisionableInterface::getRevision()`: `RevisionInterface` + - `RevisionInterface::getUser()`: `string` + - `RevisionInterface::getCreatedAt()`: `\DateTimeInterface` + - `RevisionFactoryInterface::createRevision()`: `RevisionInterface` (no longer + nullable) diff --git a/src/Factory/RevisionFactoryInterface.php b/src/Factory/RevisionFactoryInterface.php index 86f8072..5efc72e 100644 --- a/src/Factory/RevisionFactoryInterface.php +++ b/src/Factory/RevisionFactoryInterface.php @@ -6,13 +6,12 @@ namespace Hostnet\Component\EntityRevision\Factory; +use Hostnet\Component\EntityRevision\RevisionInterface; + interface RevisionFactoryInterface { /** * Create a revision - * - * @param \DateTime $created_at - * @return \Hostnet\Component\EntityRevision\RevisionInterface */ - public function createRevision(\DateTime $created_at); + public function createRevision(\DateTime $created_at): RevisionInterface; } diff --git a/src/Listener/RevisionListener.php b/src/Listener/RevisionListener.php index fb98efa..b5fec3d 100644 --- a/src/Listener/RevisionListener.php +++ b/src/Listener/RevisionListener.php @@ -6,8 +6,8 @@ namespace Hostnet\Component\EntityRevision\Listener; +use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\PostFlushEventArgs; -use Doctrine\ORM\Event\PreFlushEventArgs; use Hostnet\Component\EntityRevision\Factory\RevisionFactoryInterface; use Hostnet\Component\EntityRevision\Resolver\RevisionResolverInterface; use Hostnet\Component\EntityRevision\RevisionableInterface; @@ -26,32 +26,16 @@ class RevisionListener public function __construct( private RevisionResolverInterface $resolver, private RevisionFactoryInterface $factory, - private ?LoggerInterface $logger = new NullLogger(), - private ?CacheItemPoolInterface $is_revision_cache = new ArrayAdapter() + private LoggerInterface $logger = new NullLogger(), + private CacheItemPoolInterface $is_revision_cache = new ArrayAdapter() ) { } - /** - * Event is used to create a new revision - * - * 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, will be removed when removing - * doctrine/annotations. - * @param 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()); - } - /** * Event is used to remove the previous revision * * Used to group all entities to the same revision - * in the same flush if they use @Revision. This method + * in the same flush if they use #[Revision]. This method * can safely be overwritten if you prefer a Revision * per Request. */ @@ -60,9 +44,6 @@ public function postFlush(PostFlushEventArgs $event): void $this->revision = null; } - /** - * @param EntityChangedEvent $event - */ public function entityChanged(EntityChangedEvent $event): void { if (!$this->shouldBePersisted($event)) { @@ -73,22 +54,16 @@ public function entityChanged(EntityChangedEvent $event): void $this->revision = $this->factory->createRevision(new \DateTime()); } - if (null === $this->revision) { - throw new \RuntimeException('No Revision set for current flush.'); - } - $event->getEntityManager()->persist($this->revision); $entity = $event->getCurrentEntity(); $entity->setRevision($this->revision); - $this->logger->info(sprintf('Added revision for entity', ['entity' => get_class($entity)])); + $this->logger->info('Added revision for entity {entity}', ['entity' => $entity::class]); } /** * Checks if the current entity is eligable for a revision. - * - * @param EntityChangedEvent $event */ private function shouldBePersisted(EntityChangedEvent $event): bool { @@ -102,16 +77,16 @@ private function shouldBePersisted(EntityChangedEvent $event): bool $fields = $this->resolver->getRevisionableFields($em, $entity); // only create a revision if the mutated fields are tracked - if (count(array_intersect($fields, $event->getMutatedFields())) == 0) { + if (count(array_intersect($fields, $event->getMutatedFields())) === 0) { return false; } return true; } - private function isRevision($em, $entity): bool + private function isRevision(EntityManagerInterface $em, object $entity): bool { - $cache_key = base64_encode('REVISION-' . get_class($entity)); + $cache_key = base64_encode('REVISION-' . $entity::class); $cached_item = $this->is_revision_cache->getItem($cache_key); if ($cached_item->isHit()) { diff --git a/src/Resolver/RevisionResolver.php b/src/Resolver/RevisionResolver.php index f9aad8c..3666013 100644 --- a/src/Resolver/RevisionResolver.php +++ b/src/Resolver/RevisionResolver.php @@ -16,7 +16,7 @@ public function __construct(private EntityMetadataProvider $provider) { } - public function getRevisionAttribute(EntityManagerInterface $em, $entity): ?Revision + public function getRevisionAttribute(EntityManagerInterface $em, object $entity): ?Revision { return $this->provider->getAttributeFromEntity(Revision::class, $em, $entity); } @@ -24,9 +24,9 @@ public function getRevisionAttribute(EntityManagerInterface $em, $entity): ?Revi /** * @see \Hostnet\Component\EntityRevision\Resolver\RevisionResolverInterface::getRevisionableFields() */ - public function getRevisionableFields(EntityManagerInterface $em, $entity): array + public function getRevisionableFields(EntityManagerInterface $em, object $entity): array { - $metadata = $em->getClassMetadata(get_class($entity)); + $metadata = $em->getClassMetadata($entity::class); return array_merge($metadata->getFieldNames(), $metadata->getAssociationNames()); } } diff --git a/src/Resolver/RevisionResolverInterface.php b/src/Resolver/RevisionResolverInterface.php index 9cb5de0..b4aea68 100644 --- a/src/Resolver/RevisionResolverInterface.php +++ b/src/Resolver/RevisionResolverInterface.php @@ -16,10 +16,10 @@ interface RevisionResolverInterface * * @return string[] */ - public function getRevisionableFields(EntityManagerInterface $em, $entity): array; + public function getRevisionableFields(EntityManagerInterface $em, object $entity): array; /** * Return the revision attribute or null */ - public function getRevisionAttribute(EntityManagerInterface $em, $entity): ?Revision; + public function getRevisionAttribute(EntityManagerInterface $em, object $entity): ?Revision; } diff --git a/src/RevisionInterface.php b/src/RevisionInterface.php index b110a10..42489f6 100644 --- a/src/RevisionInterface.php +++ b/src/RevisionInterface.php @@ -6,22 +6,15 @@ namespace Hostnet\Component\EntityRevision; -/** - * @TODO: add returntypehints on next BC break, when removing doctrine/annotation support - */ interface RevisionInterface { /** * Return the user that created the revision. - * - * @return string */ - public function getUser(); + public function getUser(): string; /** * Return the date on which the revision was created. - * - * @return \DateTime */ - public function getCreatedAt(); + public function getCreatedAt(): \DateTimeInterface; } diff --git a/src/RevisionableInterface.php b/src/RevisionableInterface.php index ac0ec86..602655f 100644 --- a/src/RevisionableInterface.php +++ b/src/RevisionableInterface.php @@ -6,22 +6,15 @@ namespace Hostnet\Component\EntityRevision; -/** - * @TODO: add returntypehints on next BC break, when removing doctrine/annotation support - */ interface RevisionableInterface { /** * Set the current revision - * - * @param RevisionInterface $revision */ - public function setRevision(RevisionInterface $revision); + public function setRevision(RevisionInterface $revision): void; /** * Return the current revision - * - * @return RevisionInterface */ - public function getRevision(); + public function getRevision(): RevisionInterface; } diff --git a/test/Listener/RevisionListenerTest.php b/test/Listener/RevisionListenerTest.php index 67ef483..e4f8ac3 100644 --- a/test/Listener/RevisionListenerTest.php +++ b/test/Listener/RevisionListenerTest.php @@ -7,13 +7,12 @@ namespace Hostnet\Component\EntityRevision\Listener; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Event\PreFlushEventArgs; use Hostnet\Component\EntityRevision\Attributes\Revision; use Hostnet\Component\EntityRevision\Factory\RevisionFactoryInterface; use Hostnet\Component\EntityRevision\Resolver\RevisionResolverInterface; -use Hostnet\Component\EntityRevision\Revision as RevisionAnnotation; use Hostnet\Component\EntityRevision\RevisionableInterface; use Hostnet\Component\EntityTracker\Event\EntityChangedEvent; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; @@ -22,11 +21,11 @@ */ class RevisionListenerTest extends TestCase { - private $em; - private $factory; - private $entity; - private $resolver; - private $logger; + private EntityManagerInterface&MockObject $em; + private RevisionFactoryInterface&MockObject $factory; + private RevisionableInterface&MockObject $entity; + private RevisionResolverInterface&MockObject $resolver; + private LoggerInterface&MockObject $logger; public function setUp(): void { @@ -37,21 +36,6 @@ public function setUp(): void $this->logger = $this->createMock(LoggerInterface::class); } - public function testPreFlush(): void - { - $this->factory - ->expects($this->once()) - ->method('createRevision'); - - $doctrine_event = $this - ->getMockBuilder(PreFlushEventArgs::class) - ->disableOriginalConstructor() - ->getMock(); - - $listener = new RevisionListener($this->resolver, $this->factory, $this->logger); - $listener->preFlush($doctrine_event); - } - public function testOnEntityChangedNoInterface(): void { $this->resolver @@ -64,7 +48,7 @@ public function testOnEntityChangedNoInterface(): void $listener->entityChanged($event); } - public function testOnEntityChangedNoAnnotation(): void + public function testOnEntityChangedNoAttribute(): void { $event = new EntityChangedEvent($this->em, $this->entity, $this->entity, []); $listener = new RevisionListener($this->resolver, $this->factory, $this->logger); @@ -120,33 +104,6 @@ public function testOnEntityChangedNoTrackedMutations(): void $listener->entityChanged($event); } - public function testOnEntityChangedNoRevisionPresentOnFlush(): void - { - $revision = new Revision(); - - $this->resolver - ->expects($this->once()) - ->method('getRevisionAttribute') - ->willReturn($revision); - - $this->resolver - ->expects($this->once()) - ->method('getRevisionableFields') - ->willReturn(['something']); - - $this->factory - ->expects($this->once()) - ->method('createRevision') - ->willReturn(null); - - $event = new EntityChangedEvent($this->em, $this->entity, $this->entity, ['something']); - $listener = new RevisionListener($this->resolver, $this->factory, $this->logger); - - $this->expectException(\RuntimeException::class); - - $listener->entityChanged($event); - } - public function testOnEntityChangedInterfaceOnlyNoAttribute(): void { $this->resolver diff --git a/test/Resolver/RevisionResolverTest.php b/test/Resolver/RevisionResolverTest.php index 95bb4d5..fad5a95 100644 --- a/test/Resolver/RevisionResolverTest.php +++ b/test/Resolver/RevisionResolverTest.php @@ -10,6 +10,7 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Hostnet\Component\EntityRevision\Attributes\Revision; use Hostnet\Component\EntityTracker\Provider\EntityMetadataProvider; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** @@ -17,22 +18,14 @@ */ class RevisionResolverTest extends TestCase { - private $provider; - private $resolver; - private $em; + private EntityMetadataProvider&MockObject $provider; + private EntityManagerInterface&MockObject $em; + private RevisionResolver $resolver; public function setUp(): void { - $this->provider = $this - ->getMockBuilder(EntityMetadataProvider::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->em = $this - ->getMockBuilder(EntityManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - + $this->provider = $this->createMock(EntityMetadataProvider::class); + $this->em = $this->createMock(EntityManagerInterface::class); $this->resolver = new RevisionResolver($this->provider); } @@ -46,7 +39,7 @@ public function testGetRevisionableFields(): void $this->em ->expects($this->once()) ->method('getClassMetadata') - ->with(get_class($entity)) + ->with($entity::class) ->willReturn($metadata); $this->assertEquals(['id', 'test'], $this->resolver->getRevisionableFields($this->em, $entity)); From b0d0421849b986cf7d2f54418a410dd5d62da172 Mon Sep 17 00:00:00 2001 From: Jan Lam Date: Mon, 14 Sep 2026 10:27:38 +0200 Subject: [PATCH 4/7] Stop using PHPUnit's deprecated withConsecutive() mock matcher withConsecutive() is deprecated since phpunit/phpunit 9.6 and removed entirely in 10.x. Replaced with willReturnCallback() collecting the call arguments into an array, asserted afterwards. This also fixes the persist() expectation, which previously only verified the first two of the three actual calls (withConsecutive's parameter sets didn't cover the third). Co-Authored-By: Claude Sonnet 5 --- test/Listener/RevisionListenerTest.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/Listener/RevisionListenerTest.php b/test/Listener/RevisionListenerTest.php index e4f8ac3..5585f1a 100644 --- a/test/Listener/RevisionListenerTest.php +++ b/test/Listener/RevisionListenerTest.php @@ -153,15 +153,21 @@ public function testOnEntityChangedAttribute(): void ->method('createRevision') ->willReturnOnConsecutiveCalls($r1, $r2); + $persisted = []; $this->em - ->expects($this->any()) + ->expects($this->exactly(3)) ->method('persist') - ->withConsecutive([$this->identicalTo($r1)], [$this->identicalTo($r2)]); + ->willReturnCallback(function ($revision) use (&$persisted): void { + $persisted[] = $revision; + }); + $set_revisions = []; $this->entity ->expects($this->exactly(3)) ->method('setRevision') - ->withConsecutive([$this->identicalTo($r1)], [$this->identicalTo($r2)], [$this->identicalTo($r2)]); + ->willReturnCallback(function ($revision) use (&$set_revisions): void { + $set_revisions[] = $revision; + }); $event = new EntityChangedEvent($this->em, $this->entity, $this->entity, ['something']); $doctrine_event = $this @@ -174,5 +180,8 @@ public function testOnEntityChangedAttribute(): void $listener->postFlush($doctrine_event); $listener->entityChanged($event); $listener->entityChanged($event); + + self::assertSame([$r1, $r2, $r2], $persisted); + self::assertSame([$r1, $r2, $r2], $set_revisions); } } From 0e45982fc59d9e926e93e64e5fdbe313fb23fa5f Mon Sep 17 00:00:00 2001 From: Jan Lam Date: Mon, 14 Sep 2026 10:31:26 +0200 Subject: [PATCH 5/7] Bump actions/checkout to v4 v2 runs on the Node16 runtime GitHub is deprecating, same class of issue already fixed for actions/cache in a prior commit and just applied to entity-tracker-component's own workflow too. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index b7896e1..19d3d82 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -15,7 +15,7 @@ jobs: php-versions: ['8.3', '8.4'] name: PHP ${{ matrix.php-versions }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} From ad90d7c0925a6f799b9c32be7496839783fb2ef9 Mon Sep 17 00:00:00 2001 From: Jan Lam Date: Mon, 14 Sep 2026 10:35:57 +0200 Subject: [PATCH 6/7] Make RevisionableInterface::setRevision() fluent Mirrors the same change just made to entity-blamable-component's BlamableInterface setters, part of the same coordinated modernization pass. Update the README's entity example and UPGRADE-2.0.md accordingly, and exclude phpcs.xml.dist from git-archive exports (also mirrored from entity-blamable-component). The existing RevisionListenerTest mock for setRevision() needed its willReturnCallback() swapped for with($this->callback(...)): a void-returning callback used as the stub now fails the mock's static return type, whereas leaving the return value unconfigured lets PHPUnit's auto-generated mock satisfy it by returning $this. Co-Authored-By: Claude Sonnet 5 --- .gitattributes | 1 + README.md | 13 ++++++++----- UPGRADE-2.0.md | 2 +- src/RevisionableInterface.php | 2 +- test/Listener/RevisionListenerTest.php | 6 ++++-- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.gitattributes b/.gitattributes index d41c778..d9f4a3b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,4 @@ .gitignore export-ignore /test export-ignore /phpunit.xml.dist export-ignore +/phpcs.xml.dist export-ignore diff --git a/README.md b/README.md index 1878bff..09b4472 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Installing is pretty easy, this package is available on [packagist](https://pack ```javascript "require" : { - "hostnet/entity-revision-component" : "1.*" + "hostnet/entity-revision-component" : "2.*" } ``` @@ -152,6 +152,7 @@ All we have to do now is put the `#[Revision]` attribute and RevisionableInterfa use Doctrine\ORM\Mapping as ORM; use Hostnet\Component\EntityRevision\Attributes\Revision as RevisionAttribute; use Hostnet\Component\EntityRevision\RevisionableInterface; +use Hostnet\Component\EntityRevision\RevisionInterface; #[ORM\Entity] #[RevisionAttribute] @@ -162,14 +163,16 @@ class MyEntity implements RevisionableInterface */ #[ORM\ManyToOne(targetEntity: Revision::class)] #[ORM\JoinColumn(name: 'revision_id', referencedColumnName: 'id')] - private $revision; + private ?RevisionInterface $revision = null; - public function setRevision(RevisionInterface $revision) + public function setRevision(RevisionInterface $revision): static { $this->revision = $revision; + + return $this; } - - public function getRevision() + + public function getRevision(): RevisionInterface { return $this->revision; } diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md index 73f33ec..7d389d8 100644 --- a/UPGRADE-2.0.md +++ b/UPGRADE-2.0.md @@ -14,7 +14,7 @@ UPGRADE FROM 1.x to 2.0 - Added return type hints to `RevisionableInterface`, `RevisionInterface` and `RevisionFactoryInterface`. Any implementation of these interfaces must add matching return types: - - `RevisionableInterface::setRevision()`: `void` + - `RevisionableInterface::setRevision()`: `static` (now fluent, must `return $this;`) - `RevisionableInterface::getRevision()`: `RevisionInterface` - `RevisionInterface::getUser()`: `string` - `RevisionInterface::getCreatedAt()`: `\DateTimeInterface` diff --git a/src/RevisionableInterface.php b/src/RevisionableInterface.php index 602655f..7f63769 100644 --- a/src/RevisionableInterface.php +++ b/src/RevisionableInterface.php @@ -11,7 +11,7 @@ interface RevisionableInterface /** * Set the current revision */ - public function setRevision(RevisionInterface $revision): void; + public function setRevision(RevisionInterface $revision): static; /** * Return the current revision diff --git a/test/Listener/RevisionListenerTest.php b/test/Listener/RevisionListenerTest.php index 5585f1a..1826089 100644 --- a/test/Listener/RevisionListenerTest.php +++ b/test/Listener/RevisionListenerTest.php @@ -165,9 +165,11 @@ public function testOnEntityChangedAttribute(): void $this->entity ->expects($this->exactly(3)) ->method('setRevision') - ->willReturnCallback(function ($revision) use (&$set_revisions): void { + ->with($this->callback(function ($revision) use (&$set_revisions): bool { $set_revisions[] = $revision; - }); + + return true; + })); $event = new EntityChangedEvent($this->em, $this->entity, $this->entity, ['something']); $doctrine_event = $this From 5147b3bf0a8bbfd1d0eeec2cc6c4cea73a93b9fe Mon Sep 17 00:00:00 2001 From: Jan Lam Date: Mon, 14 Sep 2026 10:39:46 +0200 Subject: [PATCH 7/7] Add #[\Override] to RevisionResolver, bump CI actions further Mirrors entity-mutation-component's own modernization pass: - #[\Override] on RevisionResolver's interface-implementing methods, replacing a now-redundant @see docblock. - actions/checkout v4 -> v7 and actions/cache v4 -> v6; GitHub is dropping the Node 20 Actions runtime on 2026-09-23, and both were still pinned to old majors. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/main.yaml | 4 ++-- src/Resolver/RevisionResolver.php | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 19d3d82..757191c 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -15,7 +15,7 @@ jobs: php-versions: ['8.3', '8.4'] name: PHP ${{ matrix.php-versions }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} @@ -23,7 +23,7 @@ jobs: id: composer-cache run: | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - uses: actions/cache@v4 + - uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} diff --git a/src/Resolver/RevisionResolver.php b/src/Resolver/RevisionResolver.php index 3666013..2bc7c10 100644 --- a/src/Resolver/RevisionResolver.php +++ b/src/Resolver/RevisionResolver.php @@ -16,14 +16,13 @@ public function __construct(private EntityMetadataProvider $provider) { } + #[\Override] public function getRevisionAttribute(EntityManagerInterface $em, object $entity): ?Revision { return $this->provider->getAttributeFromEntity(Revision::class, $em, $entity); } - /** - * @see \Hostnet\Component\EntityRevision\Resolver\RevisionResolverInterface::getRevisionableFields() - */ + #[\Override] public function getRevisionableFields(EntityManagerInterface $em, object $entity): array { $metadata = $em->getClassMetadata($entity::class);