diff --git a/Compiler/AbstractRecursivePass.php b/Compiler/AbstractRecursivePass.php index f18baa57c..1b744d611 100644 --- a/Compiler/AbstractRecursivePass.php +++ b/Compiler/AbstractRecursivePass.php @@ -82,7 +82,7 @@ protected function processValue(mixed $value, bool $isRoot = false) continue; } if ($isRoot) { - if ($v->hasTag('container.excluded')) { + if ($v instanceof Definition && $v->hasTag('container.excluded')) { continue; } $this->currentId = $k; diff --git a/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php b/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php index 7a6dd7687..bbf318982 100644 --- a/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php +++ b/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php @@ -60,15 +60,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed if (isset($this->serviceLocatorContextIds[$currentId])) { $currentId = $this->serviceLocatorContextIds[$currentId]; $locator = $this->container->getDefinition($this->currentId)->getFactory()[0]; - - foreach ($locator->getArgument(0) as $k => $v) { - if ($v->getValues()[0] === $value) { - if ($k !== $id) { - $currentId = $k.'" in the container provided to "'.$currentId; - } - throw new ServiceNotFoundException($id, $currentId, null, $this->getAlternatives($id)); - } - } + $this->throwServiceNotFoundException($value, $currentId, $locator->getArgument(0)); } if ('.' === $currentId[0] && $graph->hasNode($currentId)) { @@ -82,14 +74,21 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed $currentId = $sourceId; break; } + + if (isset($this->serviceLocatorContextIds[$sourceId])) { + $currentId = $this->serviceLocatorContextIds[$sourceId]; + $locator = $this->container->getDefinition($this->currentId); + $this->throwServiceNotFoundException($value, $currentId, $locator->getArgument(0)); + } } } - throw new ServiceNotFoundException($id, $currentId, null, $this->getAlternatives($id)); + $this->throwServiceNotFoundException($value, $currentId, $value); } - private function getAlternatives(string $id): array + private function throwServiceNotFoundException(Reference $ref, string $sourceId, $value): void { + $id = (string) $ref; $alternatives = []; foreach ($this->container->getServiceIds() as $knownId) { if ('' === $knownId || '.' === $knownId[0] || $knownId === $this->currentId) { @@ -102,6 +101,28 @@ private function getAlternatives(string $id): array } } - return $alternatives; + $pass = new class() extends AbstractRecursivePass { + public Reference $ref; + public string $sourceId; + public array $alternatives; + + public function processValue(mixed $value, bool $isRoot = false): mixed + { + if ($this->ref !== $value) { + return parent::processValue($value, $isRoot); + } + $sourceId = $this->sourceId; + if (null !== $this->currentId && $this->currentId !== (string) $value) { + $sourceId = $this->currentId.'" in the container provided to "'.$sourceId; + } + + throw new ServiceNotFoundException((string) $value, $sourceId, null, $this->alternatives); + } + }; + $pass->ref = $ref; + $pass->sourceId = $sourceId; + $pass->alternatives = $alternatives; + + $pass->processValue($value, true); } } diff --git a/Dumper/PhpDumper.php b/Dumper/PhpDumper.php index 81b500c5a..23e65483b 100644 --- a/Dumper/PhpDumper.php +++ b/Dumper/PhpDumper.php @@ -624,7 +624,9 @@ private function generateProxyClasses(): array $proxyCode = substr(self::stripComments($proxyCode), 5); } - $proxyClass = explode(' ', $this->inlineRequires ? substr($proxyCode, \strlen($code)) : $proxyCode, 3)[1]; + $proxyClass = $this->inlineRequires ? substr($proxyCode, \strlen($code)) : $proxyCode; + $i = strpos($proxyClass, 'class'); + $proxyClass = substr($proxyClass, 6 + $i, strpos($proxyClass, ' ', 7 + $i) - $i - 6); if ($this->asFiles || $this->namespace) { $proxyCode .= "\nif (!\\class_exists('$proxyClass', false)) {\n \\class_alias(__NAMESPACE__.'\\\\$proxyClass', '$proxyClass', false);\n}\n"; diff --git a/LazyProxy/PhpDumper/LazyServiceDumper.php b/LazyProxy/PhpDumper/LazyServiceDumper.php index b1df5f0f0..0d4426c44 100644 --- a/LazyProxy/PhpDumper/LazyServiceDumper.php +++ b/LazyProxy/PhpDumper/LazyServiceDumper.php @@ -105,7 +105,7 @@ public function getProxyCode(Definition $definition, ?string $id = null): string if ($asGhostObject) { try { - return 'class '.$proxyClass.ProxyHelper::generateLazyGhost($class); + return (\PHP_VERSION_ID >= 80200 && $class?->isReadOnly() ? 'readonly ' : '').'class '.$proxyClass.ProxyHelper::generateLazyGhost($class); } catch (LogicException $e) { throw new InvalidArgumentException(sprintf('Cannot generate lazy ghost for service "%s".', $id ?? $definition->getClass()), 0, $e); } diff --git a/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php b/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php index bce810364..04a121d63 100644 --- a/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php +++ b/Tests/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php @@ -84,14 +84,20 @@ public function testProcessDefinitionWithBindings() $this->addToAssertionCount(1); } - public function testWithErroredServiceLocator() + /** + * @testWith [true] + * [false] + */ + public function testWithErroredServiceLocator(bool $inline) { $container = new ContainerBuilder(); ServiceLocatorTagPass::register($container, ['foo' => new Reference('baz')], 'bar'); (new AnalyzeServiceReferencesPass())->process($container); - (new InlineServiceDefinitionsPass())->process($container); + if ($inline) { + (new InlineServiceDefinitionsPass())->process($container); + } $this->expectException(ServiceNotFoundException::class); $this->expectExceptionMessage('The service "foo" in the container provided to "bar" has a dependency on a non-existent service "baz".'); @@ -99,14 +105,20 @@ public function testWithErroredServiceLocator() $this->process($container); } - public function testWithErroredHiddenService() + /** + * @testWith [true] + * [false] + */ + public function testWithErroredHiddenService(bool $inline) { $container = new ContainerBuilder(); ServiceLocatorTagPass::register($container, ['foo' => new Reference('foo')], 'bar'); (new AnalyzeServiceReferencesPass())->process($container); - (new InlineServiceDefinitionsPass())->process($container); + if ($inline) { + (new InlineServiceDefinitionsPass())->process($container); + } $this->expectException(ServiceNotFoundException::class); $this->expectExceptionMessage('The service "bar" has a dependency on a non-existent service "foo".'); diff --git a/Tests/Dumper/YamlDumperTest.php b/Tests/Dumper/YamlDumperTest.php index 68931050b..f9ff3fff7 100644 --- a/Tests/Dumper/YamlDumperTest.php +++ b/Tests/Dumper/YamlDumperTest.php @@ -163,7 +163,11 @@ public function testDumpHandlesEnumeration() $container->compile(); $dumper = new YamlDumper($container); - $this->assertEquals(file_get_contents(self::$fixturesPath.'/yaml/services_with_enumeration.yml'), $dumper->dump()); + if (str_starts_with(Yaml::dump(FooUnitEnum::BAR), '!php/enum')) { + $this->assertEquals(file_get_contents(self::$fixturesPath.'/yaml/services_with_enumeration_enum_tag.yml'), $dumper->dump()); + } else { + $this->assertEquals(file_get_contents(self::$fixturesPath.'/yaml/services_with_enumeration.yml'), $dumper->dump()); + } } /** diff --git a/Tests/Fixtures/ReadOnlyClass.php b/Tests/Fixtures/ReadOnlyClass.php new file mode 100644 index 000000000..b247ea216 --- /dev/null +++ b/Tests/Fixtures/ReadOnlyClass.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +readonly class ReadOnlyClass +{ + public function say(): string + { + return 'hello'; + } +} diff --git a/Tests/Fixtures/yaml/services_with_enumeration_enum_tag.yml b/Tests/Fixtures/yaml/services_with_enumeration_enum_tag.yml new file mode 100644 index 000000000..c34ce4f8e --- /dev/null +++ b/Tests/Fixtures/yaml/services_with_enumeration_enum_tag.yml @@ -0,0 +1,13 @@ +parameters: + unit_enum: !php/enum Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum::BAR + enum_array: [!php/enum Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum::BAR, !php/enum Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum::FOO] + +services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + public: true + synthetic: true + Symfony\Component\DependencyInjection\Tests\Fixtures\FooClassWithEnumAttribute: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\FooClassWithEnumAttribute + public: true + arguments: [!php/const 'Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum::BAR'] diff --git a/Tests/LazyProxy/PhpDumper/LazyServiceDumperTest.php b/Tests/LazyProxy/PhpDumper/LazyServiceDumperTest.php index 064bfc3cc..467972a88 100644 --- a/Tests/LazyProxy/PhpDumper/LazyServiceDumperTest.php +++ b/Tests/LazyProxy/PhpDumper/LazyServiceDumperTest.php @@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\LazyServiceDumper; +use Symfony\Component\DependencyInjection\Tests\Fixtures\ReadOnlyClass; class LazyServiceDumperTest extends TestCase { @@ -52,6 +53,18 @@ public function testInvalidClass() $this->expectExceptionMessage('Invalid "proxy" tag for service "stdClass": class "stdClass" doesn\'t implement "Psr\Container\ContainerInterface".'); $dumper->getProxyCode($definition); } + + /** + * @requires PHP 8.3 + */ + public function testReadonlyClass() + { + $dumper = new LazyServiceDumper(); + $definition = (new Definition(ReadOnlyClass::class))->setLazy(true); + + $this->assertTrue($dumper->isProxyCandidate($definition)); + $this->assertStringContainsString('readonly class ReadOnlyClassGhost', $dumper->getProxyCode($definition)); + } } final class TestContainer implements ContainerInterface