diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index f925e3c..b7896e1 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - php-versions: ['7.3', '7.4', '8.0'] + php-versions: ['8.3', '8.4'] name: PHP ${{ matrix.php-versions }} steps: - uses: actions/checkout@v2 diff --git a/composer.json b/composer.json index 64ab9e5..aaf4e30 100644 --- a/composer.json +++ b/composer.json @@ -4,9 +4,10 @@ "license": "MIT", "minimum-stability": "stable", "require": { - "php": "^7.3|^8.0", + "php": "^8.3", "doctrine/orm": "^2.4.0", - "hostnet/entity-tracker-component": "^1.2.0||^2.0.0" + "hostnet/entity-tracker-component": "^2.3.0", + "symfony/cache": "^6.4|^7.4" }, "require-dev": { "hostnet/phpcs-tool": "^9.1.0", diff --git a/src/Attributes/Blamable.php b/src/Attributes/Blamable.php new file mode 100644 index 0000000..99eaae7 --- /dev/null +++ b/src/Attributes/Blamable.php @@ -0,0 +1,14 @@ +resolver = $resolver; - $this->provider = $provider; } /** * @param EntityChangedEvent $event */ - public function entityChanged(EntityChangedEvent $event) + public function entityChanged(EntityChangedEvent $event): void { - $entity = $event->getCurrentEntity(); - $annotation = $this->resolver->getBlamableAnnotation($event->getEntityManager(), $entity); + $entity = $event->getCurrentEntity(); - if (null === $annotation || !$entity instanceof BlamableInterface) { + if (!$this->isBlamable($event->getEntityManager(), $entity)) { return; } @@ -64,4 +52,36 @@ public function entityChanged(EntityChangedEvent $event) $entity->setCreatedAt($changed_at); } } + + private function isBlamable(ObjectManager $em, mixed $entity): bool + { + $cache_key = base64_encode('BLAMABLE-' . get_class($entity)); + $cached_item = $this->is_blamable_cache->getItem($cache_key); + + if ($cached_item->isHit()) { + return $cached_item->get(); + } + + if (!($entity instanceof BlamableInterface)) { + return $this->save($cached_item, false); + } + + if (null !== $this->resolver->getBlamableAnnotation($em, $entity)) { + return $this->save($cached_item, true); + } + + if (null !== $this->resolver->getBlamableAttribute($em, $entity)) { + return $this->save($cached_item, true); + } + + return $this->save($cached_item, false); + } + + private function save(CacheItemInterface $item, bool $value): bool + { + $item->set($value); + $this->is_blamable_cache->save($item); + + return $value; + } } diff --git a/src/Provider/BlamableProviderInterface.php b/src/Provider/BlamableProviderInterface.php index 97f5a56..987c486 100644 --- a/src/Provider/BlamableProviderInterface.php +++ b/src/Provider/BlamableProviderInterface.php @@ -6,6 +6,9 @@ namespace Hostnet\Component\EntityBlamable\Provider; +/** + * @TODO: add (return)typehints on next BC break, when removing doctrine/annotations + */ interface BlamableProviderInterface { /** diff --git a/src/Resolver/BlamableResolver.php b/src/Resolver/BlamableResolver.php index fbd1d0c..ca3a7c2 100644 --- a/src/Resolver/BlamableResolver.php +++ b/src/Resolver/BlamableResolver.php @@ -7,33 +7,28 @@ namespace Hostnet\Component\EntityBlamable\Resolver; use Doctrine\ORM\EntityManagerInterface; -use Hostnet\Component\EntityTracker\Provider\EntityAnnotationMetadataProvider; +use Hostnet\Component\EntityBlamable\Attributes\Blamable; +use Hostnet\Component\EntityBlamable\Blamable as BlamabeAnnotation; +use Hostnet\Component\EntityTracker\Provider\EntityMetadataProvider; class BlamableResolver implements BlamableResolverInterface { - /** - * @var string - */ - private $annotation = 'Hostnet\Component\EntityBlamable\Blamable'; - - /** - * @var EntityAnnotationMetadataProvider - */ - private $provider; - - /** - * @param EntityAnnotationMetadataProvider $provider - */ - public function __construct(EntityAnnotationMetadataProvider $provider) + public function __construct(private EntityMetadataProvider $provider) { - $this->provider = $provider; } /** * @see \Hostnet\Component\EntityBlamable\Resolver\BlamableResolverInterface::getBlamableAnnotation() + * + * @deprecated Please use the attribute instead. */ public function getBlamableAnnotation(EntityManagerInterface $em, $entity) { - return $this->provider->getAnnotationFromEntity($em, $entity, $this->annotation); + return $this->provider->getAnnotationFromEntity($em, $entity, BlamabeAnnotation::class); + } + + public function getBlamableAttribute(EntityManagerInterface $em, $entity): ?Blamable + { + return $this->provider->getAttributeFromEntity(Blamable::class, $em, $entity); } } diff --git a/src/Resolver/BlamableResolverInterface.php b/src/Resolver/BlamableResolverInterface.php index 294761e..97898ff 100644 --- a/src/Resolver/BlamableResolverInterface.php +++ b/src/Resolver/BlamableResolverInterface.php @@ -7,6 +7,7 @@ namespace Hostnet\Component\EntityBlamable\Resolver; use Doctrine\ORM\EntityManagerInterface; +use Hostnet\Component\EntityBlamable\Attributes\Blamable; interface BlamableResolverInterface { @@ -15,7 +16,10 @@ interface BlamableResolverInterface * * @param EntityManagerInterface $em * @param mixed $entity - * @return Blamable + * + * @deprecated Please use the attribute instead. */ public function getBlamableAnnotation(EntityManagerInterface $em, $entity); + + public function getBlamableAttribute(EntityManagerInterface $em, $entity): ?Blamable; } diff --git a/test/Attributes/BlamableTest.php b/test/Attributes/BlamableTest.php new file mode 100644 index 0000000..57697b2 --- /dev/null +++ b/test/Attributes/BlamableTest.php @@ -0,0 +1,23 @@ +resolver ->expects($this->once()) ->method('getBlamableAnnotation') - ->willReturn(new Blamable()); + ->willReturn(new BlamableAnnotation()); $event = new EntityChangedEvent($this->em, $this->entity, new \stdClass(), []); $listener = new BlamableListener($this->resolver, $this->provider); @@ -108,10 +109,87 @@ public function testOnEntityChangedNewEntity(): void $this->resolver ->expects($this->once()) ->method('getBlamableAnnotation') - ->willReturn(new Blamable()); + ->willReturn(new BlamableAnnotation()); $event = new EntityChangedEvent($this->em, $this->entity, null, []); $listener = new BlamableListener($this->resolver, $this->provider); $listener->entityChanged($event); } + + public function testOnEntityChangedNewEntityWithAttribute(): void + { + $at = new \DateTime(); + $by = 'henk'; + + $this->provider + ->expects($this->once()) + ->method('getChangedAt') + ->willReturn($at); + + $this->provider + ->expects($this->once()) + ->method('getUpdatedBy') + ->willReturn($by); + + $this->resolver + ->expects($this->once()) + ->method('getBlamableAnnotation') + ->willReturn(null); + + $this->resolver + ->expects($this->once()) + ->method('getBlamableAttribute') + ->willReturn(new Blamable()); + + $entity = new EntityWithAttribute(); + $event = new EntityChangedEvent($this->em, $entity, null, []); + + $this->assertNull($entity->getCreatedAt()); + $this->assertNull($entity->getUpdatedBy()); + $this->assertNull($entity->getUpdatedAt()); + + $listener = new BlamableListener($this->resolver, $this->provider); + $listener->entityChanged($event); + + $this->assertSame($at, $entity->getCreatedAt()); + $this->assertSame($by, $entity->getUpdatedBy()); + $this->assertSame($at, $entity->getUpdatedAt()); + } + + public function testOnEntityChangedNewEntityWithInterfaceOnly(): void + { + $this->provider + ->expects($this->never()) + ->method('getChangedAt'); + + $this->provider + ->expects($this->never()) + ->method('getUpdatedBy'); + + $this->resolver + ->expects($this->once()) + ->method('getBlamableAnnotation') + ->willReturn(null); + + $this->resolver + ->expects($this->once()) + ->method('getBlamableAttribute') + ->willReturn(null); + + $entity = new EntityWithInterfaceOnly(); + $event = new EntityChangedEvent($this->em, $entity, null, []); + + $this->assertNull($entity->getCreatedAt()); + $this->assertNull($entity->getUpdatedBy()); + $this->assertNull($entity->getUpdatedAt()); + + $listener = new BlamableListener($this->resolver, $this->provider); + $listener->entityChanged($event); + + $this->assertNull($entity->getCreatedAt()); + $this->assertNull($entity->getUpdatedBy()); + $this->assertNull($entity->getUpdatedAt()); + + $listener->entityChanged($event); // cover the case where the blamable cache is already filled. + } } diff --git a/test/Listener/EntityWithAttribute.php b/test/Listener/EntityWithAttribute.php new file mode 100644 index 0000000..a14aa75 --- /dev/null +++ b/test/Listener/EntityWithAttribute.php @@ -0,0 +1,49 @@ +updated_by = $by; + } + + public function getUpdatedBy(): ?string + { + return $this->updated_by; + } + + public function setUpdatedAt(\DateTime $at): void + { + $this->updated_at = $at; + } + + public function getUpdatedAt(): ?\DateTime + { + return $this->updated_at; + } + + public function setCreatedAt(\DateTime $at): void + { + $this->created_at = $at; + } + + public function getCreatedAt(): ?\DateTime + { + return $this->created_at; + } +} diff --git a/test/Listener/EntityWithInterfaceOnly.php b/test/Listener/EntityWithInterfaceOnly.php new file mode 100644 index 0000000..5fc50cd --- /dev/null +++ b/test/Listener/EntityWithInterfaceOnly.php @@ -0,0 +1,47 @@ +updated_by = $by; + } + + public function getUpdatedBy(): ?string + { + return $this->updated_by; + } + + public function setUpdatedAt(\DateTime $at): void + { + $this->updated_at = $at; + } + + public function getUpdatedAt(): ?\DateTime + { + return $this->updated_at; + } + + public function setCreatedAt(\DateTime $at): void + { + $this->created_at = $at; + } + + public function getCreatedAt(): ?\DateTime + { + return $this->created_at; + } +} diff --git a/test/Resolver/BlamableResolverTest.php b/test/Resolver/BlamableResolverTest.php index c910e1a..cb03318 100644 --- a/test/Resolver/BlamableResolverTest.php +++ b/test/Resolver/BlamableResolverTest.php @@ -6,6 +6,8 @@ namespace Hostnet\Component\EntityBlamable\Resolver; +use Hostnet\Component\EntityBlamable\Attributes\Blamable; +use Hostnet\Component\EntityBlamable\Blamable as BlamableAnnotation; use PHPUnit\Framework\TestCase; /** @@ -39,8 +41,23 @@ public function testGetBlamableAnnotation(): void $this->provider ->expects($this->once()) ->method('getAnnotationFromEntity') - ->with($this->em, $entity, 'Hostnet\Component\EntityBlamable\Blamable'); + ->with($this->em, $entity, BlamableAnnotation::class); $this->resolver->getBlamableAnnotation($this->em, $entity); } + + public function testGetRevisionAttribute(): void + { + $entity = new \stdClass(); + + $attribute = new Blamable(); + + $this->provider + ->expects($this->once()) + ->method('getAttributeFromEntity') + ->with(Blamable::class, $this->em, $entity) + ->willReturn($attribute); + + self::assertSame($attribute, $this->resolver->getBlamableAttribute($this->em, $entity)); + } }