Skip to content

Commit 4ee0f1f

Browse files
authored
Attribute support, drop PHP<8.3 support (#30)
* Attribute support * Drop PHP<8.3 support
1 parent 942c2e4 commit 4ee0f1f

13 files changed

Lines changed: 328 additions & 56 deletions

.github/workflows/main.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88

99
strategy:
1010
matrix:
11-
php-versions: ['7.3', '7.4', '8.0']
11+
php-versions: ['8.3', '8.4']
1212
name: PHP ${{ matrix.php-versions }}
1313
steps:
1414
- uses: actions/checkout@v2

composer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
"license": "MIT",
55
"minimum-stability": "stable",
66
"require": {
7-
"php": "^7.3||^8.0",
7+
"php": "^8.3",
88
"doctrine/orm": "^2.7.4",
9-
"hostnet/entity-tracker-component": "^2.0.1"
9+
"hostnet/entity-tracker-component": "^2.3.0"
1010
},
1111
"require-dev": {
1212
"hostnet/database-test-lib": "^2.0.2",
1313
"hostnet/phpcs-tool": "^9.1.0",
1414
"phpunit/phpunit": "^9.5.6",
15-
"symfony/cache": "^5.3"
15+
"symfony/cache": "^6.4|^7.4"
1616
},
1717
"autoload": {
1818
"psr-4": {

src/Attributes/Mutation.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
/**
3+
* @copyright 2026-present Hostnet B.V.
4+
*/
5+
declare(strict_types=1);
6+
7+
namespace Hostnet\Component\EntityMutation\Attributes;
8+
9+
use Hostnet\Component\EntityTracker\Attributes\Tracked;
10+
11+
#[\Attribute(\Attribute::TARGET_CLASS)]
12+
class Mutation extends Tracked
13+
{
14+
public function __construct(
15+
public string $strategy = self::STRATEGY_COPY_PREVIOUS,
16+
) {
17+
}
18+
/**
19+
* The Previous values will be stored in the mutation table. This is the
20+
* default strategy.
21+
*/
22+
public const string STRATEGY_COPY_PREVIOUS = 'previous';
23+
24+
/**
25+
* The current values will be stored in the mutation table. And the
26+
* mutation will also be added on creation of the entity.
27+
*/
28+
public const string STRATEGY_COPY_CURRENT = 'current';
29+
30+
/**
31+
* Get the strategy for storing the mutation data.
32+
*
33+
* @return Mutation::STRATEGY_COPY_PREVIOUS|Mutation::STRATEGY_COPY_CURRENT
34+
*/
35+
public function getStrategy(): string
36+
{
37+
if (!in_array($this->strategy, [self::STRATEGY_COPY_PREVIOUS, self::STRATEGY_COPY_CURRENT], true)) {
38+
throw new \RuntimeException(
39+
sprintf("Unknown strategy '%s' for class %s.", $this->strategy, get_class($this))
40+
);
41+
}
42+
43+
return $this->strategy;
44+
}
45+
}

src/Listener/MutationListener.php

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,34 @@
66

77
namespace Hostnet\Component\EntityMutation\Listener;
88

9-
use Hostnet\Component\EntityMutation\Mutation;
9+
use Hostnet\Component\EntityMutation\Attributes\Mutation;
1010
use Hostnet\Component\EntityMutation\MutationAwareInterface;
1111
use Hostnet\Component\EntityMutation\Resolver\MutationResolverInterface;
1212
use Hostnet\Component\EntityTracker\Event\EntityChangedEvent;
13+
use Psr\Cache\CacheItemInterface;
14+
use Psr\Cache\CacheItemPoolInterface;
15+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
1316

1417
class MutationListener
1518
{
16-
/**
17-
* @var MutationResolverInterface
18-
*/
19-
private $resolver;
20-
2119
/**
2220
* @param MutationResolverInterface $resolver
2321
*/
24-
public function __construct(MutationResolverInterface $resolver)
25-
{
26-
$this->resolver = $resolver;
22+
public function __construct(
23+
private MutationResolverInterface $resolver,
24+
private CacheItemPoolInterface $is_mutation_cache = new ArrayAdapter()
25+
) {
2726
}
2827

2928
/**
3029
* @param EntityChangedEvent $event
3130
*/
32-
public function entityChanged(EntityChangedEvent $event)
31+
public function entityChanged(EntityChangedEvent $event): void
3332
{
3433
$em = $event->getEntityManager();
3534
$entity = $event->getCurrentEntity();
3635

37-
if (null === ($annotation = $this->resolver->getMutationAnnotation($em, $entity))) {
36+
if (false === $strategy = $this->getMutationStrategy($em, $entity)) {
3837
return;
3938
}
4039

@@ -44,8 +43,6 @@ public function entityChanged(EntityChangedEvent $event)
4443
return;
4544
}
4645

47-
$strategy = $annotation->getStrategy();
48-
4946
if ($strategy === Mutation::STRATEGY_COPY_PREVIOUS && null === $event->getOriginalEntity()) {
5047
return;
5148
}
@@ -72,4 +69,32 @@ public function entityChanged(EntityChangedEvent $event)
7269
$entity->addMutation($mutation);
7370
}
7471
}
72+
73+
private function getMutationStrategy($em, $entity): false|string
74+
{
75+
$cache_key = base64_encode('MUTATION-' . get_class($entity));
76+
$cached_item = $this->is_mutation_cache->getItem($cache_key);
77+
78+
if ($cached_item->isHit()) {
79+
return $cached_item->get();
80+
}
81+
82+
if (null !== $attribute = $this->resolver->getMutationAttribute($em, $entity)) {
83+
return $this->save($cached_item, $attribute->getStrategy());
84+
}
85+
86+
if (null !== $annotation = $this->resolver->getMutationAnnotation($em, $entity)) {
87+
return $this->save($cached_item, $annotation->getStrategy());
88+
}
89+
90+
return $this->save($cached_item, false);
91+
}
92+
93+
private function save(CacheItemInterface $item, false|string $value): false|string
94+
{
95+
$item->set($value);
96+
$this->is_mutation_cache->save($item);
97+
98+
return $value;
99+
}
75100
}

src/Mutation.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,26 @@
1111
/**
1212
* @Annotation
1313
* @Target({"CLASS"})
14+
*
15+
* @deprecated Please use the attribute instead.
1416
*/
1517
class Mutation extends Tracked
1618
{
1719
/**
1820
* The Previous values will be stored in the mutation table. This is the
1921
* default strategy.
2022
*/
21-
public const STRATEGY_COPY_PREVIOUS = 'previous';
23+
public const string STRATEGY_COPY_PREVIOUS = 'previous';
2224

2325
/**
2426
* The Previous values will be stored in the mutation table. And the
2527
* mutation will also be added on creation of the entity.
2628
*/
27-
public const STRATEGY_COPY_CURRENT = 'current';
29+
public const string STRATEGY_COPY_CURRENT = 'current';
2830

31+
/**
32+
* @deprecated Not supported by the attribute
33+
*/
2934
public $class = '';
3035

3136
/**
@@ -37,6 +42,8 @@ class Mutation extends Tracked
3742
* Get the strategy for storing the mutation data.
3843
*
3944
* @return Mutation::STRATEGY_COPY_PREVIOUS|Mutation::STRATEGY_COPY_CURRENT
45+
*
46+
* @deprecated Please use the attribute instead.
4047
*/
4148
public function getStrategy()
4249
{

src/MutationAwareInterface.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
namespace Hostnet\Component\EntityMutation;
88

9+
/**
10+
* TODO: add typehints on next BC break, removing doctrine/annotations
11+
*/
912
interface MutationAwareInterface
1013
{
1114
/**

src/Resolver/MutationResolver.php

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,43 @@
77
namespace Hostnet\Component\EntityMutation\Resolver;
88

99
use Doctrine\ORM\EntityManagerInterface;
10-
use Hostnet\Component\EntityMutation\Mutation;
11-
use Hostnet\Component\EntityTracker\Provider\EntityAnnotationMetadataProvider;
10+
use Hostnet\Component\EntityMutation\Attributes\Mutation;
11+
use Hostnet\Component\EntityMutation\Mutation as MutationAnnotation;
12+
use Hostnet\Component\EntityTracker\Provider\EntityMetadataProvider;
1213

1314
class MutationResolver implements MutationResolverInterface
1415
{
1516
/**
1617
* @var string
1718
*/
18-
private $annotation = Mutation::class;
19+
private $annotation = MutationAnnotation::class;
1920

20-
/**
21-
* @var EntityAnnotationMetadataProvider
22-
*/
23-
private $provider;
24-
25-
/**
26-
* @param EntityAnnotationMetadataProvider $provider
27-
*/
28-
public function __construct(EntityAnnotationMetadataProvider $provider)
21+
public function __construct(private EntityMetadataProvider $provider)
2922
{
30-
$this->provider = $provider;
3123
}
3224

3325
/**
3426
* {@inheritdoc}
3527
*/
36-
public function getMutationAnnotation(EntityManagerInterface $em, $entity)
28+
public function getMutationAnnotation(EntityManagerInterface $em, $entity): ?MutationAnnotation
3729
{
3830
return $this->provider->getAnnotationFromEntity($em, $entity, $this->annotation);
3931
}
4032

33+
public function getMutationAttribute(EntityManagerInterface $em, $entity): ?Mutation
34+
{
35+
return $this->provider->getAttributeFromEntity(Mutation::class, $em, $entity);
36+
}
37+
4138
/**
4239
* {@inheritdoc}
4340
*/
44-
public function getMutationClassName(EntityManagerInterface $em, $entity)
41+
public function getMutationClassName(EntityManagerInterface $em, $entity): string
4542
{
46-
if (null === ($annotation = $this->getMutationAnnotation($em, $entity))) {
47-
return null;
43+
$annotation = $this->getMutationAnnotation($em, $entity);
44+
// If $annotation is null, we must be using the attribute, otherwise this code would not get hit.
45+
if (null === $annotation) {
46+
return get_class($entity) . 'Mutation';
4847
}
4948

5049
return !empty($annotation->class) ? $annotation->class : get_class($entity) . 'Mutation';
@@ -53,7 +52,7 @@ public function getMutationClassName(EntityManagerInterface $em, $entity)
5352
/**
5453
* {@inheritdoc}
5554
*/
56-
public function getMutatableFields(EntityManagerInterface $em, $entity)
55+
public function getMutatableFields(EntityManagerInterface $em, $entity): array
5756
{
5857
$mutation_class = $this->getMutationClassName($em, $entity);
5958
$metadata = $em->getClassMetadata(get_class($entity));

src/Resolver/MutationResolverInterface.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,30 @@
77
namespace Hostnet\Component\EntityMutation\Resolver;
88

99
use Doctrine\ORM\EntityManagerInterface;
10-
use Hostnet\Component\EntityMutation\Mutation;
10+
use Hostnet\Component\EntityMutation\Attributes\Mutation;
11+
use Hostnet\Component\EntityMutation\Mutation as MutationAnnotation;
1112

1213
interface MutationResolverInterface
1314
{
1415
/**
1516
* Return the mutation annotation
1617
*
17-
* @return Mutation
18+
*
19+
* @deprecated Please use the attribute instead.
1820
*/
19-
public function getMutationAnnotation(EntityManagerInterface $em, $entity);
21+
public function getMutationAnnotation(EntityManagerInterface $em, $entity): ?MutationAnnotation;
22+
23+
public function getMutationAttribute(EntityManagerInterface $em, $entity): ?Mutation;
2024

2125
/**
2226
* Return the mutation class name
23-
*
24-
* @return string
2527
*/
26-
public function getMutationClassName(EntityManagerInterface $em, $entity);
28+
public function getMutationClassName(EntityManagerInterface $em, $entity): string;
2729

2830
/**
2931
* Return list of mutatable fields
3032
*
3133
* @return string[]
3234
*/
33-
public function getMutatableFields(EntityManagerInterface $em, $entity);
35+
public function getMutatableFields(EntityManagerInterface $em, $entity): array;
3436
}

test/Attributes/MutationTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
/**
3+
* @copyright 2014-present Hostnet B.V.
4+
*/
5+
declare(strict_types=1);
6+
7+
namespace Hostnet\Component\EntityMutation\Attributes;
8+
9+
use PHPUnit\Framework\TestCase;
10+
11+
/**
12+
* @covers \Hostnet\Component\EntityMutation\Attributes\Mutation
13+
*/
14+
class MutationTest extends TestCase
15+
{
16+
public function testGetStrategy(): void
17+
{
18+
$mutation = new Mutation();
19+
$this->assertEquals(Mutation::STRATEGY_COPY_PREVIOUS, $mutation->getStrategy());
20+
$mutation->strategy = Mutation::STRATEGY_COPY_CURRENT;
21+
$this->assertEquals(Mutation::STRATEGY_COPY_CURRENT, $mutation->getStrategy());
22+
}
23+
24+
/**
25+
* @dataProvider getStrategyExceptionProvider
26+
* @param mixed $strategy
27+
*/
28+
public function testGetStrategyException($strategy): void
29+
{
30+
$mutation = new Mutation();
31+
$mutation->strategy = $strategy;
32+
$this->expectException(\RuntimeException::class);
33+
$mutation->getStrategy();
34+
}
35+
36+
public function getStrategyExceptionProvider(): iterable
37+
{
38+
return [
39+
[''],
40+
['test'],
41+
['last'],
42+
['before'],
43+
];
44+
}
45+
}

0 commit comments

Comments
 (0)