Skip to content

Commit 91fd236

Browse files
committed
[FrameworkBundle] object mapper cache warmer
1 parent 01cc717 commit 91fd236

File tree

7 files changed

+311
-3
lines changed

7 files changed

+311
-3
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\CacheWarmer;
13+
14+
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
15+
use Symfony\Component\ObjectMapper\Internal\MappingCacheGenerator;
16+
use Symfony\Component\ObjectMapper\Internal\MappingCacheGeneratorInterface;
17+
use Symfony\Component\ObjectMapper\Internal\MappingCacheTrait;
18+
use Symfony\Component\ObjectMapper\Metadata\ReflectionObjectMapperMetadataFactory;
19+
20+
/**
21+
* @author Antoine Bluchet <soyuka@gmail.com>
22+
*
23+
* @internal
24+
*/
25+
final class CachedObjectMapperCacheWarmer implements CacheWarmerInterface
26+
{
27+
use MappingCacheTrait;
28+
29+
/**
30+
* @param iterable<array{source: class-string, target: class-string}> $mappedAttributes
31+
*/
32+
public function __construct(
33+
private readonly string $cacheDir,
34+
private readonly iterable $mappedAttributes,
35+
?MappingCacheGeneratorInterface $generator = null,
36+
) {
37+
$this->generator = $generator ?? new MappingCacheGenerator(new ReflectionObjectMapperMetadataFactory());
38+
}
39+
40+
public function warmUp(string $cacheDir, ?string $buildDir = null): array
41+
{
42+
if (!$this->mappedAttributes) {
43+
return [];
44+
}
45+
46+
foreach ($this->mappedAttributes as ['source' => $sourceClass, 'target' => $targetClass]) {
47+
$cacheFile = $this->getCacheFile($sourceClass, $targetClass);
48+
49+
if (is_file($cacheFile)) {
50+
continue;
51+
}
52+
53+
$this->writeCacheFile($cacheFile, $sourceClass, $targetClass);
54+
}
55+
56+
return [];
57+
}
58+
59+
public function isOptional(): bool
60+
{
61+
return true;
62+
}
63+
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ class UnusedTagsPass implements CompilerPassInterface
110110
'workflow',
111111
'object_mapper.transform_callable',
112112
'object_mapper.condition_callable',
113+
'object_mapper.attribute_metadata',
113114
];
114115

115116
public function process(ContainerBuilder $container): void

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,7 @@
154154
use Symfony\Component\Notifier\Recipient\Recipient;
155155
use Symfony\Component\Notifier\TexterInterface;
156156
use Symfony\Component\Notifier\Transport\TransportFactoryInterface as NotifierTransportFactoryInterface;
157-
use Symfony\Component\ObjectMapper\ConditionCallableInterface;
158-
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
159-
use Symfony\Component\ObjectMapper\TransformCallableInterface;
157+
use Symfony\Component\ObjectMapper\Attribute\Map;
160158
use Symfony\Component\Process\Messenger\RunProcessMessageHandler;
161159
use Symfony\Component\PropertyAccess\PropertyAccessor;
162160
use Symfony\Component\PropertyInfo\Extractor\ConstructorArgumentTypeExtractorInterface;
@@ -3530,6 +3528,23 @@ private function registerObjectMapperConfiguration(ContainerBuilder $container,
35303528
return;
35313529
}
35323530

3531+
$container->registerAttributeForAutoconfiguration(Map::class, function (ChildDefinition $definition, Map $attribute, \ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflector) {
3532+
if (!$reflector instanceof \ReflectionClass) {
3533+
return;
3534+
}
3535+
3536+
$cl = $reflector->getName();
3537+
$source = $attribute->source ?? $cl;
3538+
$target = $attribute->target ?? $cl;
3539+
3540+
if ($source !== $target) {
3541+
$definition->addTag('object_mapper.attribute_metadata', [
3542+
'source' => $source,
3543+
'target' => $target,
3544+
]);
3545+
}
3546+
});
3547+
35333548
$container->setAlias(ObjectMapperInterface::class, 'object_mapper.cached');
35343549
}
35353550

src/Symfony/Bundle/FrameworkBundle/Resources/config/object_mapper.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

1414
use Symfony\Component\ObjectMapper\CachedObjectMapper;
15+
use Symfony\Component\ObjectMapper\Internal\CacheWarmer\CachedObjectMapperCacheWarmer;
1516
use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface;
1617
use Symfony\Component\ObjectMapper\Metadata\ReflectionObjectMapperMetadataFactory;
1718
use Symfony\Component\ObjectMapper\ObjectMapper;
@@ -38,5 +39,8 @@
3839
tagged_locator('object_mapper.transform_callable'),
3940
tagged_locator('object_mapper.condition_callable'),
4041
])
42+
43+
->set('object_mapper.cached.cache_warmer', CachedObjectMapperCacheWarmer::class)
44+
->args([null, 'object_mapper.cached'])
4145
;
4246
};
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Tests\CacheWarmer;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bundle\FrameworkBundle\CacheWarmer\CachedObjectMapperCacheWarmer;
16+
use Symfony\Component\Filesystem\Filesystem;
17+
use Symfony\Component\ObjectMapper\Exception\MappingException;
18+
use Symfony\Component\ObjectMapper\Tests\Fixtures\A;
19+
use Symfony\Component\ObjectMapper\Tests\Fixtures\B;
20+
use Symfony\Component\ObjectMapper\Tests\Fixtures\C;
21+
use Symfony\Component\ObjectMapper\Tests\Fixtures\D;
22+
use Symfony\Component\ObjectMapper\Tests\Fixtures\AbstractA;
23+
24+
class CachedObjectMapperCacheWarmerTest extends TestCase
25+
{
26+
private ?string $cacheDir = null;
27+
28+
protected function setUp(): void
29+
{
30+
$this->cacheDir = sys_get_temp_dir().'/symfony_object_mapper_'.uniqid();
31+
(new Filesystem())->mkdir($this->cacheDir);
32+
}
33+
34+
protected function tearDown(): void
35+
{
36+
if (null !== $this->cacheDir) {
37+
(new Filesystem())->remove($this->cacheDir);
38+
}
39+
}
40+
41+
public function testWarmUp()
42+
{
43+
$mappingPairs = [
44+
['source' => A::class, 'target' => B::class],
45+
['source' => C::class, 'target' => D::class],
46+
];
47+
48+
$warmer = new CachedObjectMapperCacheWarmer($this->cacheDir, $mappingPairs);
49+
$warmer->warmUp($this->cacheDir);
50+
51+
$this->assertFileExists($this->cacheDir.'/'.hash('xxh128', A::class.'-to-'.B::class).'.php');
52+
$this->assertFileExists($this->cacheDir.'/'.hash('xxh128', C::class.'-to-'.D::class).'.php');
53+
}
54+
55+
public function testWarmUpWithNoPairs()
56+
{
57+
$warmer = new CachedObjectMapperCacheWarmer($this->cacheDir, []);
58+
$warmer->warmUp($this->cacheDir);
59+
60+
$this->assertEmpty(glob($this->cacheDir.'/*.php'));
61+
}
62+
63+
public function testWarmUpWithAbstractClass()
64+
{
65+
$this->expectException(MappingException::class);
66+
$this->expectExceptionMessage('Can not generate mapping metadata from an abstract class "Symfony\Component\ObjectMapper\Tests\Fixtures\AbstractA".');
67+
$mappingPairs = [
68+
['source' => AbstractA::class, 'target' => B::class],
69+
['source' => C::class, 'target' => D::class],
70+
];
71+
72+
$warmer = new CachedObjectMapperCacheWarmer($this->cacheDir, $mappingPairs);
73+
$warmer->warmUp($this->cacheDir);
74+
75+
$this->assertFileExists($this->cacheDir.'/'.hash('xxh128', C::class.'-to-'.D::class).'.php');
76+
$this->assertFileDoesNotExist($this->cacheDir.'/'.hash('xxh128', AbstractA::class.'-to-'.B::class).'.php');
77+
}
78+
}
79+
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\ObjectMapper\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
17+
18+
/**
19+
* @author Antoine Bluchet <soyuka@gmail.com>
20+
*/
21+
final class AttributeMetadataPass implements CompilerPassInterface
22+
{
23+
public function process(ContainerBuilder $container): void
24+
{
25+
$warmerServiceId = 'object_mapper.cached.cache_warmer';
26+
if (!$container->hasDefinition($warmerServiceId)) {
27+
return;
28+
}
29+
30+
$mappedPairs = [];
31+
$resolve = $container->getParameterBag()->resolveValue(...);
32+
foreach ($container->getDefinitions() as $id => $definition) {
33+
if (!$tags = $definition->getTag('object_mapper.attribute_metadata')) {
34+
continue;
35+
}
36+
37+
if (!$definition->hasTag('container.excluded')) {
38+
throw new InvalidArgumentException(\sprintf('The resource "%s" with a "Map" attribute must be tagged with "container.excluded".', $id));
39+
}
40+
41+
foreach ($tags as $tag) {
42+
if (!isset($tag['source']) || !isset($tag['target'])) {
43+
continue;
44+
}
45+
46+
$source = $resolve($tag['source']);
47+
$target = $resolve($tag['target']);
48+
49+
if (class_exists($source) && class_exists($target)) {
50+
$mappedPairs[] = ['source' => $source, 'target' => $target];
51+
}
52+
}
53+
54+
$container->removeDefinition($id);
55+
}
56+
57+
if (!$mappedPairs) {
58+
return;
59+
}
60+
61+
$container->getDefinition($warmerServiceId)
62+
->replaceArgument(0, $mappedPairs);
63+
}
64+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace Symfony\Component\ObjectMapper\Tests\DependencyInjection;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Symfony\Component\DependencyInjection\ContainerBuilder;
7+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
8+
use Symfony\Component\ObjectMapper\DependencyInjection\AttributeMetadataPass;
9+
use Symfony\Component\ObjectMapper\Tests\Fixtures\A;
10+
use Symfony\Component\ObjectMapper\Tests\Fixtures\B;
11+
use Symfony\Component\ObjectMapper\Tests\Fixtures\C;
12+
use Symfony\Component\ObjectMapper\Tests\Fixtures\D;
13+
use Symfony\Component\ObjectMapper\Tests\Fixtures\ClassWithoutTarget;
14+
15+
class AttributeMetadataPassTest extends TestCase
16+
{
17+
public function testProcessWithNoWarmer()
18+
{
19+
$container = new ContainerBuilder();
20+
(new AttributeMetadataPass())->process($container);
21+
$this->expectNotToPerformAssertions();
22+
}
23+
24+
public function testProcessWithWarmerButNoTaggedServices()
25+
{
26+
$container = new ContainerBuilder();
27+
$container->register('object_mapper.cached.cache_warmer');
28+
29+
(new AttributeMetadataPass())->process($container);
30+
31+
$this->assertCount(0, $container->getDefinition('object_mapper.cached.cache_warmer')->getArguments());
32+
}
33+
34+
public function testProcessThrowsExceptionForMissingExcludeTag()
35+
{
36+
$this->expectException(InvalidArgumentException::class);
37+
$this->expectExceptionMessage(\sprintf('The resource "%s" with a "Map" attribute must be tagged with "container.excluded".', A::class));
38+
39+
$container = new ContainerBuilder();
40+
$container->register('object_mapper.cached.cache_warmer', \stdClass::class)->addArgument([]);
41+
$container->register(A::class)
42+
->addTag('object_mapper.attribute_metadata', ['source' => A::class, 'target' => B::class]);
43+
44+
(new AttributeMetadataPass())->process($container);
45+
}
46+
47+
public function testProcessWithTaggedServices()
48+
{
49+
$container = new ContainerBuilder();
50+
$container->setParameter('source.class_a', A::class);
51+
$container->register('object_mapper.cached.cache_warmer', \stdClass::class)->addArgument([]);
52+
53+
$container->register('service1', '%source.class_a%')
54+
->addTag('object_mapper.attribute_metadata', ['source' => '%source.class_a%', 'target' => B::class])
55+
->addTag('container.excluded');
56+
$container->register('service2', C::class)
57+
->addTag('object_mapper.attribute_metadata', ['source' => C::class, 'target' => D::class])
58+
->addTag('container.excluded');
59+
$container->register('service3', ClassWithoutTarget::class)
60+
->addTag('object_mapper.attribute_metadata', ['source' => ClassWithoutTarget::class])
61+
->addTag('container.excluded');
62+
63+
(new AttributeMetadataPass())->process($container);
64+
65+
$warmerDef = $container->getDefinition('object_mapper.cached.cache_warmer');
66+
$this->assertCount(1, $warmerDef->getArguments());
67+
$mappedPairs = $warmerDef->getArgument(0);
68+
69+
$expectedPairs = [
70+
['source' => A::class, 'target' => B::class],
71+
['source' => C::class, 'target' => D::class],
72+
];
73+
74+
$this->assertCount(2, $mappedPairs);
75+
$this->assertEquals($expectedPairs, $mappedPairs);
76+
77+
$this->assertFalse($container->hasDefinition('service1'));
78+
$this->assertFalse($container->hasDefinition('service2'));
79+
$this->assertFalse($container->hasDefinition('service3'));
80+
}
81+
}
82+

0 commit comments

Comments
 (0)