From b8b1446121c4821ec2f793bb253475d5cdd86314 Mon Sep 17 00:00:00 2001 From: Jan Lam Date: Mon, 14 Sep 2026 08:53:56 +0200 Subject: [PATCH 1/7] Drop annotation support, require attribute driver only Remove the deprecated docblock-annotation Mutation marker and its resolver/listener support in favor of the PHP attribute added in #30. Bump hostnet/entity-tracker-component to ^3.0.0 and update the functional test fixtures and setup to use Doctrine's AttributeDriver instead of AnnotationDriver. Co-Authored-By: Claude Sonnet 5 --- composer.json | 2 +- phpunit.xml.dist | 2 +- src/Listener/MutationListener.php | 4 -- src/Mutation.php | 58 ------------------- src/Resolver/MutationResolver.php | 22 +------ src/Resolver/MutationResolverInterface.php | 9 --- test/Functional/DiscriminatorMapTest.php | 25 ++++---- test/Functional/EmbeddableTest.php | 25 ++++---- test/Functional/Entity/Client.php | 36 ++++-------- test/Functional/Entity/ClientMutation.php | 32 ++++------ test/Functional/Entity/ContactInfo.php | 28 +++------ test/Functional/Entity/Contract.php | 42 +++++--------- test/Functional/Entity/ContractMutation.php | 50 ++++++---------- test/Functional/Entity/DomainContract.php | 12 ++-- .../Entity/DomainContractMutation.php | 8 +-- test/Functional/Entity/HostingContract.php | 12 ++-- .../Entity/HostingContractMutation.php | 8 +-- test/Listener/MutationListenerTest.php | 49 ++++++---------- test/MutationTest.php | 53 ----------------- test/Resolver/MutationResolverTest.php | 37 +----------- test/bootstrap.php | 11 ---- 21 files changed, 120 insertions(+), 405 deletions(-) delete mode 100644 src/Mutation.php delete mode 100644 test/MutationTest.php delete mode 100644 test/bootstrap.php diff --git a/composer.json b/composer.json index 60ce454..a58d3d7 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "require": { "php": "^8.3", "doctrine/orm": "^2.7.4", - "hostnet/entity-tracker-component": "^2.3.0" + "hostnet/entity-tracker-component": "^3.0.0" }, "require-dev": { "hostnet/database-test-lib": "^2.0.2", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1303baa..cbb4412 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,5 @@ - + src diff --git a/src/Listener/MutationListener.php b/src/Listener/MutationListener.php index 2cf187a..f5596bd 100644 --- a/src/Listener/MutationListener.php +++ b/src/Listener/MutationListener.php @@ -83,10 +83,6 @@ private function getMutationStrategy($em, $entity): false|string return $this->save($cached_item, $attribute->getStrategy()); } - if (null !== $annotation = $this->resolver->getMutationAnnotation($em, $entity)) { - return $this->save($cached_item, $annotation->getStrategy()); - } - return $this->save($cached_item, false); } diff --git a/src/Mutation.php b/src/Mutation.php deleted file mode 100644 index 781e0c0..0000000 --- a/src/Mutation.php +++ /dev/null @@ -1,58 +0,0 @@ -strategy, [self::STRATEGY_COPY_PREVIOUS, self::STRATEGY_COPY_CURRENT], true)) { - throw new \RuntimeException( - sprintf("Unknown strategy '%s' for class %s.", $this->strategy, get_class($this)) - ); - } - - return $this->strategy; - } -} diff --git a/src/Resolver/MutationResolver.php b/src/Resolver/MutationResolver.php index 311d1e4..7f7ec58 100644 --- a/src/Resolver/MutationResolver.php +++ b/src/Resolver/MutationResolver.php @@ -8,28 +8,14 @@ use Doctrine\ORM\EntityManagerInterface; use Hostnet\Component\EntityMutation\Attributes\Mutation; -use Hostnet\Component\EntityMutation\Mutation as MutationAnnotation; use Hostnet\Component\EntityTracker\Provider\EntityMetadataProvider; class MutationResolver implements MutationResolverInterface { - /** - * @var string - */ - private $annotation = MutationAnnotation::class; - public function __construct(private EntityMetadataProvider $provider) { } - /** - * {@inheritdoc} - */ - public function getMutationAnnotation(EntityManagerInterface $em, $entity): ?MutationAnnotation - { - return $this->provider->getAnnotationFromEntity($em, $entity, $this->annotation); - } - public function getMutationAttribute(EntityManagerInterface $em, $entity): ?Mutation { return $this->provider->getAttributeFromEntity(Mutation::class, $em, $entity); @@ -40,13 +26,7 @@ public function getMutationAttribute(EntityManagerInterface $em, $entity): ?Muta */ public function getMutationClassName(EntityManagerInterface $em, $entity): string { - $annotation = $this->getMutationAnnotation($em, $entity); - // If $annotation is null, we must be using the attribute, otherwise this code would not get hit. - if (null === $annotation) { - return get_class($entity) . 'Mutation'; - } - - return !empty($annotation->class) ? $annotation->class : get_class($entity) . 'Mutation'; + return get_class($entity) . 'Mutation'; } /** diff --git a/src/Resolver/MutationResolverInterface.php b/src/Resolver/MutationResolverInterface.php index 4a0aa93..86023f5 100644 --- a/src/Resolver/MutationResolverInterface.php +++ b/src/Resolver/MutationResolverInterface.php @@ -8,18 +8,9 @@ use Doctrine\ORM\EntityManagerInterface; use Hostnet\Component\EntityMutation\Attributes\Mutation; -use Hostnet\Component\EntityMutation\Mutation as MutationAnnotation; interface MutationResolverInterface { - /** - * Return the mutation annotation - * - * - * @deprecated Please use the attribute instead. - */ - public function getMutationAnnotation(EntityManagerInterface $em, $entity): ?MutationAnnotation; - public function getMutationAttribute(EntityManagerInterface $em, $entity): ?Mutation; /** diff --git a/test/Functional/DiscriminatorMapTest.php b/test/Functional/DiscriminatorMapTest.php index d40e9e8..1fc638d 100644 --- a/test/Functional/DiscriminatorMapTest.php +++ b/test/Functional/DiscriminatorMapTest.php @@ -6,10 +6,9 @@ namespace Hostnet\Component\EntityMutation\Functional; -use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\EventManager; use Doctrine\ORM\EntityManager; -use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; use Doctrine\ORM\Tools\SchemaTool; use Doctrine\ORM\Tools\Setup; use Functional\Entity\Contract; @@ -22,7 +21,7 @@ use Hostnet\Component\EntityMutation\Listener\MutationListener; use Hostnet\Component\EntityMutation\Resolver\MutationResolver; use Hostnet\Component\EntityTracker\Listener\EntityChangedListener; -use Hostnet\Component\EntityTracker\Provider\EntityAnnotationMetadataProvider; +use Hostnet\Component\EntityTracker\Provider\EntityMetadataProvider; use Hostnet\Component\EntityTracker\Provider\EntityMutationMetadataProvider; use PHPUnit\Framework\TestCase; @@ -45,26 +44,24 @@ class DiscriminatorMapTest extends TestCase public static function setUpBeforeClass(): void { - self::$connection = new MysqlPersistentConnection(); - $params = self::$connection->getConnectionParams(); - $configuration = Setup::createConfiguration(true); - $event_manager = new EventManager(); - $annotation_reader = new AnnotationReader(); + self::$connection = new MysqlPersistentConnection(); + $params = self::$connection->getConnectionParams(); + $configuration = Setup::createConfiguration(true); + $event_manager = new EventManager(); - $configuration->setMetadataDriverImpl(new AnnotationDriver($annotation_reader, [__DIR__ . '/Entity'])); + $configuration->setMetadataDriverImpl(new AttributeDriver([__DIR__ . '/Entity'])); - $annotation_metadata_provider = new EntityAnnotationMetadataProvider($annotation_reader); - $mutation_metadata_provider = new EntityMutationMetadataProvider($annotation_reader); + $entity_metadat_provider = new EntityMetadataProvider(); + $mutation_metadata_provider = new EntityMutationMetadataProvider(); $entity_changed_listener = new EntityChangedListener( - $annotation_metadata_provider, + $entity_metadat_provider, $mutation_metadata_provider ); - $mutation_resolver = new MutationResolver($annotation_metadata_provider); + $mutation_resolver = new MutationResolver($entity_metadat_provider); $mutation_listener = new MutationListener($mutation_resolver); - $event_manager->addEventListener('prePersist', $entity_changed_listener); $event_manager->addEventListener('preFlush', $entity_changed_listener); $event_manager->addEventListener('entityChanged', $mutation_listener); diff --git a/test/Functional/EmbeddableTest.php b/test/Functional/EmbeddableTest.php index 19e5dd0..82aee88 100644 --- a/test/Functional/EmbeddableTest.php +++ b/test/Functional/EmbeddableTest.php @@ -6,10 +6,9 @@ namespace Hostnet\Component\EntityMutation\Functional; -use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\EventManager; use Doctrine\ORM\EntityManager; -use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; use Doctrine\ORM\Tools\SchemaTool; use Doctrine\ORM\Tools\Setup; use Hostnet\Component\DatabaseTest\MysqlPersistentConnection; @@ -18,7 +17,7 @@ use Hostnet\Component\EntityMutation\Listener\MutationListener; use Hostnet\Component\EntityMutation\Resolver\MutationResolver; use Hostnet\Component\EntityTracker\Listener\EntityChangedListener; -use Hostnet\Component\EntityTracker\Provider\EntityAnnotationMetadataProvider; +use Hostnet\Component\EntityTracker\Provider\EntityMetadataProvider; use Hostnet\Component\EntityTracker\Provider\EntityMutationMetadataProvider; use PHPUnit\Framework\TestCase; @@ -41,26 +40,24 @@ class EmbeddableTest extends TestCase public static function setUpBeforeClass(): void { - self::$connection = new MysqlPersistentConnection(); - $params = self::$connection->getConnectionParams(); - $configuration = Setup::createConfiguration(true); - $event_manager = new EventManager(); - $annotation_reader = new AnnotationReader(); + self::$connection = new MysqlPersistentConnection(); + $params = self::$connection->getConnectionParams(); + $configuration = Setup::createConfiguration(true); + $event_manager = new EventManager(); - $configuration->setMetadataDriverImpl(new AnnotationDriver($annotation_reader, [__DIR__ . '/Entity'])); + $configuration->setMetadataDriverImpl(new AttributeDriver([__DIR__ . '/Entity'])); - $annotation_metadata_provider = new EntityAnnotationMetadataProvider($annotation_reader); - $mutation_metadata_provider = new EntityMutationMetadataProvider($annotation_reader); + $entity_metadata_provider = new EntityMetadataProvider(); + $mutation_metadata_provider = new EntityMutationMetadataProvider(); $entity_changed_listener = new EntityChangedListener( - $annotation_metadata_provider, + $entity_metadata_provider, $mutation_metadata_provider ); - $mutation_resolver = new MutationResolver($annotation_metadata_provider); + $mutation_resolver = new MutationResolver($entity_metadata_provider); $mutation_listener = new MutationListener($mutation_resolver); - $event_manager->addEventListener('prePersist', $entity_changed_listener); $event_manager->addEventListener('preFlush', $entity_changed_listener); $event_manager->addEventListener('entityChanged', $mutation_listener); diff --git a/test/Functional/Entity/Client.php b/test/Functional/Entity/Client.php index 9803ecc..3bedc7b 100644 --- a/test/Functional/Entity/Client.php +++ b/test/Functional/Entity/Client.php @@ -9,39 +9,27 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use Hostnet\Component\EntityMutation\Mutation; +use Hostnet\Component\EntityMutation\Attributes\Mutation; use Hostnet\Component\EntityMutation\MutationAwareInterface; -/** - * @ORM\Entity() - * @Mutation(strategy="current") - */ +#[ORM\Entity] +#[Mutation(strategy: Mutation::STRATEGY_COPY_CURRENT)] class Client implements MutationAwareInterface { - /** - * @ORM\Id() - * @ORM\GeneratedValue() - * @ORM\Column(type="integer") - * - * @var int - */ - private $id; + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private int $id; - /** - * @ORM\Embedded(class="ContactInfo") - * - * @var ContactInfo - */ - private $contact_info; + #[ORM\Embedded(class: ContactInfo::class)] + private ContactInfo $contact_info; /** * The history of this object. - * - * @ORM\OneToMany(targetEntity="ClientMutation", mappedBy="client") - * @ORM\OrderBy(value={"id"="DESC"}) - * @var Collection */ - private $mutations; + #[ORM\OneToMany(targetEntity: ClientMutation::class, mappedBy: 'client')] + #[ORM\OrderBy(['id' => 'DESC'])] + private Collection $mutations; /** * @param ContactInfo $contact_info diff --git a/test/Functional/Entity/ClientMutation.php b/test/Functional/Entity/ClientMutation.php index 7a1873c..f89de1e 100644 --- a/test/Functional/Entity/ClientMutation.php +++ b/test/Functional/Entity/ClientMutation.php @@ -8,32 +8,20 @@ use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity() - */ +#[ORM\Entity] class ClientMutation { - /** - * @ORM\Id() - * @ORM\GeneratedValue() - * @ORM\Column(type="integer") - * - * @var int - */ - private $id; + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private int $id; - /** - * @ORM\Embedded(class="ContactInfo") - * - * @var ContactInfo - */ - private $contact_info; + #[ORM\Embedded(class: ContactInfo::class)] + private ContactInfo $contact_info; - /** - * @ORM\ManyToOne(targetEntity="Client", inversedBy="mutations") - * @ORM\JoinColumn() - */ - private $client; + #[ORM\ManyToOne(targetEntity: Client::class, inversedBy: 'mutations')] + #[ORM\JoinColumn] + private Client $client; /** * @param Client $client diff --git a/test/Functional/Entity/ContactInfo.php b/test/Functional/Entity/ContactInfo.php index 478811e..1bead4a 100644 --- a/test/Functional/Entity/ContactInfo.php +++ b/test/Functional/Entity/ContactInfo.php @@ -8,31 +8,17 @@ use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Embeddable() - */ +#[ORM\Embeddable] class ContactInfo { - /** - * @ORM\Column(type="string") - * - * @var string - */ - private $address_line; + #[ORM\Column(type: 'string')] + private string $address_line; - /** - * @ORM\Column(type="string") - * - * @var string - */ - private $name; + #[ORM\Column(type: 'string')] + private string $name; - /** - * @ORM\Column(type="datetime") - * - * @var \DateTime - */ - private $created_at; + #[ORM\Column(type: 'datetime')] + private \DateTime $created_at; /** * @param string $address_line diff --git a/test/Functional/Entity/Contract.php b/test/Functional/Entity/Contract.php index c539198..3e8d1ec 100644 --- a/test/Functional/Entity/Contract.php +++ b/test/Functional/Entity/Contract.php @@ -9,47 +9,33 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use Hostnet\Component\EntityMutation\Mutation; +use Hostnet\Component\EntityMutation\Attributes\Mutation; use Hostnet\Component\EntityMutation\MutationAwareInterface; -/** - * @ORM\Entity() - * @ORM\InheritanceType("JOINED") - * @ORM\DiscriminatorColumn("type") - * @ORM\DiscriminatorMap({ - * 1 = "HostingContract", - * 2 = "DomainContract", - * 3 = "Contract" - * }) - * @Mutation(strategy="current") - */ +#[ORM\Entity] +#[ORM\InheritanceType('JOINED')] +#[ORM\DiscriminatorColumn(name: 'type')] +#[ORM\DiscriminatorMap([1 => HostingContract::class, 2 => DomainContract::class, 3 => Contract::class])] +#[Mutation(strategy: Mutation::STRATEGY_COPY_CURRENT)] class Contract implements MutationAwareInterface { - /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] private $id; - /** - * @ORM\Column(type="string") - */ + #[ORM\Column(type: 'string')] private $identifier; - /** - * @ORM\Column(type="integer") - */ + #[ORM\Column(type: 'integer')] private $status; /** * The history of this object. - * - * @ORM\OneToMany(targetEntity="ContractMutation", mappedBy="contract") - * @ORM\OrderBy(value={"id"="DESC"}) - * @var Collection */ - private $mutations; + #[ORM\OneToMany(targetEntity: ContractMutation::class, mappedBy: 'contract')] + #[ORM\OrderBy(['id' => 'DESC'])] + private Collection $mutations; /** * @param string $identifier diff --git a/test/Functional/Entity/ContractMutation.php b/test/Functional/Entity/ContractMutation.php index 71f21ed..9d490f4 100644 --- a/test/Functional/Entity/ContractMutation.php +++ b/test/Functional/Entity/ContractMutation.php @@ -8,44 +8,30 @@ use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity() - * @ORM\InheritanceType("JOINED") - * @ORM\DiscriminatorColumn("type") - * @ORM\DiscriminatorMap({ - * 1 = "HostingContractMutation", - * 2 = "DomainContractMutation", - * 3 = "ContractMutation" - * }) - */ +#[ORM\Entity] +#[ORM\InheritanceType('JOINED')] +#[ORM\DiscriminatorColumn(name: 'type')] +#[ORM\DiscriminatorMap([ + 1 => HostingContractMutation::class, + 2 => DomainContractMutation::class, + 3 => ContractMutation::class, +])] class ContractMutation { - /** - * @ORM\Id() - * @ORM\GeneratedValue() - * @ORM\Column(type="integer") - * - * @var int - */ - private $id; + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private int $id; - /** - * @ORM\ManyToOne(targetEntity="Contract", inversedBy="mutations") - * @ORM\JoinColumn() - */ - private $contract; + #[ORM\ManyToOne(targetEntity: Contract::class, inversedBy: 'mutations')] + #[ORM\JoinColumn] + private Contract $contract; - /** - * @ORM\Column(type="string") - */ + #[ORM\Column(type: 'string')] private $identifier; - /** - * @ORM\Column(type="integer") - * - * @var int - */ - private $status; + #[ORM\Column(type: 'integer')] + private int $status; /** * @param Contract $contract diff --git a/test/Functional/Entity/DomainContract.php b/test/Functional/Entity/DomainContract.php index 55a4cc6..8a11b17 100644 --- a/test/Functional/Entity/DomainContract.php +++ b/test/Functional/Entity/DomainContract.php @@ -7,17 +7,13 @@ namespace Functional\Entity; use Doctrine\ORM\Mapping as ORM; -use Hostnet\Component\EntityMutation\Mutation; +use Hostnet\Component\EntityMutation\Attributes\Mutation; -/** - * @ORM\Entity() - * @Mutation(strategy="current") - */ +#[ORM\Entity] +#[Mutation(strategy: Mutation::STRATEGY_COPY_CURRENT)] class DomainContract extends Contract { - /** - * @ORM\Column(type="string") - */ + #[ORM\Column(type: 'string')] private $domain; /** diff --git a/test/Functional/Entity/DomainContractMutation.php b/test/Functional/Entity/DomainContractMutation.php index 4a05624..80e5f9f 100644 --- a/test/Functional/Entity/DomainContractMutation.php +++ b/test/Functional/Entity/DomainContractMutation.php @@ -8,14 +8,10 @@ use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity() - */ +#[ORM\Entity] class DomainContractMutation extends ContractMutation { - /** - * @ORM\Column(type="string") - */ + #[ORM\Column(type: 'string')] private $domain; public function getDomain(): string diff --git a/test/Functional/Entity/HostingContract.php b/test/Functional/Entity/HostingContract.php index 310ab9f..348b8f6 100644 --- a/test/Functional/Entity/HostingContract.php +++ b/test/Functional/Entity/HostingContract.php @@ -7,17 +7,13 @@ namespace Functional\Entity; use Doctrine\ORM\Mapping as ORM; -use Hostnet\Component\EntityMutation\Mutation; +use Hostnet\Component\EntityMutation\Attributes\Mutation; -/** - * @ORM\Entity() - * @Mutation(strategy="current") - */ +#[ORM\Entity] +#[Mutation(strategy: Mutation::STRATEGY_COPY_CURRENT)] class HostingContract extends Contract { - /** - * @ORM\Column(type="string") - */ + #[ORM\Column(type: 'string')] private $service; /** diff --git a/test/Functional/Entity/HostingContractMutation.php b/test/Functional/Entity/HostingContractMutation.php index f0db418..1dd4bca 100644 --- a/test/Functional/Entity/HostingContractMutation.php +++ b/test/Functional/Entity/HostingContractMutation.php @@ -8,14 +8,10 @@ use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity() - */ +#[ORM\Entity] class HostingContractMutation extends ContractMutation { - /** - * @ORM\Column(type="string") - */ + #[ORM\Column(type: 'string')] private $service; public function getService(): string diff --git a/test/Listener/MutationListenerTest.php b/test/Listener/MutationListenerTest.php index 69cd9f6..7fdf8d0 100644 --- a/test/Listener/MutationListenerTest.php +++ b/test/Listener/MutationListenerTest.php @@ -13,7 +13,6 @@ use Hostnet\Component\EntityMutation\Mocked\MockMutationEntityAttribute; use Hostnet\Component\EntityMutation\Mocked\MockMutationEntityAttributeMutation; use Hostnet\Component\EntityMutation\Mocked\MockMutationEntityMutation; -use Hostnet\Component\EntityMutation\Mutation as MutationAnnotation; use Hostnet\Component\EntityMutation\Resolver\MutationResolverInterface; use Hostnet\Component\EntityTracker\Event\EntityChangedEvent; use PHPUnit\Framework\TestCase; @@ -53,13 +52,13 @@ public function testOnEntityChanged(): void ->with($this->em, $current_entity) ->willReturn(['id']); - $annotation = new MutationAnnotation(); + $attribute = new Mutation(); $this->resolver ->expects($this->once()) - ->method('getMutationAnnotation') + ->method('getMutationAttribute') ->with($this->em, $current_entity) - ->willReturn($annotation); + ->willReturn($attribute); $this->resolver ->expects($this->once()) @@ -106,14 +105,14 @@ public function testOnEntityChangedCopyCurrent(): void ->with($this->em, $current_entity) ->willReturn(['id']); - $annotation = new MutationAnnotation(); - $annotation->strategy = MutationAnnotation::STRATEGY_COPY_CURRENT; + $attribute = new Mutation(); + $attribute->strategy = Mutation::STRATEGY_COPY_CURRENT; $this->resolver ->expects($this->once()) - ->method('getMutationAnnotation') + ->method('getMutationAttribute') ->with($this->em, $current_entity) - ->willReturn($annotation); + ->willReturn($attribute); $this->resolver ->expects($this->once()) @@ -160,17 +159,17 @@ public function testOnEntityChangedUnknownStrategy(): void ->with($this->em, $current_entity) ->willReturn(['id']); - $annotation = $this->createMock('Hostnet\Component\EntityMutation\Mutation'); - $annotation + $attribute = $this->createMock('Hostnet\Component\EntityMutation\Attributes\Mutation'); + $attribute ->expects($this->once()) ->method('getStrategy') ->willReturn('phpunit'); $this->resolver ->expects($this->once()) - ->method('getMutationAnnotation') + ->method('getMutationAttribute') ->with($this->em, $current_entity) - ->willReturn($annotation); + ->willReturn($attribute); $event = new EntityChangedEvent($this->em, $current_entity, $original_entity, $mutated_fields); $this->expectException(\RuntimeException::class); @@ -192,17 +191,13 @@ public function testOnEntityChangedInsertedWithPreviousStrategy(): void ->with($this->em, $current_entity) ->willReturn(['id']); - $annotation = $this->createMock('Hostnet\Component\EntityMutation\Mutation'); - $annotation - ->expects($this->once()) - ->method('getStrategy') - ->willReturn('previous'); + $attribute = new Mutation(); $this->resolver ->expects($this->once()) - ->method('getMutationAnnotation') + ->method('getMutationAttribute') ->with($this->em, $current_entity) - ->willReturn($annotation); + ->willReturn($attribute); $this->em ->expects($this->never()) @@ -227,14 +222,14 @@ public function testOnEntityChangedEmptyChanges(): void ->with($this->em, $current_entity) ->willReturn([]); - $annotation = new MutationAnnotation(); - $annotation->strategy = MutationAnnotation::STRATEGY_COPY_CURRENT; + $attribute = new Mutation(); + $attribute->strategy = Mutation::STRATEGY_COPY_CURRENT; $this->resolver ->expects($this->once()) - ->method('getMutationAnnotation') + ->method('getMutationAttribute') ->with($this->em, $current_entity) - ->willReturn($annotation); + ->willReturn($attribute); $this->em ->expects($this->never()) @@ -247,7 +242,7 @@ public function testOnEntityChangedEmptyChanges(): void $this->assertCount(0, $current_entity->getMutations()); } - public function testOnEntityChangedNoAnnotation(): void + public function testOnEntityChangedNoAttribute(): void { $current_entity = new MockMutationEntity(); $original_entity = new MockMutationEntity(); @@ -256,12 +251,6 @@ public function testOnEntityChangedNoAnnotation(): void $original_entity->id = 1; $mutated_fields = ['id']; - $this->resolver - ->expects($this->once()) - ->method('getMutationAnnotation') - ->with($this->em, $current_entity) - ->willReturn(null); - $this->em ->expects($this->never()) ->method('persist') diff --git a/test/MutationTest.php b/test/MutationTest.php deleted file mode 100644 index 0eb79d5..0000000 --- a/test/MutationTest.php +++ /dev/null @@ -1,53 +0,0 @@ -assertEquals(Mutation::STRATEGY_COPY_PREVIOUS, $mutation->getStrategy()); - $mutation->strategy = Mutation::STRATEGY_COPY_CURRENT; - $this->assertEquals(Mutation::STRATEGY_COPY_CURRENT, $mutation->getStrategy()); - } - - /** - * @dataProvider getStrategyExceptionProvider - * @param mixed $strategy - */ - public function testGetStrategyException($strategy): void - { - $mutation = new Mutation(); - $mutation->strategy = $strategy; - $this->expectException(\RuntimeException::class); - $mutation->getStrategy(); - } - - public function getStrategyExceptionProvider(): iterable - { - return [ - [null], - [''], - ['test'], - ['last'], - ['before'], - [true], - [false], - [0], - [-1], - [1], - [1000], - [-1000], - ]; - } -} diff --git a/test/Resolver/MutationResolverTest.php b/test/Resolver/MutationResolverTest.php index e5deb14..eb4af13 100644 --- a/test/Resolver/MutationResolverTest.php +++ b/test/Resolver/MutationResolverTest.php @@ -9,8 +9,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\Mapping\ClassMetadata; use Hostnet\Component\EntityMutation\Attributes\Mutation; -use Hostnet\Component\EntityMutation\Mutation as MutationAnnotation; -use Hostnet\Component\EntityTracker\Provider\EntityAnnotationMetadataProvider; +use Hostnet\Component\EntityTracker\Provider\EntityMetadataProvider; use PHPUnit\Framework\TestCase; /** @@ -25,7 +24,7 @@ class MutationResolverTest extends TestCase public function setUp(): void { $this->provider = $this - ->getMockBuilder(EntityAnnotationMetadataProvider::class) + ->getMockBuilder(EntityMetadataProvider::class) ->disableOriginalConstructor() ->getMock(); @@ -37,35 +36,10 @@ public function setUp(): void $this->resolver = new MutationResolver($this->provider); } - public function testGetMutationAnnotation(): void - { - $entity = new \stdClass(); - - $this->provider - ->expects($this->once()) - ->method('getAnnotationFromEntity') - ->with($this->em, $entity, MutationAnnotation::class); - - $this->resolver->getMutationAnnotation($this->em, $entity); - } - public function testGetMutationClassName(): void { - $entity = new \stdClass(); - $annotation = new MutationAnnotation(); - $annotation->class = 'Phpunit'; - - $this->provider - ->expects($this->exactly(3)) - ->method('getAnnotationFromEntity') - ->with($this->em, $entity, 'Hostnet\Component\EntityMutation\Mutation') - ->willReturnOnConsecutiveCalls(null, new MutationAnnotation(), $annotation); - - // Without annotation, assuming the attribute is in use - $this->assertEquals('stdClassMutation', $this->resolver->getMutationClassName($this->em, $entity)); - + $entity = new \stdClass(); $this->assertEquals('stdClassMutation', $this->resolver->getMutationClassName($this->em, $entity)); - $this->assertEquals('Phpunit', $this->resolver->getMutationClassName($this->em, $entity)); } public function testGetMutatedFields(): void @@ -74,11 +48,6 @@ public function testGetMutatedFields(): void $metadata = $this->createMock(ClassMetadata::class); $metadata_meta = $this->createMock(ClassMetadata::class); - $this->provider - ->expects($this->once()) - ->method('getAnnotationFromEntity') - ->willReturnOnConsecutiveCalls(new MutationAnnotation()); - $metadata->expects($this->once())->method('getFieldNames')->willReturn(['id']); $metadata_meta->expects($this->once())->method('getFieldNames')->willReturn(['id']); $metadata->expects($this->once())->method('getAssociationNames')->willReturn(['test']); diff --git a/test/bootstrap.php b/test/bootstrap.php deleted file mode 100644 index 393fd60..0000000 --- a/test/bootstrap.php +++ /dev/null @@ -1,11 +0,0 @@ - Date: Mon, 14 Sep 2026 09:04:40 +0200 Subject: [PATCH 2/7] Modernize code to PHP 8.3 conventions - Add #[\Override] on interface/parent-overriding methods. - Type-hint MutationAwareInterface and MutationResolverInterface (the former's TODO to do so "on next BC break" is now resolved). - Use constructor property promotion for entity/embeddable fixtures, including promoting Doctrine mapping attributes onto the params. - Replace switch/case strategy dispatch in MutationListener with match. - Use spaceship operator + arrow functions for the mutation usort() comparators, and static return types for fluent setters. - Remove PHPDoc that only restated a typed signature, and drop the dead references to the deleted docblock-only Mutation annotation class and a never-existing MockEntity left in the test mocks. Co-Authored-By: Claude Sonnet 5 --- src/Attributes/Mutation.php | 9 ++-- src/Listener/MutationListener.php | 24 ++++------- src/MutationAwareInterface.php | 17 ++------ src/Resolver/MutationResolver.php | 15 +++---- src/Resolver/MutationResolverInterface.php | 6 +-- test/Attributes/MutationTest.php | 3 +- test/Functional/Entity/Client.php | 41 ++++++------------ test/Functional/Entity/ClientMutation.php | 18 +++----- test/Functional/Entity/ContactInfo.php | 42 +++++-------------- test/Functional/Entity/Contract.php | 39 ++++++----------- test/Functional/Entity/ContractMutation.php | 22 ++++------ test/Functional/Entity/DomainContract.php | 18 +++----- .../Entity/DomainContractMutation.php | 6 +-- test/Functional/Entity/HostingContract.php | 18 +++----- .../Entity/HostingContractMutation.php | 6 +-- test/Listener/MutationListenerTest.php | 7 ++-- test/Mocked/MockMutationEntity.php | 42 ++++--------------- test/Mocked/MockMutationEntityAttribute.php | 38 ++++------------- test/Resolver/MutationResolverTest.php | 7 ++-- 19 files changed, 115 insertions(+), 263 deletions(-) diff --git a/src/Attributes/Mutation.php b/src/Attributes/Mutation.php index 8d516f8..05bcf78 100644 --- a/src/Attributes/Mutation.php +++ b/src/Attributes/Mutation.php @@ -15,10 +15,11 @@ public function __construct( public string $strategy = self::STRATEGY_COPY_PREVIOUS, ) { } - /** - * The Previous values will be stored in the mutation table. This is the - * default strategy. - */ + + /** + * The Previous values will be stored in the mutation table. This is the + * default strategy. + */ public const string STRATEGY_COPY_PREVIOUS = 'previous'; /** diff --git a/src/Listener/MutationListener.php b/src/Listener/MutationListener.php index f5596bd..ea18de3 100644 --- a/src/Listener/MutationListener.php +++ b/src/Listener/MutationListener.php @@ -6,6 +6,7 @@ namespace Hostnet\Component\EntityMutation\Listener; +use Doctrine\ORM\EntityManagerInterface; use Hostnet\Component\EntityMutation\Attributes\Mutation; use Hostnet\Component\EntityMutation\MutationAwareInterface; use Hostnet\Component\EntityMutation\Resolver\MutationResolverInterface; @@ -16,18 +17,12 @@ class MutationListener { - /** - * @param MutationResolverInterface $resolver - */ public function __construct( private MutationResolverInterface $resolver, private CacheItemPoolInterface $is_mutation_cache = new ArrayAdapter() ) { } - /** - * @param EntityChangedEvent $event - */ public function entityChanged(EntityChangedEvent $event): void { $em = $event->getEntityManager(); @@ -47,16 +42,11 @@ public function entityChanged(EntityChangedEvent $event): void return; } - switch ($strategy) { - case Mutation::STRATEGY_COPY_CURRENT: - $mutation_source = $entity; - break; - case Mutation::STRATEGY_COPY_PREVIOUS: - $mutation_source = $event->getOriginalEntity(); - break; - default: - throw new \RuntimeException(sprintf("Unknown strategy '%s'.", $strategy)); - } + $mutation_source = match ($strategy) { + Mutation::STRATEGY_COPY_CURRENT => $entity, + Mutation::STRATEGY_COPY_PREVIOUS => $event->getOriginalEntity(), + default => throw new \RuntimeException(sprintf("Unknown strategy '%s'.", $strategy)), + }; $mutation = $em ->getClassMetadata($this->resolver->getMutationClassName($em, $entity)) @@ -70,7 +60,7 @@ public function entityChanged(EntityChangedEvent $event): void } } - private function getMutationStrategy($em, $entity): false|string + private function getMutationStrategy(EntityManagerInterface $em, object $entity): false|string { $cache_key = base64_encode('MUTATION-' . get_class($entity)); $cached_item = $this->is_mutation_cache->getItem($cache_key); diff --git a/src/MutationAwareInterface.php b/src/MutationAwareInterface.php index 19e00c6..e55e1aa 100644 --- a/src/MutationAwareInterface.php +++ b/src/MutationAwareInterface.php @@ -6,23 +6,14 @@ namespace Hostnet\Component\EntityMutation; -/** - * TODO: add typehints on next BC break, removing doctrine/annotations - */ interface MutationAwareInterface { - /** - * @param mixed $mutation - */ - public function addMutation($mutation); + public function addMutation(object $mutation): void; /** - * @return array of mutations + * @return object[] */ - public function getMutations(); + public function getMutations(): array; - /** - * @return mixed previous mutation object - */ - public function getPreviousMutation(); + public function getPreviousMutation(): ?object; } diff --git a/src/Resolver/MutationResolver.php b/src/Resolver/MutationResolver.php index 7f7ec58..f622675 100644 --- a/src/Resolver/MutationResolver.php +++ b/src/Resolver/MutationResolver.php @@ -16,23 +16,20 @@ public function __construct(private EntityMetadataProvider $provider) { } - public function getMutationAttribute(EntityManagerInterface $em, $entity): ?Mutation + #[\Override] + public function getMutationAttribute(EntityManagerInterface $em, object $entity): ?Mutation { return $this->provider->getAttributeFromEntity(Mutation::class, $em, $entity); } - /** - * {@inheritdoc} - */ - public function getMutationClassName(EntityManagerInterface $em, $entity): string + #[\Override] + public function getMutationClassName(EntityManagerInterface $em, object $entity): string { return get_class($entity) . 'Mutation'; } - /** - * {@inheritdoc} - */ - public function getMutatableFields(EntityManagerInterface $em, $entity): array + #[\Override] + public function getMutatableFields(EntityManagerInterface $em, object $entity): array { $mutation_class = $this->getMutationClassName($em, $entity); $metadata = $em->getClassMetadata(get_class($entity)); diff --git a/src/Resolver/MutationResolverInterface.php b/src/Resolver/MutationResolverInterface.php index 86023f5..38d96a6 100644 --- a/src/Resolver/MutationResolverInterface.php +++ b/src/Resolver/MutationResolverInterface.php @@ -11,17 +11,17 @@ interface MutationResolverInterface { - public function getMutationAttribute(EntityManagerInterface $em, $entity): ?Mutation; + public function getMutationAttribute(EntityManagerInterface $em, object $entity): ?Mutation; /** * Return the mutation class name */ - public function getMutationClassName(EntityManagerInterface $em, $entity): string; + public function getMutationClassName(EntityManagerInterface $em, object $entity): string; /** * Return list of mutatable fields * * @return string[] */ - public function getMutatableFields(EntityManagerInterface $em, $entity): array; + public function getMutatableFields(EntityManagerInterface $em, object $entity): array; } diff --git a/test/Attributes/MutationTest.php b/test/Attributes/MutationTest.php index 6621edb..142a5dc 100644 --- a/test/Attributes/MutationTest.php +++ b/test/Attributes/MutationTest.php @@ -23,9 +23,8 @@ public function testGetStrategy(): void /** * @dataProvider getStrategyExceptionProvider - * @param mixed $strategy */ - public function testGetStrategyException($strategy): void + public function testGetStrategyException(string $strategy): void { $mutation = new Mutation(); $mutation->strategy = $strategy; diff --git a/test/Functional/Entity/Client.php b/test/Functional/Entity/Client.php index 3bedc7b..4d867f8 100644 --- a/test/Functional/Entity/Client.php +++ b/test/Functional/Entity/Client.php @@ -21,9 +21,6 @@ class Client implements MutationAwareInterface #[ORM\Column(type: 'integer')] private int $id; - #[ORM\Embedded(class: ContactInfo::class)] - private ContactInfo $contact_info; - /** * The history of this object. */ @@ -31,13 +28,11 @@ class Client implements MutationAwareInterface #[ORM\OrderBy(['id' => 'DESC'])] private Collection $mutations; - /** - * @param ContactInfo $contact_info - */ - public function __construct(ContactInfo $contact_info) - { - $this->contact_info = $contact_info; - $this->mutations = new ArrayCollection(); + public function __construct( + #[ORM\Embedded(class: ContactInfo::class)] + private ContactInfo $contact_info, + ) { + $this->mutations = new ArrayCollection(); } public function getContactInfo(): ContactInfo @@ -45,29 +40,20 @@ public function getContactInfo(): ContactInfo return $this->contact_info; } - /** - * @return mixed - */ - public function getId() + public function getId(): int { return $this->id; } - /** - * @param ContactInfo $contact_info - * @return $this - */ - public function setContactInfo(ContactInfo $contact_info) + public function setContactInfo(ContactInfo $contact_info): static { $this->contact_info = $contact_info; return $this; } - /** - * @param ClientMutation $mutation - */ - public function addMutation($mutation): void + #[\Override] + public function addMutation(object $mutation): void { // $this->mutations is sorted by id descending, so we should add new // items at the start of the Collection. Doctrine collections don't @@ -79,18 +65,15 @@ public function addMutation($mutation): void /** * @return ClientMutation[] */ + #[\Override] public function getMutations(): array { $mutations = $this->mutations->toArray(); - usort($mutations, function (ClientMutation $ma, ClientMutation $mb) { - if ($ma->getId() === $mb->getId()) { - return 0; - } - return ($ma->getId() > $mb->getId()) ? -1 : 1; - }); + usort($mutations, fn(ClientMutation $ma, ClientMutation $mb) => $mb->getId() <=> $ma->getId()); return $mutations; } + #[\Override] public function getPreviousMutation(): ClientMutation { throw new \BadMethodCallException(__METHOD__ . ' is not implemented.'); diff --git a/test/Functional/Entity/ClientMutation.php b/test/Functional/Entity/ClientMutation.php index f89de1e..1733929 100644 --- a/test/Functional/Entity/ClientMutation.php +++ b/test/Functional/Entity/ClientMutation.php @@ -19,18 +19,12 @@ class ClientMutation #[ORM\Embedded(class: ContactInfo::class)] private ContactInfo $contact_info; - #[ORM\ManyToOne(targetEntity: Client::class, inversedBy: 'mutations')] - #[ORM\JoinColumn] - private Client $client; - - /** - * @param Client $client - * @param Client $orginal - */ - public function __construct(Client $client, Client $orginal) - { - $this->client = $client; - + public function __construct( + #[ORM\ManyToOne(targetEntity: Client::class, inversedBy: 'mutations')] + #[ORM\JoinColumn] + private Client $client, + Client $orginal, + ) { // Clone the embeddable so we keep the original state in case it mutates. // Note: This *must* be a deep clone. $this->contact_info = clone $orginal->getContactInfo(); diff --git a/test/Functional/Entity/ContactInfo.php b/test/Functional/Entity/ContactInfo.php index 1bead4a..1b6eafc 100644 --- a/test/Functional/Entity/ContactInfo.php +++ b/test/Functional/Entity/ContactInfo.php @@ -11,25 +11,14 @@ #[ORM\Embeddable] class ContactInfo { - #[ORM\Column(type: 'string')] - private string $address_line; - - #[ORM\Column(type: 'string')] - private string $name; - - #[ORM\Column(type: 'datetime')] - private \DateTime $created_at; - - /** - * @param string $address_line - * @param string $name - * @param \DateTime $created_at - */ - public function __construct($address_line, $name, \DateTime $created_at) - { - $this->address_line = $address_line; - $this->name = $name; - $this->created_at = $created_at; + public function __construct( + #[ORM\Column(type: 'string')] + private string $address_line, + #[ORM\Column(type: 'string')] + private string $name, + #[ORM\Column(type: 'datetime')] + private \DateTime $created_at, + ) { } public function getAddressLine(): string @@ -47,30 +36,21 @@ public function getCreatedAt(): \DateTime return $this->created_at; } - /** - * @param \DateTime $created_at - */ - public function setCreatedAt($created_at): ContactInfo + public function setCreatedAt(\DateTime $created_at): static { $this->created_at = $created_at; return $this; } - /** - * @param string $name - */ - public function setName($name): ContactInfo + public function setName(string $name): static { $this->name = $name; return $this; } - /** - * @param string $address_line - */ - public function setAddressLine($address_line): ContactInfo + public function setAddressLine(string $address_line): static { $this->address_line = $address_line; diff --git a/test/Functional/Entity/Contract.php b/test/Functional/Entity/Contract.php index 3e8d1ec..578f2e7 100644 --- a/test/Functional/Entity/Contract.php +++ b/test/Functional/Entity/Contract.php @@ -22,13 +22,7 @@ class Contract implements MutationAwareInterface #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] - private $id; - - #[ORM\Column(type: 'string')] - private $identifier; - - #[ORM\Column(type: 'integer')] - private $status; + private int $id; /** * The history of this object. @@ -37,15 +31,13 @@ class Contract implements MutationAwareInterface #[ORM\OrderBy(['id' => 'DESC'])] private Collection $mutations; - /** - * @param string $identifier - * @param int $status - */ - public function __construct($identifier, $status) - { - $this->identifier = $identifier; - $this->status = $status; - $this->mutations = new ArrayCollection(); + public function __construct( + #[ORM\Column(type: 'string')] + private string $identifier, + #[ORM\Column(type: 'integer')] + private int $status, + ) { + $this->mutations = new ArrayCollection(); } public function getId(): int @@ -63,10 +55,8 @@ public function getStatus(): int return $this->status; } - /** - * @param DomainContractMutation $mutation - */ - public function addMutation($mutation): void + #[\Override] + public function addMutation(object $mutation): void { // $this->mutations is sorted by id descending, so we should add new // items at the start of the Collection. Doctrine collections don't @@ -78,18 +68,15 @@ public function addMutation($mutation): void /** * @return ContractMutation[] */ + #[\Override] public function getMutations(): array { $mutations = $this->mutations->toArray(); - usort($mutations, function (ContractMutation $ma, ContractMutation $mb) { - if ($ma->getId() === $mb->getId()) { - return 0; - } - return ($ma->getId() > $mb->getId()) ? -1 : 1; - }); + usort($mutations, fn(ContractMutation $ma, ContractMutation $mb) => $mb->getId() <=> $ma->getId()); return $mutations; } + #[\Override] public function getPreviousMutation(): DomainContractMutation { throw new \BadMethodCallException(__METHOD__ . ' is not implemented.'); diff --git a/test/Functional/Entity/ContractMutation.php b/test/Functional/Entity/ContractMutation.php index 9d490f4..9321e85 100644 --- a/test/Functional/Entity/ContractMutation.php +++ b/test/Functional/Entity/ContractMutation.php @@ -23,23 +23,18 @@ class ContractMutation #[ORM\Column(type: 'integer')] private int $id; - #[ORM\ManyToOne(targetEntity: Contract::class, inversedBy: 'mutations')] - #[ORM\JoinColumn] - private Contract $contract; - #[ORM\Column(type: 'string')] - private $identifier; + private string $identifier; #[ORM\Column(type: 'integer')] private int $status; - /** - * @param Contract $contract - * @param Contract $original - */ - public function __construct(Contract $contract, Contract $original) - { - $this->contract = $contract; + public function __construct( + #[ORM\ManyToOne(targetEntity: Contract::class, inversedBy: 'mutations')] + #[ORM\JoinColumn] + private Contract $contract, + Contract $original, + ) { $this->absorb($original); } @@ -63,9 +58,6 @@ public function getStatus(): int return $this->status; } - /** - * @param Contract $original - */ protected function absorb(Contract $original): void { $this->identifier = $original->getIdentifier(); diff --git a/test/Functional/Entity/DomainContract.php b/test/Functional/Entity/DomainContract.php index 8a11b17..e40faee 100644 --- a/test/Functional/Entity/DomainContract.php +++ b/test/Functional/Entity/DomainContract.php @@ -13,19 +13,13 @@ #[Mutation(strategy: Mutation::STRATEGY_COPY_CURRENT)] class DomainContract extends Contract { - #[ORM\Column(type: 'string')] - private $domain; - - /** - * @param string $identifier - * @param int $status - * @param string $domain - */ - public function __construct($identifier, $status, $domain) - { + public function __construct( + string $identifier, + int $status, + #[ORM\Column(type: 'string')] + private string $domain, + ) { parent::__construct($identifier, $status); - - $this->domain = $domain; } public function getDomain(): string diff --git a/test/Functional/Entity/DomainContractMutation.php b/test/Functional/Entity/DomainContractMutation.php index 80e5f9f..2a8fd0b 100644 --- a/test/Functional/Entity/DomainContractMutation.php +++ b/test/Functional/Entity/DomainContractMutation.php @@ -12,16 +12,14 @@ class DomainContractMutation extends ContractMutation { #[ORM\Column(type: 'string')] - private $domain; + private string $domain; public function getDomain(): string { return $this->domain; } - /** - * @param Contract $original - */ + #[\Override] protected function absorb(Contract $original): void { if (!($original instanceof DomainContract)) { diff --git a/test/Functional/Entity/HostingContract.php b/test/Functional/Entity/HostingContract.php index 348b8f6..e287664 100644 --- a/test/Functional/Entity/HostingContract.php +++ b/test/Functional/Entity/HostingContract.php @@ -13,19 +13,13 @@ #[Mutation(strategy: Mutation::STRATEGY_COPY_CURRENT)] class HostingContract extends Contract { - #[ORM\Column(type: 'string')] - private $service; - - /** - * @param string $identifier - * @param int $status - * @param string $service - */ - public function __construct($identifier, $status, $service) - { + public function __construct( + string $identifier, + int $status, + #[ORM\Column(type: 'string')] + private string $service, + ) { parent::__construct($identifier, $status); - - $this->service = $service; } public function getService(): string diff --git a/test/Functional/Entity/HostingContractMutation.php b/test/Functional/Entity/HostingContractMutation.php index 1dd4bca..de4140f 100644 --- a/test/Functional/Entity/HostingContractMutation.php +++ b/test/Functional/Entity/HostingContractMutation.php @@ -12,16 +12,14 @@ class HostingContractMutation extends ContractMutation { #[ORM\Column(type: 'string')] - private $service; + private string $service; public function getService(): string { return $this->service; } - /** - * @param Contract $original - */ + #[\Override] protected function absorb(Contract $original): void { if (!($original instanceof HostingContract)) { diff --git a/test/Listener/MutationListenerTest.php b/test/Listener/MutationListenerTest.php index 7fdf8d0..7d68312 100644 --- a/test/Listener/MutationListenerTest.php +++ b/test/Listener/MutationListenerTest.php @@ -15,6 +15,7 @@ use Hostnet\Component\EntityMutation\Mocked\MockMutationEntityMutation; use Hostnet\Component\EntityMutation\Resolver\MutationResolverInterface; use Hostnet\Component\EntityTracker\Event\EntityChangedEvent; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** @@ -22,9 +23,9 @@ */ class MutationListenerTest extends TestCase { - private $resolver; - private $listener; - private $em; + private MutationResolverInterface&MockObject $resolver; + private MutationListener $listener; + private EntityManagerInterface&MockObject $em; public function setUp(): void { diff --git a/test/Mocked/MockMutationEntity.php b/test/Mocked/MockMutationEntity.php index 37bfd97..eb06cf8 100644 --- a/test/Mocked/MockMutationEntity.php +++ b/test/Mocked/MockMutationEntity.php @@ -6,54 +6,28 @@ namespace Hostnet\Component\EntityMutation\Mocked; -use Hostnet\Component\EntityMutation\Mutation; use Hostnet\Component\EntityMutation\MutationAwareInterface; -/** - * @Mutation() - */ class MockMutationEntity implements MutationAwareInterface { - /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(name="id",type="integer") - */ - public $id; - - /** - * @ORM\ManyToOne(targetEntity="MockEntity", inversedBy="mutations") - * @ORM\JoinColumn(name="id", referencedColumnName="id") - */ - public $mutations = []; + public ?int $id = null; - /** - * @ORM\OneToOne(targetEntity="MockEntity") - * @ORM\JoinColumn(name="parent", referencedColumnName="id") - * @var unknown - */ - public $parent; + public array $mutations = []; - /** - * @see \Hostnet\Component\EntityMutation\MutationAwareInterface::addMutation() - */ - public function addMutation($mutation): void + #[\Override] + public function addMutation(object $mutation): void { $this->mutations[] = $mutation; } - /** - * @see \Hostnet\Component\EntityMutation\MutationAwareInterface::getMutations() - */ - public function getMutations() + #[\Override] + public function getMutations(): array { return $this->mutations; } - /** - * @see \Hostnet\Component\EntityMutation\MutationAwareInterface::getPreviousMutation() - */ - public function getPreviousMutation() + #[\Override] + public function getPreviousMutation(): ?object { return current($this->mutations) ?: null; } diff --git a/test/Mocked/MockMutationEntityAttribute.php b/test/Mocked/MockMutationEntityAttribute.php index 0d5cc2e..1355ee0 100644 --- a/test/Mocked/MockMutationEntityAttribute.php +++ b/test/Mocked/MockMutationEntityAttribute.php @@ -12,46 +12,24 @@ #[Mutation(strategy: Mutation::STRATEGY_COPY_CURRENT)] class MockMutationEntityAttribute implements MutationAwareInterface { - /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(name="id",type="integer") - */ - public $id; + public ?int $id = null; - /** - * @ORM\ManyToOne(targetEntity="MockEntity", inversedBy="mutations") - * @ORM\JoinColumn(name="id", referencedColumnName="id") - */ - public $mutations = []; + public array $mutations = []; - /** - * @ORM\OneToOne(targetEntity="MockEntity") - * @ORM\JoinColumn(name="parent", referencedColumnName="id") - * @var unknown - */ - public $parent; - - /** - * @see \Hostnet\Component\EntityMutation\MutationAwareInterface::addMutation() - */ - public function addMutation($mutation): void + #[\Override] + public function addMutation(object $mutation): void { $this->mutations[] = $mutation; } - /** - * @see \Hostnet\Component\EntityMutation\MutationAwareInterface::getMutations() - */ - public function getMutations() + #[\Override] + public function getMutations(): array { return $this->mutations; } - /** - * @see \Hostnet\Component\EntityMutation\MutationAwareInterface::getPreviousMutation() - */ - public function getPreviousMutation() + #[\Override] + public function getPreviousMutation(): ?object { return current($this->mutations) ?: null; } diff --git a/test/Resolver/MutationResolverTest.php b/test/Resolver/MutationResolverTest.php index eb4af13..ed6e93b 100644 --- a/test/Resolver/MutationResolverTest.php +++ b/test/Resolver/MutationResolverTest.php @@ -10,6 +10,7 @@ use Doctrine\Persistence\Mapping\ClassMetadata; use Hostnet\Component\EntityMutation\Attributes\Mutation; use Hostnet\Component\EntityTracker\Provider\EntityMetadataProvider; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** @@ -17,9 +18,9 @@ */ class MutationResolverTest extends TestCase { - private $provider; - private $resolver; - private $em; + private EntityMetadataProvider&MockObject $provider; + private MutationResolver $resolver; + private EntityManagerInterface&MockObject $em; public function setUp(): void { From e685e6d3ebed4df5dffc94b9e0162af5f9e09d10 Mon Sep 17 00:00:00 2001 From: Jan Lam Date: Mon, 14 Sep 2026 09:12:50 +0200 Subject: [PATCH 3/7] Remove deprecated Doctrine bootstrap and flush() usage in functional tests Replace the deprecated Doctrine\ORM\Tools\Setup and EntityManager::create() with ORMSetup::createAttributeMetadataConfiguration() and DriverManager::getConnection() + the EntityManager constructor. Also enable lazy ghost objects and a DBAL schema manager factory, and opt into AttributeDriver's reportFieldsWhereDeclared mode, since all of these are otherwise deprecated defaults heading into Doctrine 3.0. Replace EmbeddableTest's flush($client) calls with flush(), since flushing a specific entity is deprecated too. Verified zero Doctrine deprecation triggers remain (via Deprecation::enableWithTriggerError()) and the full suite still passes. Co-Authored-By: Claude Sonnet 5 --- test/Functional/DiscriminatorMapTest.php | 18 +++++++++++++----- test/Functional/EmbeddableTest.php | 22 +++++++++++++++------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/test/Functional/DiscriminatorMapTest.php b/test/Functional/DiscriminatorMapTest.php index 1fc638d..add4035 100644 --- a/test/Functional/DiscriminatorMapTest.php +++ b/test/Functional/DiscriminatorMapTest.php @@ -7,10 +7,11 @@ namespace Hostnet\Component\EntityMutation\Functional; use Doctrine\Common\EventManager; +use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; use Doctrine\ORM\EntityManager; -use Doctrine\ORM\Mapping\Driver\AttributeDriver; +use Doctrine\ORM\ORMSetup; use Doctrine\ORM\Tools\SchemaTool; -use Doctrine\ORM\Tools\Setup; use Functional\Entity\Contract; use Functional\Entity\ContractMutation; use Functional\Entity\DomainContract; @@ -46,10 +47,15 @@ public static function setUpBeforeClass(): void { self::$connection = new MysqlPersistentConnection(); $params = self::$connection->getConnectionParams(); - $configuration = Setup::createConfiguration(true); $event_manager = new EventManager(); - $configuration->setMetadataDriverImpl(new AttributeDriver([__DIR__ . '/Entity'])); + $configuration = ORMSetup::createAttributeMetadataConfiguration( + paths: [__DIR__ . '/Entity'], + isDevMode: true, + reportFieldsWhereDeclared: true, + ); + $configuration->setLazyGhostObjectEnabled(true); + $configuration->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); $entity_metadat_provider = new EntityMetadataProvider(); $mutation_metadata_provider = new EntityMutationMetadataProvider(); @@ -65,7 +71,9 @@ public static function setUpBeforeClass(): void $event_manager->addEventListener('preFlush', $entity_changed_listener); $event_manager->addEventListener('entityChanged', $mutation_listener); - self::$entity_manager = EntityManager::create($params, $configuration, $event_manager); + $dbal_connection = DriverManager::getConnection($params, $configuration, $event_manager); + + self::$entity_manager = new EntityManager($dbal_connection, $configuration, $event_manager); $metadata = self::$entity_manager->getMetadataFactory()->getAllMetadata(); $schema_tool = new SchemaTool(self::$entity_manager); diff --git a/test/Functional/EmbeddableTest.php b/test/Functional/EmbeddableTest.php index 82aee88..12b722d 100644 --- a/test/Functional/EmbeddableTest.php +++ b/test/Functional/EmbeddableTest.php @@ -7,10 +7,11 @@ namespace Hostnet\Component\EntityMutation\Functional; use Doctrine\Common\EventManager; +use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; use Doctrine\ORM\EntityManager; -use Doctrine\ORM\Mapping\Driver\AttributeDriver; +use Doctrine\ORM\ORMSetup; use Doctrine\ORM\Tools\SchemaTool; -use Doctrine\ORM\Tools\Setup; use Hostnet\Component\DatabaseTest\MysqlPersistentConnection; use Hostnet\Component\EntityMutation\Functional\Entity\Client; use Hostnet\Component\EntityMutation\Functional\Entity\ContactInfo; @@ -42,10 +43,15 @@ public static function setUpBeforeClass(): void { self::$connection = new MysqlPersistentConnection(); $params = self::$connection->getConnectionParams(); - $configuration = Setup::createConfiguration(true); $event_manager = new EventManager(); - $configuration->setMetadataDriverImpl(new AttributeDriver([__DIR__ . '/Entity'])); + $configuration = ORMSetup::createAttributeMetadataConfiguration( + paths: [__DIR__ . '/Entity'], + isDevMode: true, + reportFieldsWhereDeclared: true, + ); + $configuration->setLazyGhostObjectEnabled(true); + $configuration->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); $entity_metadata_provider = new EntityMetadataProvider(); $mutation_metadata_provider = new EntityMutationMetadataProvider(); @@ -61,7 +67,9 @@ public static function setUpBeforeClass(): void $event_manager->addEventListener('preFlush', $entity_changed_listener); $event_manager->addEventListener('entityChanged', $mutation_listener); - self::$entity_manager = EntityManager::create($params, $configuration, $event_manager); + $dbal_connection = DriverManager::getConnection($params, $configuration, $event_manager); + + self::$entity_manager = new EntityManager($dbal_connection, $configuration, $event_manager); $metadata = self::$entity_manager->getMetadataFactory()->getAllMetadata(); $schema_tool = new SchemaTool(self::$entity_manager); @@ -77,7 +85,7 @@ public function testEmbeddable($mutate, $clear_after_insert, $clear_after_update $client = new Client($info); self::$entity_manager->persist($client); - self::$entity_manager->flush($client); + self::$entity_manager->flush(); /** @var Client $client */ if ($clear_after_insert) { @@ -95,7 +103,7 @@ public function testEmbeddable($mutate, $clear_after_insert, $clear_after_update $client->setContactInfo($info); } - self::$entity_manager->flush($client); + self::$entity_manager->flush(); unset($info); if ($clear_after_update) { From aa1da67c197313688f845ce8dc7679a4706e610e Mon Sep 17 00:00:00 2001 From: Jan Lam Date: Mon, 14 Sep 2026 09:18:51 +0200 Subject: [PATCH 4/7] Bump actions/checkout to v7 and actions/cache to v6 Both were pinned to old major versions (v2 and v4). actions/checkout@v2 in particular predates GitHub's Node 20 runtime, which Actions runners stop supporting entirely on 2026-09-23. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/main.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 4df289f..ff91edd 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -11,7 +11,7 @@ jobs: php-versions: ['8.3', '8.4'] name: PHP ${{ matrix.php-versions }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v7 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} @@ -19,7 +19,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') }} From cb9c5c7a4ee07a1e12003a666e30e033ff3e1e40 Mon Sep 17 00:00:00 2001 From: Jan Lam Date: Mon, 14 Sep 2026 09:24:08 +0200 Subject: [PATCH 5/7] Exclude phpcs config when deploying --- .gitattributes | 1 + 1 file changed, 1 insertion(+) 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 From a575bc4cd6e442de358c4d913748a56db8f3a178 Mon Sep 17 00:00:00 2001 From: Jan Lam Date: Mon, 14 Sep 2026 09:36:48 +0200 Subject: [PATCH 6/7] Update README for attribute-based configuration and PHP 8.3 Replace the docblock @Mutation annotation examples with the #[Mutation] attribute, drop the removed prePersist listener registration and the class option (no longer supported by the attribute), rename EntityAnnotationMetadataProvider to EntityMetadataProvider, and bump the stated PHP requirement to 8.3. Co-Authored-By: Claude Sonnet 5 --- README.md | 103 +++++++++++++++++++++++------------------------------- 1 file changed, 44 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 5a4b751..45b2dbf 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ This component lets you automatically store mutations based on two different str Requirements ------------ -The Entity Mutation Component requires a minimum of php 7.3 and runs on Doctrine2. For specific requirements, please check [composer.json](../master/composer.json). +The Entity Mutation Component requires a minimum of PHP 8.3 and Doctrine ORM 2.7. For specific requirements, please check [composer.json](../master/composer.json). Installation ------------ @@ -40,13 +40,13 @@ Documentation How does it work? ----------------- -It works by putting the `@Mutation` annotation 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 `#[Mutation]` attribute 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. Setup ----- - - You have to add `@Mutation` to your Entity + - You have to add `#[Mutation]` to your Entity - You have to create your Mutation Entity - Optionally you can add the `MutationAwareInterface` if your Entity knows about its own mutations @@ -61,112 +61,100 @@ It might look a bit complicated to set up, but it's pretty much setting up the t ```php +use Hostnet\Component\EntityMutation\Listener\MutationListener; use Hostnet\Component\EntityMutation\Resolver\MutationResolver; 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 -$annotation_metadata_provider = new EntityAnnotationMetadataProvider($annotation_reader); -$mutation_metadata_provider = new EntityMutationMetadataProvider($annotation_reader); +$entity_metadata_provider = new EntityMetadataProvider(); +$mutation_metadata_provider = new EntityMutationMetadataProvider(); -// pre flush event listener that uses the @Mutation annotation +// preFlush event listener that uses the #[Mutation] attribute $entity_changed_listener = new EntityChangedListener( - $annotation_metadata_provider, + $entity_metadata_provider, $mutation_metadata_provider ); -// the resolver is used to find the correct annotation, which +// the resolver is used to find the #[Mutation] attribute, which // fields are considered to be tracked and stored as mutation // and which entity represents your Mutation entity. -$mutation_resolver = new MutationResolver($annotation_metadata_provider); +$mutation_resolver = new MutationResolver($entity_metadata_provider); // creating the mutation listener $mutation_listener = new MutationListener($mutation_resolver); // register the events -$event_manager->addEventListener('prePersist', $entity_changed_listener); $event_manager->addEventListener('preFlush', $entity_changed_listener); $event_manager->addEventListener('entityChanged', $mutation_listener); ``` #### Configuring the Entity -All we have to do now is put the `@Mutation` annotation on our Entity. The annotation has 2 options: +All we have to do now is put the `#[Mutation]` attribute on our Entity. The attribute has 1 option: - strategy; This will determine if the current state or the previous state is stored in the Mutation - - class; the full namespace to your mutation class. By default it's the current class name suffixed with Mutation (i.e. `Acme\MyEntity` would be `Acme\MyEntityMutation` by default). + +The Mutation class is always the entity's own class name suffixed with `Mutation` (i.e. `Acme\MyEntity`'s mutations are stored via `Acme\MyEntityMutation`) — this is no longer configurable. Additionally you can configure your entity to be MutationAware, this is optional however. ```php -use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\Mapping as ORM; -use Hostnet\Component\EntityMutation\Mutation; +use Hostnet\Component\EntityMutation\Attributes\Mutation; use Hostnet\Component\EntityMutation\MutationAwareInterface; -/** - * @ORM\Entity - * @ORM\Table(name="users") - * @Mutation( - * class = "MyUserEntityMutation", - * strategy = "previous" - * ) - * The above values are equal to the defaults. They are only - * here to show how you can use them outside of this example - */ +// The strategy below is the default, only shown for illustration. +#[ORM\Entity] +#[ORM\Table(name: 'users')] +#[Mutation(strategy: Mutation::STRATEGY_COPY_PREVIOUS)] class MyUserEntity implements MutationAwareInterface { ... private $id; - /** - * @ORM\... - */ - private $city; + #[ORM\Column(...)] + private string $city; - /** - * @ORM\... - */ - private $name; + #[ORM\Column(...)] + private string $name; - /** - * @ORM\OneToMany... - * @var ArrayCollection - */ - private $mutations; + #[ORM\OneToMany(...)] + private Collection $mutations; - public function setName($name) { ... } - public function getName() { ... } + public function setName(string $name): void { ... } + public function getName(): string { ... } - public function addMutation($element) + #[\Override] + public function addMutation(object $mutation): void { - $this->mutations->add($element); + $this->mutations->add($mutation); } - public function getMutations() + #[\Override] + public function getMutations(): array { - return $this->mutations; + return $this->mutations->toArray(); } /** * Used to get the last mutation stored, you might want to change * it to return the one before that if your strategy is current. */ - public function getPreviousMutation() + #[\Override] + public function getPreviousMutation(): ?object { $criteria = (new Criteria()) ->orderBy(['id' => Criteria::DESC]) ->setMaxResults(1); - return $this->mutations->matching($criteria)->current() ? : null; + return $this->mutations->matching($criteria)->current() ?: null; } } @@ -185,22 +173,19 @@ This is done so you have full control over the what fields and how you want to s use Doctrine\ORM\Mapping as ORM; +#[ORM\Entity] class MyUserEntityMutation { ... - /** - * @ORM\ManyToOne(targetEntity="MyUserEntity", inversedBy="mutations") - */ - private $user; + #[ORM\ManyToOne(targetEntity: MyUserEntity::class, inversedBy: 'mutations')] + private MyUserEntity $user; - /** - * @ORM\... - */ - private $name; + #[ORM\Column(...)] + private string $name; - public function setName($name) { ... } - public function getName() { ... } + public function setName(string $name): void { ... } + public function getName(): string { ... } public function __construct(MyUserEntity $user, MyUserEntity $original_data) { From c5bb160917653cfe56862dda96e399c05d4697c9 Mon Sep 17 00:00:00 2001 From: Jan Lam Date: Tue, 15 Sep 2026 08:37:35 +0200 Subject: [PATCH 7/7] Stop enabling Doctrine lazy ghost objects in test bootstrap setLazyGhostObjectEnabled(true) requires a Symfony trait removed in var-exporter 8.0, which a fresh PHP 8.4 CI install resolves, crashing the functional tests. It only silenced a harmless ORM 3.0 deprecation notice, so drop it. Co-Authored-By: Claude Sonnet 5 --- test/Functional/DiscriminatorMapTest.php | 1 - test/Functional/EmbeddableTest.php | 1 - 2 files changed, 2 deletions(-) diff --git a/test/Functional/DiscriminatorMapTest.php b/test/Functional/DiscriminatorMapTest.php index add4035..a98d0f2 100644 --- a/test/Functional/DiscriminatorMapTest.php +++ b/test/Functional/DiscriminatorMapTest.php @@ -54,7 +54,6 @@ public static function setUpBeforeClass(): void isDevMode: true, reportFieldsWhereDeclared: true, ); - $configuration->setLazyGhostObjectEnabled(true); $configuration->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); $entity_metadat_provider = new EntityMetadataProvider(); diff --git a/test/Functional/EmbeddableTest.php b/test/Functional/EmbeddableTest.php index 12b722d..71f4a90 100644 --- a/test/Functional/EmbeddableTest.php +++ b/test/Functional/EmbeddableTest.php @@ -50,7 +50,6 @@ public static function setUpBeforeClass(): void isDevMode: true, reportFieldsWhereDeclared: true, ); - $configuration->setLazyGhostObjectEnabled(true); $configuration->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); $entity_metadata_provider = new EntityMetadataProvider();