Skip to content

Commit dd1a058

Browse files
fix(symfony): register property_info fallback when not provided by Symfony
When symfony/framework-bundle has property_info disabled (the default on full-stack Symfony unless explicitly opted-in), the prependExtensionConfig fallback in ApiPlatformExtension::prepend() can be overridden by user configuration, resulting in "service not found" errors at container compile time. Add a PropertyInfoPass compiler pass that registers a minimal PropertyInfoExtractor backed by ReflectionExtractor when the property_info service is absent, ensuring API Platform never requires it to be explicitly enabled. Fixes #7876 Signed-off-by: Guillaume Delré <delre.guillaume@gmail.com>
1 parent 1d7695d commit dd1a058

3 files changed

Lines changed: 140 additions & 0 deletions

File tree

src/Symfony/Bundle/ApiPlatformBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass;
2424
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass;
2525
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MutatorPass;
26+
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\PropertyInfoPass;
2627
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass;
2728
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass;
2829
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass;
@@ -57,6 +58,7 @@ public function build(ContainerBuilder $container): void
5758
$container->addCompilerPass(new TestClientPass());
5859
$container->addCompilerPass(new TestMercureHubPass());
5960
$container->addCompilerPass(new AuthenticatorManagerPass());
61+
$container->addCompilerPass(new PropertyInfoPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 200);
6062
$container->addCompilerPass(new SerializerMappingLoaderPass());
6163
$container->addCompilerPass(new MutatorPass());
6264
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.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+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler;
15+
16+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
17+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
18+
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
use Symfony\Component\DependencyInjection\Definition;
20+
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
21+
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
22+
23+
/**
24+
* Ensures the property_info service is always available.
25+
*
26+
* When symfony/framework-bundle has property_info disabled (which is the default
27+
* on a full-stack Symfony application unless explicitly opted-in), API Platform's
28+
* prependExtensionConfig() fallback can be overridden by user configuration.
29+
* This pass registers a minimal fallback so API Platform never fails with
30+
* "service not found" for property_info.
31+
*
32+
* @internal
33+
*/
34+
final class PropertyInfoPass implements CompilerPassInterface
35+
{
36+
public function process(ContainerBuilder $container): void
37+
{
38+
if ($container->hasDefinition('property_info') || $container->hasAlias('property_info')) {
39+
return;
40+
}
41+
42+
if (!$container->hasDefinition('property_info.reflection_extractor')) {
43+
$reflectionExtractor = new Definition(ReflectionExtractor::class);
44+
$reflectionExtractor->addTag('property_info.list_extractor', ['priority' => -1000]);
45+
$reflectionExtractor->addTag('property_info.type_extractor', ['priority' => -1002]);
46+
$reflectionExtractor->addTag('property_info.access_extractor', ['priority' => -1000]);
47+
$reflectionExtractor->addTag('property_info.initializable_extractor', ['priority' => -1000]);
48+
$container->setDefinition('property_info.reflection_extractor', $reflectionExtractor);
49+
}
50+
51+
$definition = new Definition(PropertyInfoExtractor::class);
52+
$definition->setArguments([
53+
new TaggedIteratorArgument('property_info.list_extractor'),
54+
new TaggedIteratorArgument('property_info.type_extractor'),
55+
new TaggedIteratorArgument('property_info.description_extractor'),
56+
new TaggedIteratorArgument('property_info.access_extractor'),
57+
new TaggedIteratorArgument('property_info.initializable_extractor'),
58+
]);
59+
$container->setDefinition('property_info', $definition);
60+
}
61+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.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+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Symfony\Tests\Bundle\DependencyInjection\Compiler;
15+
16+
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\PropertyInfoPass;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
20+
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
21+
22+
class PropertyInfoPassTest extends TestCase
23+
{
24+
public function testRegistersPropertyInfoFallbackWhenMissing(): void
25+
{
26+
$container = new ContainerBuilder();
27+
28+
(new PropertyInfoPass())->process($container);
29+
30+
$this->assertTrue($container->hasDefinition('property_info'));
31+
$this->assertTrue($container->hasDefinition('property_info.reflection_extractor'));
32+
33+
$definition = $container->getDefinition('property_info');
34+
$this->assertSame(PropertyInfoExtractor::class, $definition->getClass());
35+
36+
$reflectionDef = $container->getDefinition('property_info.reflection_extractor');
37+
$this->assertSame(ReflectionExtractor::class, $reflectionDef->getClass());
38+
$this->assertArrayHasKey('property_info.list_extractor', $reflectionDef->getTags());
39+
$this->assertArrayHasKey('property_info.type_extractor', $reflectionDef->getTags());
40+
$this->assertArrayHasKey('property_info.access_extractor', $reflectionDef->getTags());
41+
$this->assertArrayHasKey('property_info.initializable_extractor', $reflectionDef->getTags());
42+
}
43+
44+
public function testSkipsWhenPropertyInfoDefinitionExists(): void
45+
{
46+
$container = new ContainerBuilder();
47+
$container->register('property_info', PropertyInfoExtractor::class);
48+
49+
(new PropertyInfoPass())->process($container);
50+
51+
$this->assertFalse($container->hasDefinition('property_info.reflection_extractor'));
52+
}
53+
54+
public function testSkipsWhenPropertyInfoAliasExists(): void
55+
{
56+
$container = new ContainerBuilder();
57+
$container->register('some_property_info', PropertyInfoExtractor::class);
58+
$container->setAlias('property_info', 'some_property_info');
59+
60+
(new PropertyInfoPass())->process($container);
61+
62+
$this->assertFalse($container->hasDefinition('property_info.reflection_extractor'));
63+
}
64+
65+
public function testDoesNotRegisterReflectionExtractorIfAlreadyPresent(): void
66+
{
67+
$container = new ContainerBuilder();
68+
$container->register('property_info.reflection_extractor', ReflectionExtractor::class);
69+
70+
(new PropertyInfoPass())->process($container);
71+
72+
$this->assertTrue($container->hasDefinition('property_info'));
73+
$existingDef = $container->getDefinition('property_info.reflection_extractor');
74+
$this->assertSame(ReflectionExtractor::class, $existingDef->getClass());
75+
$this->assertEmpty($existingDef->getTags());
76+
}
77+
}

0 commit comments

Comments
 (0)