Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\FrameworkBundle\CacheWarmer;

use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
use Symfony\Component\ObjectMapper\Internal\MappingCacheGenerator;
use Symfony\Component\ObjectMapper\Internal\MappingCacheGeneratorInterface;
use Symfony\Component\ObjectMapper\Internal\MappingCacheTrait;
use Symfony\Component\ObjectMapper\Metadata\ReflectionObjectMapperMetadataFactory;

/**
* @internal
*/
final class CachedObjectMapperCacheWarmer implements CacheWarmerInterface
{
use MappingCacheTrait;

/**
* @param iterable<array{source: class-string, target: class-string}> $mappedAttributes
*/
public function __construct(
private readonly string $cacheDir,
private readonly iterable $mappedAttributes,
?MappingCacheGeneratorInterface $generator = null,
) {
$this->generator = $generator ?? new MappingCacheGenerator(new ReflectionObjectMapperMetadataFactory());
}

public function warmUp(string $cacheDir, ?string $buildDir = null): array
{
if (!$this->mappedAttributes) {
return [];
}

foreach ($this->mappedAttributes as ['source' => $sourceClass, 'target' => $targetClass]) {
$cacheFile = $this->getCacheFile($sourceClass, $targetClass);

if (is_file($cacheFile)) {
continue;
}

$this->writeCacheFile($cacheFile, $sourceClass, $targetClass);
}

return [];
}

public function isOptional(): bool
{
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ class UnusedTagsPass implements CompilerPassInterface
'mime.mime_type_guesser',
'monolog.logger',
'notifier.channel',
'object_mapper.condition_callable',
'object_mapper.transform_callable',
'property_info.access_extractor',
'property_info.constructor_extractor',
'property_info.initializable_extractor',
Expand Down Expand Up @@ -108,8 +110,6 @@ class UnusedTagsPass implements CompilerPassInterface
'validator.group_provider',
'validator.initializer',
'workflow',
'object_mapper.transform_callable',
'object_mapper.condition_callable',
];

public function process(ContainerBuilder $container): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Notifier\Notifier;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter;
Expand Down Expand Up @@ -194,6 +195,7 @@ public function getConfigTreeBuilder(): TreeBuilder
$this->addWebhookSection($rootNode, $enableIfStandalone);
$this->addRemoteEventSection($rootNode, $enableIfStandalone);
$this->addJsonStreamerSection($rootNode, $enableIfStandalone);
$this->addObjectMapperSection($rootNode, $enableIfStandalone);

return $treeBuilder;
}
Expand Down Expand Up @@ -2751,4 +2753,16 @@ private function addJsonStreamerSection(ArrayNodeDefinition $rootNode, callable
->end()
;
}

private function addObjectMapperSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone): void
{
$rootNode
->children()
->arrayNode('object_mapper')
->info('Object Mapper configuration')
->{$enableIfStandalone('symfony/object-mapper', ObjectMapperInterface::class)}()
->end()
->end()
;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
use Symfony\Component\Notifier\Recipient\Recipient;
use Symfony\Component\Notifier\TexterInterface;
use Symfony\Component\Notifier\Transport\TransportFactoryInterface as NotifierTransportFactoryInterface;
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\ObjectMapper\ConditionCallableInterface;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
use Symfony\Component\ObjectMapper\TransformCallableInterface;
Expand Down Expand Up @@ -612,12 +613,8 @@ public function load(array $configs, ContainerBuilder $container): void
$loader->load('mime_type.php');
}

if (ContainerBuilder::willBeAvailable('symfony/object-mapper', ObjectMapperInterface::class, ['symfony/framework-bundle'])) {
$loader->load('object_mapper.php');
$container->registerForAutoconfiguration(TransformCallableInterface::class)
->addTag('object_mapper.transform_callable');
$container->registerForAutoconfiguration(ConditionCallableInterface::class)
->addTag('object_mapper.condition_callable');
if ($this->readConfigEnabled('object_mapper', $container, $config['object_mapper'])) {
$this->registerObjectMapperConfiguration($container, $loader);
}

$container->registerForAutoconfiguration(PackageInterface::class)
Expand Down Expand Up @@ -3430,6 +3427,47 @@ private function registerHtmlSanitizerConfiguration(array $config, ContainerBuil
}
}

private function registerObjectMapperConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void
{
$loader->load('object_mapper.php');
$container->setParameter('.object_mapper.cache_dir', '%kernel.cache_dir%/object_mapper');
$container->registerForAutoconfiguration(TransformCallableInterface::class)
->addTag('object_mapper.transform_callable');
$container->registerForAutoconfiguration(ConditionCallableInterface::class)
->addTag('object_mapper.condition_callable');

if ($container->getParameter('kernel.debug')) {
$container->setAlias(ObjectMapperInterface::class, 'object_mapper');

return;
}

$container->registerAttributeForAutoconfiguration(Map::class, function (ChildDefinition $definition, Map $attribute, \ReflectionClass $reflector) {
$cl = $reflector->getName();
$source = $attribute->source ?? $cl;
$target = $attribute->target ?? $cl;

if ($source !== $target) {
$definition->addTag('object_mapper.attribute_metadata', [
'source' => $source,
'target' => $target,
]);
}
});

$container->setAlias(ObjectMapperInterface::class, 'object_mapper.cached');
}

public function getXsdValidationBasePath(): string|false
{
return \dirname(__DIR__).'/Resources/config/schema';
}

public function getNamespace(): string
{
return 'http://symfony.com/schema/dic/symfony';
}

protected function isConfigEnabled(ContainerBuilder $container, array $config): bool
{
throw new \LogicException('To prevent using outdated configuration, you must use the "readConfigEnabled" method instead.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,35 @@

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Bundle\FrameworkBundle\CacheWarmer\CachedObjectMapperCacheWarmer;
use Symfony\Component\ObjectMapper\CachedObjectMapper;
use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface;
use Symfony\Component\ObjectMapper\Metadata\ReflectionObjectMapperMetadataFactory;
use Symfony\Component\ObjectMapper\ObjectMapper;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;

return static function (ContainerConfigurator $container) {
$container->services()
->set('object_mapper.metadata_factory', ReflectionObjectMapperMetadataFactory::class)
->alias(ObjectMapperMetadataFactoryInterface::class, 'object_mapper.metadata_factory')

->set('object_mapper', ObjectMapper::class)
->args([
service('object_mapper.metadata_factory'),
service('property_accessor')->ignoreOnInvalid(),
tagged_locator('object_mapper.transform_callable'),
tagged_locator('object_mapper.condition_callable'),
])
->alias(ObjectMapperInterface::class, 'object_mapper')
->args([
service('object_mapper.metadata_factory'),
service('property_accessor')->ignoreOnInvalid(),
tagged_locator('object_mapper.transform_callable'),
tagged_locator('object_mapper.condition_callable'),
])

->set('object_mapper.cached', CachedObjectMapper::class)
->args([
param('.object_mapper.cache_dir'),
service('object_mapper.metadata_factory'),
service('property_accessor')->ignoreOnInvalid(),
tagged_locator('object_mapper.transform_callable'),
tagged_locator('object_mapper.condition_callable'),
])

->set('object_mapper.cached.cache_warmer', CachedObjectMapperCacheWarmer::class)
->args([null, 'object_mapper.cached'])
;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\FrameworkBundle\Tests\CacheWarmer;

use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\CacheWarmer\CachedObjectMapperCacheWarmer;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\ObjectMapper\Exception\MappingException;
use Symfony\Component\ObjectMapper\Tests\Fixtures\A;
use Symfony\Component\ObjectMapper\Tests\Fixtures\AbstractA;
use Symfony\Component\ObjectMapper\Tests\Fixtures\B;
use Symfony\Component\ObjectMapper\Tests\Fixtures\C;
use Symfony\Component\ObjectMapper\Tests\Fixtures\D;

class CachedObjectMapperCacheWarmerTest extends TestCase
{
private ?string $cacheDir = null;

protected function setUp(): void
{
$this->cacheDir = sys_get_temp_dir().'/symfony_object_mapper_'.uniqid();
(new Filesystem())->mkdir($this->cacheDir);
}

protected function tearDown(): void
{
if (null !== $this->cacheDir) {
(new Filesystem())->remove($this->cacheDir);
}
}

public function testWarmUp()
{
$mappingPairs = [
['source' => A::class, 'target' => B::class],
['source' => C::class, 'target' => D::class],
];

$warmer = new CachedObjectMapperCacheWarmer($this->cacheDir, $mappingPairs);
$warmer->warmUp($this->cacheDir);

$this->assertFileExists($this->cacheDir.'/'.hash('xxh128', A::class.'-to-'.B::class).'.php');
$this->assertFileExists($this->cacheDir.'/'.hash('xxh128', C::class.'-to-'.D::class).'.php');
}

public function testWarmUpWithNoPairs()
{
$warmer = new CachedObjectMapperCacheWarmer($this->cacheDir, []);
$warmer->warmUp($this->cacheDir);

$this->assertEmpty(glob($this->cacheDir.'/*.php'));
}

public function testWarmUpWithAbstractClass()
{
$this->expectException(MappingException::class);
$this->expectExceptionMessage('Can not generate mapping metadata from an abstract class "Symfony\Component\ObjectMapper\Tests\Fixtures\AbstractA".');
$mappingPairs = [
['source' => AbstractA::class, 'target' => B::class],
['source' => C::class, 'target' => D::class],
];

$warmer = new CachedObjectMapperCacheWarmer($this->cacheDir, $mappingPairs);
$warmer->warmUp($this->cacheDir);

$this->assertFileExists($this->cacheDir.'/'.hash('xxh128', C::class.'-to-'.D::class).'.php');
$this->assertFileDoesNotExist($this->cacheDir.'/'.hash('xxh128', AbstractA::class.'-to-'.B::class).'.php');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use Symfony\Component\Lock\Store\SemaphoreStore;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Notifier\Notifier;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter;
use Symfony\Component\RemoteEvent\RemoteEvent;
use Symfony\Component\Scheduler\Messenger\SchedulerTransportFactory;
Expand Down Expand Up @@ -986,6 +987,9 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
'json_streamer' => [
'enabled' => !class_exists(FullStack::class) && class_exists(JsonStreamWriter::class),
],
'object_mapper' => [
'enabled' => class_exists(ObjectMapperInterface::class),
],
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2811,9 +2811,11 @@ public function testJsonStreamerEnabled()
public function testObjectMapperEnabled()
{
$container = $this->createContainerFromClosure(function (ContainerBuilder $container) {
$container->loadFromExtension('framework', []);
$container->loadFromExtension('framework', ['object_mapper' => ['enabled' => true]]);
});
$this->assertTrue($container->has('object_mapper'));
$this->assertTrue($container->has('object_mapper.cached'));
$this->assertTrue($container->hasParameter('.object_mapper.cache_dir'));
}

protected function createContainer(array $data = [])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ObjectMapper\ObjectToBeMapped;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Antoine Bluchet <soyuka@gmail.com>
*/
class ObjectMapperTest extends AbstractWebTestCase
{
Expand All @@ -28,4 +28,14 @@ public function testObjectMapper()
$mapped = $objectMapper->map(new ObjectToBeMapped());
$this->assertSame($mapped->a, 'transformed');
}

public function testCachedObjectMapper()
{
static::bootKernel(['test_case' => 'ObjectMapper']);

/** @var Symfony\Component\ObjectMapper\ObjectMapperInterface<ObjectMapped> */
$objectMapper = static::getContainer()->get('object_mapper.cached.alias');
$mapped = $objectMapper->map(new ObjectToBeMapped());
$this->assertSame($mapped->a, 'transformed');
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
imports:
- { resource: ../config/default.yml }

framework:
object_mapper:
enabled: true
services:
object_mapper.alias:
alias: object_mapper
public: true
Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ObjectMapper\TransformCallable:
autoconfigure: true
object_mapper.cached.alias:
alias: object_mapper.cached
public: true
Loading
Loading