Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 14 additions & 0 deletions src/Attributes/Blamable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
/**
* @copyright 2026-present Hostnet B.V.
*/
declare(strict_types=1);

namespace Hostnet\Component\EntityBlamable\Attributes;

use Hostnet\Component\EntityTracker\Attributes\Tracked;

#[\Attribute(\Attribute::TARGET_CLASS)]
class Blamable extends Tracked
{
}
2 changes: 2 additions & 0 deletions src/Blamable.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
/**
* @Annotation
* @Target({"CLASS"})
*
* @deprecated Please use the attribute instead
*/
class Blamable extends Tracked
{
Expand Down
2 changes: 2 additions & 0 deletions src/BlamableInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

/**
* Implement on Entities to trigger the BlamableEntityListener
*
* @TODO: add (return)typehints on next BC break, when removing doctrine/annotations
*/
interface BlamableInterface
{
Expand Down
64 changes: 42 additions & 22 deletions src/Listener/BlamableListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@

namespace Hostnet\Component\EntityBlamable\Listener;

use Doctrine\Persistence\ObjectManager;
use Hostnet\Component\EntityBlamable\BlamableInterface;
use Hostnet\Component\EntityBlamable\Provider\BlamableProviderInterface;
use Hostnet\Component\EntityBlamable\Resolver\BlamableResolverInterface;
use Hostnet\Component\EntityTracker\Event\EntityChangedEvent;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;

/**
* Listens to "Events::entityChanged"
Expand All @@ -19,37 +23,21 @@
*/
class BlamableListener
{
/**
* @var BlamableResolverInterface
*/
private $resolver;

/**
* @var BlamableProviderInterface
*/
private $provider;

/**
* @param BlamableResolverInterface $resolver
* @param BlamableProviderInterface $provider
*/
public function __construct(
BlamableResolverInterface $resolver,
BlamableProviderInterface $provider
private BlamableResolverInterface $resolver,
private BlamableProviderInterface $provider,
private CacheItemPoolInterface $is_blamable_cache = new ArrayAdapter()
) {
$this->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;
}

Expand All @@ -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;
}
}
3 changes: 3 additions & 0 deletions src/Provider/BlamableProviderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

namespace Hostnet\Component\EntityBlamable\Provider;

/**
* @TODO: add (return)typehints on next BC break, when removing doctrine/annotations
*/
interface BlamableProviderInterface
{
/**
Expand Down
29 changes: 12 additions & 17 deletions src/Resolver/BlamableResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
6 changes: 5 additions & 1 deletion src/Resolver/BlamableResolverInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
namespace Hostnet\Component\EntityBlamable\Resolver;

use Doctrine\ORM\EntityManagerInterface;
use Hostnet\Component\EntityBlamable\Attributes\Blamable;

interface BlamableResolverInterface
{
Expand All @@ -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;
}
23 changes: 23 additions & 0 deletions test/Attributes/BlamableTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
/**
* @copyright 2026-present Hostnet B.V.
*/
declare(strict_types=1);

namespace Hostnet\Component\EntityBlamable\Attributes;

use Hostnet\Component\EntityTracker\Attributes\Tracked;
use PHPUnit\Framework\TestCase;

/**
* @covers \Hostnet\Component\EntityBlamable\Attributes\Blamable
*/
class BlamableTest extends TestCase
{
public function test(): void
{
$blamable = new Blamable();

self::assertInstanceOf(Tracked::class, $blamable);
}
}
84 changes: 81 additions & 3 deletions test/Listener/BlamableListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

namespace Hostnet\Component\EntityBlamable\Listener;

use Hostnet\Component\EntityBlamable\Blamable;
use Hostnet\Component\EntityBlamable\Attributes\Blamable;
use Hostnet\Component\EntityBlamable\Blamable as BlamableAnnotation;
use Hostnet\Component\EntityTracker\Event\EntityChangedEvent;
use PHPUnit\Framework\TestCase;

Expand Down Expand Up @@ -68,7 +69,7 @@ public function testOnEntityChangedExistingEntity(): void
$this->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);
Expand Down Expand Up @@ -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.
}
}
Loading
Loading