From 830f28646402670d9df809cbe2de5545766fa3aa Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 22 Sep 2017 11:57:45 +0100 Subject: [PATCH 01/13] [DI] Dont use JSON_BIGINT_AS_STRING --- src/Symfony/Component/DependencyInjection/EnvVarProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php index 51a19367e2..73514712e8 100644 --- a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php +++ b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php @@ -119,7 +119,7 @@ class EnvVarProcessor implements EnvVarProcessorInterface } if ('json' === $prefix) { - $env = json_decode($env, true, JSON_BIGINT_AS_STRING); + $env = json_decode($env, true); if (JSON_ERROR_NONE !== json_last_error()) { throw new RuntimeException(sprintf('Invalid JSON in env var "%s": '.json_last_error_msg(), $name)); From cb935e789e397d3134f7d32be0bb58324d5a5405 Mon Sep 17 00:00:00 2001 From: Maarten de Boer Date: Thu, 21 Sep 2017 14:46:00 +0200 Subject: [PATCH 02/13] [Serializer] Getter for extra attributes in ExtraAttributesException --- src/Symfony/Component/Serializer/CHANGELOG.md | 1 + .../Exception/ExtraAttributesException.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index ef59f22499..9e48c5c41e 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * added `AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT` context option to disable throwing an `UnexpectedValueException` on a type mismatch * added support for serializing `DateInterval` objects + * added getter for extra attributes in `ExtraAttributesException` 3.3.0 ----- diff --git a/src/Symfony/Component/Serializer/Exception/ExtraAttributesException.php b/src/Symfony/Component/Serializer/Exception/ExtraAttributesException.php index d321618b8e..74d87f87f5 100644 --- a/src/Symfony/Component/Serializer/Exception/ExtraAttributesException.php +++ b/src/Symfony/Component/Serializer/Exception/ExtraAttributesException.php @@ -18,10 +18,24 @@ namespace Symfony\Component\Serializer\Exception; */ class ExtraAttributesException extends RuntimeException { + private $extraAttributes; + public function __construct(array $extraAttributes, \Exception $previous = null) { $msg = sprintf('Extra attributes are not allowed ("%s" are unknown).', implode('", "', $extraAttributes)); + $this->extraAttributes = $extraAttributes; + parent::__construct($msg, 0, $previous); } + + /** + * Get the extra attributes that are not allowed. + * + * @return array + */ + public function getExtraAttributes() + { + return $this->extraAttributes; + } } From e46b366fc52eb4c2bee683797b8569dc4186c9a9 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Fri, 22 Sep 2017 20:41:17 +0200 Subject: [PATCH 03/13] Reset the authentication token between requests. --- .../Bundle/SecurityBundle/Resources/config/security.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 79a4866d8c..049d67f5a0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -21,7 +21,9 @@ - + + + From 35f9c0ba2d26cb21eca8f75f135a35052ca22ef3 Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Fri, 22 Sep 2017 22:13:43 +0200 Subject: [PATCH 04/13] [Form] Add ambiguous & exception debug:form tests --- .../Component/Form/Command/DebugCommand.php | 2 +- .../Form/Tests/Command/DebugCommandTest.php | 99 +++++++++++++------ .../Tests/Fixtures/Debug/A/AmbiguousType.php | 18 ++++ .../Tests/Fixtures/Debug/B/AmbiguousType.php | 18 ++++ 4 files changed, 104 insertions(+), 33 deletions(-) create mode 100644 src/Symfony/Component/Form/Tests/Fixtures/Debug/A/AmbiguousType.php create mode 100644 src/Symfony/Component/Form/Tests/Fixtures/Debug/B/AmbiguousType.php diff --git a/src/Symfony/Component/Form/Command/DebugCommand.php b/src/Symfony/Component/Form/Command/DebugCommand.php index d9b3eee838..d8e1f5a078 100644 --- a/src/Symfony/Component/Form/Command/DebugCommand.php +++ b/src/Symfony/Component/Form/Command/DebugCommand.php @@ -118,7 +118,7 @@ EOF return $classes[0]; } if (!$input->isInteractive()) { - throw new InvalidArgumentException(sprintf("The type \"%s\" is ambiguous.\nDid you mean one of these?\n %s", $shortClassName, implode("\n ", $classes))); + throw new InvalidArgumentException(sprintf("The type \"%s\" is ambiguous.\n\nDid you mean one of these?\n %s", $shortClassName, implode("\n ", $classes))); } return $io->choice(sprintf("The type \"%s\" is ambiguous.\n\n Select one of the following form types to display its information:", $shortClassName), $classes, $classes[0]); diff --git a/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php index 610f6197d6..d24af0a4b1 100644 --- a/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php @@ -13,11 +13,11 @@ namespace Symfony\Component\Form\Tests\Command; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; +use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Form\Command\DebugCommand; -use Symfony\Component\Form\Extension\Core\Type\FormType; -use Symfony\Component\Form\FormRegistryInterface; -use Symfony\Component\Form\ResolvedFormTypeInterface; +use Symfony\Component\Form\FormRegistry; +use Symfony\Component\Form\ResolvedFormTypeFactory; class DebugCommandTest extends TestCase { @@ -39,6 +39,67 @@ class DebugCommandTest extends TestCase $this->assertContains('Symfony\Component\Form\Extension\Core\Type\FormType (Block prefix: "form")', $tester->getDisplay()); } + /** + * @expectedException \Symfony\Component\Console\Exception\InvalidArgumentException + * @expectedExceptionMessage Could not find type "NonExistentType" + */ + public function testDebugSingleFormTypeNotFound() + { + $tester = $this->createCommandTester(); + $tester->execute(array('class' => 'NonExistentType'), array('decorated' => false, 'interactive' => false)); + } + + public function testDebugAmbiguousFormType() + { + $expectedMessage = <<expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($expectedMessage); + } else { + $this->setExpectedException(InvalidArgumentException::class, $expectedMessage); + } + + $tester = $this->createCommandTester(array( + 'Symfony\Component\Form\Tests\Fixtures\Debug\A', + 'Symfony\Component\Form\Tests\Fixtures\Debug\B', + )); + + $tester->execute(array('class' => 'AmbiguousType'), array('decorated' => false, 'interactive' => false)); + } + + public function testDebugAmbiguousFormTypeInteractive() + { + $tester = $this->createCommandTester(array( + 'Symfony\Component\Form\Tests\Fixtures\Debug\A', + 'Symfony\Component\Form\Tests\Fixtures\Debug\B', + )); + + $tester->setInputs(array(0)); + $tester->execute(array('class' => 'AmbiguousType'), array('decorated' => false, 'interactive' => true)); + + $this->assertEquals(0, $tester->getStatusCode(), 'Returns 0 in case of success'); + $output = $tester->getDisplay(true); + $this->assertStringMatchesFormat(<<createCommandTester()->execute(array('class' => 'test')); } - /** - * @return CommandTester - */ - private function createCommandTester() + private function createCommandTester(array $namespaces = null) { - $resolvedFormType = $this->getMockBuilder(ResolvedFormTypeInterface::class)->getMock(); - $resolvedFormType - ->expects($this->any()) - ->method('getParent') - ->willReturn(null) - ; - $resolvedFormType - ->expects($this->any()) - ->method('getInnerType') - ->willReturn(new FormType()) - ; - $resolvedFormType - ->expects($this->any()) - ->method('getTypeExtensions') - ->willReturn(array()) - ; - - $formRegistry = $this->getMockBuilder(FormRegistryInterface::class)->getMock(); - $formRegistry - ->expects($this->any()) - ->method('getType') - ->will($this->returnValue($resolvedFormType)) - ; - - $command = new DebugCommand($formRegistry); + $formRegistry = new FormRegistry(array(), new ResolvedFormTypeFactory()); + $command = null === $namespaces ? new DebugCommand($formRegistry) : new DebugCommand($formRegistry, $namespaces); $application = new Application(); $application->add($command); diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Debug/A/AmbiguousType.php b/src/Symfony/Component/Form/Tests/Fixtures/Debug/A/AmbiguousType.php new file mode 100644 index 0000000000..e6e1839028 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/Debug/A/AmbiguousType.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Fixtures\Debug\A; + +use Symfony\Component\Form\AbstractType; + +class AmbiguousType extends AbstractType +{ +} diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Debug/B/AmbiguousType.php b/src/Symfony/Component/Form/Tests/Fixtures/Debug/B/AmbiguousType.php new file mode 100644 index 0000000000..c670e8e925 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/Debug/B/AmbiguousType.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Fixtures\Debug\B; + +use Symfony\Component\Form\AbstractType; + +class AmbiguousType extends AbstractType +{ +} From 42f0984e71fa38c72bf95ff58264274456e3f115 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Thu, 21 Sep 2017 18:42:43 +0200 Subject: [PATCH 05/13] [VarDumper] Make `dump()` a little bit more easier to use --- .../Component/VarDumper/Resources/functions/dump.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Symfony/Component/VarDumper/Resources/functions/dump.php b/src/Symfony/Component/VarDumper/Resources/functions/dump.php index b6c243c8b6..0e5216abc2 100644 --- a/src/Symfony/Component/VarDumper/Resources/functions/dump.php +++ b/src/Symfony/Component/VarDumper/Resources/functions/dump.php @@ -20,5 +20,11 @@ if (!function_exists('dump')) { foreach (func_get_args() as $var) { VarDumper::dump($var); } + + if ($moreVars) { + return func_get_args(); + } + + return $var; } } From cb6ead1635a13875a486814b5314a1d4feb08c01 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 24 Sep 2017 10:13:45 +0100 Subject: [PATCH 06/13] allow forms without translations and validator --- .../DependencyInjection/FrameworkExtension.php | 18 ++++++++++++------ .../Resources/config/form_csrf.xml | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index e0d7258902..4a940a70f0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -74,6 +74,7 @@ use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand; +use Symfony\Component\Translation\Translator; use Symfony\Component\Validator\ConstraintValidatorInterface; use Symfony\Component\Validator\ObjectInitializerInterface; use Symfony\Component\WebLink\HttpHeaderSerializer; @@ -189,15 +190,13 @@ class FrameworkExtension extends Extension throw new LogicException('Translation support cannot be enabled as the Translation component is not installed.'); } - if (!class_exists('Symfony\Component\Translation\Translator') && $this->isConfigEnabled($container, $config['form'])) { - throw new LogicException('Form support cannot be enabled as the Translation component is not installed.'); - } - if (!class_exists('Symfony\Component\Translation\Translator') && $this->isConfigEnabled($container, $config['validation'])) { throw new LogicException('Validation support cannot be enabled as the Translation component is not installed.'); } - $loader->load('identity_translator.xml'); + if (class_exists(Translator::class)) { + $loader->load('identity_translator.xml'); + } } if (isset($config['secret'])) { @@ -250,7 +249,10 @@ class FrameworkExtension extends Extension $config['validation']['enabled'] = true; if (!class_exists('Symfony\Component\Validator\Validation')) { - throw new LogicException('The Validator component is required to use the Form component.'); + $container->setParameter('validator.translation_domain', 'validators'); + + $container->removeDefinition('form.type_extension.form.validator'); + $container->removeDefinition('form.type_guesser.validator'); } } else { $container->removeDefinition('Symfony\Component\Form\Command\DebugCommand'); @@ -448,6 +450,10 @@ class FrameworkExtension extends Extension } else { $container->setParameter('form.type_extension.csrf.enabled', false); } + + if (!class_exists(Translator::class)) { + $container->removeDefinition('form.type_extension.upload.validator'); + } } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml index 22b0c119e2..d0162ea098 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml @@ -12,7 +12,7 @@ %form.type_extension.csrf.enabled% %form.type_extension.csrf.field_name% - + %validator.translation_domain% From e17426c65cda22b71d19f4a6ed918003f9eda587 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 22 Sep 2017 13:07:23 +0200 Subject: [PATCH 07/13] added missing @author tag for new class --- src/Symfony/Component/DependencyInjection/EnvVarProcessor.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php index 73514712e8..63bb5bc8c0 100644 --- a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php +++ b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php @@ -15,6 +15,9 @@ use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; +/** + * @author Nicolas Grekas + */ class EnvVarProcessor implements EnvVarProcessorInterface { private $container; From 88549fff5b4eb4c2739562d666ebfaf12aca56ba Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Sat, 23 Sep 2017 12:08:51 +0200 Subject: [PATCH 08/13] [DI][DX] Throw exception on some ContainerBuilder methods used from extensions --- .../MergeExtensionConfigurationPass.php | 46 ++++++++++++++++++- .../MergeExtensionConfigurationPassTest.php | 17 +++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/DependencyInjection/Compiler/MergeExtensionConfigurationPass.php b/src/Symfony/Component/DependencyInjection/Compiler/MergeExtensionConfigurationPass.php index c3b4d78dd6..af3a6baf0b 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/MergeExtensionConfigurationPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/MergeExtensionConfigurationPass.php @@ -12,10 +12,13 @@ namespace Symfony\Component\DependencyInjection\Compiler; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface; use Symfony\Component\DependencyInjection\Extension\Extension; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; /** * Merges extension configs into the container builder. @@ -52,7 +55,7 @@ class MergeExtensionConfigurationPass implements CompilerPassInterface } $config = $resolvingBag->resolveValue($config); - $tmpContainer = new ContainerBuilder($resolvingBag); + $tmpContainer = new MergeExtensionConfigurationContainerBuilder($extension, $resolvingBag); $tmpContainer->setResourceTracking($container->isTrackingResources()); $tmpContainer->addObjectResource($extension); if ($extension instanceof ConfigurationExtensionInterface && null !== $configuration = $extension->getConfiguration($config, $tmpContainer)) { @@ -121,3 +124,44 @@ class MergeExtensionConfigurationParameterBag extends EnvPlaceholderParameterBag return null !== $this->processedEnvPlaceholders ? $this->processedEnvPlaceholders : parent::getEnvPlaceholders(); } } + +/** + * A container builder preventing using methods that wouldn't have any effect from extensions. + * + * @internal + */ +class MergeExtensionConfigurationContainerBuilder extends ContainerBuilder +{ + private $extensionClass; + + public function __construct(ExtensionInterface $extension, ParameterBagInterface $parameterBag = null) + { + parent::__construct($parameterBag); + + $this->extensionClass = get_class($extension); + } + + /** + * {@inheritdoc} + */ + public function addCompilerPass(CompilerPassInterface $pass, $type = PassConfig::TYPE_BEFORE_OPTIMIZATION/*, int $priority = 0*/) + { + throw new LogicException(sprintf('You cannot add compiler pass "%s" from extension "%s". Compiler passes must be registered before the container is compiled.', get_class($pass), $this->extensionClass)); + } + + /** + * {@inheritdoc} + */ + public function registerExtension(ExtensionInterface $extension) + { + throw new LogicException(sprintf('You cannot register extension "%s" from "%s". Extensions must be registered before the container is compiled.', get_class($extension), $this->extensionClass)); + } + + /** + * {@inheritdoc} + */ + public function compile($resolveEnvPlaceholders = false) + { + throw new LogicException(sprintf('Cannot compile the container in extension "%s".', $this->extensionClass)); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/MergeExtensionConfigurationPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/MergeExtensionConfigurationPassTest.php index 44d1933a80..57bb634f4b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/MergeExtensionConfigurationPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/MergeExtensionConfigurationPassTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\DependencyInjection\Compiler\MergeExtensionConfigurationPass; +use Symfony\Component\DependencyInjection\Compiler\MergeExtensionConfigurationContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; @@ -54,6 +55,22 @@ class MergeExtensionConfigurationPassTest extends TestCase $this->assertEquals(array($provider), $tmpProviders); } + public function testExtensionLoadGetAMergeExtensionConfigurationContainerBuilderInstance() + { + $extension = $this->getMockBuilder(FooExtension::class)->setMethods(array('load'))->getMock(); + $extension->expects($this->once()) + ->method('load') + ->with($this->isType('array'), $this->isInstanceOf(MergeExtensionConfigurationContainerBuilder::class)) + ; + + $container = new ContainerBuilder(new ParameterBag()); + $container->registerExtension($extension); + $container->prependExtensionConfig('foo', array()); + + $pass = new MergeExtensionConfigurationPass(); + $pass->process($container); + } + public function testExtensionConfigurationIsTrackedByDefault() { $extension = $this->getMockBuilder(FooExtension::class)->setMethods(array('getConfiguration'))->getMock(); From 14c91f2bc98978c400eef484cdf7d274b1aa002c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 15 Sep 2017 22:03:33 +0200 Subject: [PATCH 09/13] [Cache] Add ResettableInterface to allow resetting any pool's local state --- .../Compiler/CachePoolPass.php | 11 +++- .../FrameworkExtension.php | 3 ++ .../Resources/config/cache.xml | 4 +- .../Cache/Adapter/AbstractAdapter.php | 3 +- .../Component/Cache/Adapter/ArrayAdapter.php | 3 +- .../Component/Cache/Adapter/ChainAdapter.php | 15 +++++- .../Cache/Adapter/PhpArrayAdapter.php | 22 ++++---- .../Component/Cache/Adapter/ProxyAdapter.php | 8 ++- .../Cache/Adapter/SimpleCacheAdapter.php | 8 ++- .../Cache/Adapter/TagAwareAdapter.php | 51 ++++++++----------- .../Cache/Adapter/TraceableAdapter.php | 36 ++++++++++++- .../Component/Cache/DoctrineProvider.php | 21 +++++++- .../Component/Cache/ResettableInterface.php | 20 ++++++++ .../Component/Cache/Simple/AbstractCache.php | 3 +- .../Component/Cache/Simple/ArrayCache.php | 3 +- .../Component/Cache/Simple/ChainCache.php | 15 +++++- .../Component/Cache/Simple/PhpArrayCache.php | 20 ++++---- .../Component/Cache/Simple/Psr6Cache.php | 8 ++- .../Component/Cache/Simple/TraceableCache.php | 36 ++++++++++++- .../Tests/Adapter/PhpArrayAdapterTest.php | 1 + .../PhpArrayAdapterWithFallbackTest.php | 1 + .../Cache/Tests/Adapter/ProxyAdapterTest.php | 1 + .../Tests/Adapter/SimpleCacheAdapterTest.php | 4 ++ .../Tests/Adapter/TraceableAdapterTest.php | 4 ++ .../Cache/Tests/Simple/PhpArrayCacheTest.php | 1 + .../Simple/PhpArrayCacheWithFallbackTest.php | 1 + .../Cache/Tests/Simple/Psr6CacheTest.php | 4 ++ .../Cache/Tests/Simple/TraceableCacheTest.php | 4 ++ .../Component/Cache/Traits/AbstractTrait.php | 11 ++++ .../Component/Cache/Traits/ArrayTrait.php | 8 +++ .../Component/Cache/Traits/DoctrineTrait.php | 9 ++++ .../Component/Cache/Traits/PhpArrayTrait.php | 5 +- .../Component/Cache/Traits/ProxyTrait.php | 41 +++++++++++++++ 33 files changed, 315 insertions(+), 70 deletions(-) create mode 100644 src/Symfony/Component/Cache/ResettableInterface.php create mode 100644 src/Symfony/Component/Cache/Traits/ProxyTrait.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php index 6a633c7b25..dafe0b7b8f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php @@ -43,6 +43,7 @@ class CachePoolPass implements CompilerPassInterface 'provider', 'namespace', 'default_lifetime', + 'reset', ); foreach ($container->findTaggedServiceIds('cache.pool') as $id => $tags) { $adapter = $pool = $container->getDefinition($id); @@ -73,13 +74,19 @@ class CachePoolPass implements CompilerPassInterface } $i = 0; foreach ($attributes as $attr) { - if (isset($tags[0][$attr]) && ('namespace' !== $attr || ArrayAdapter::class !== $adapter->getClass())) { + if (!isset($tags[0][$attr])) { + // no-op + } elseif ('reset' === $attr) { + if ($tags[0][$attr]) { + $pool->addTag('kernel.reset', array('method' => $tags[0][$attr])); + } + } elseif ('namespace' !== $attr || ArrayAdapter::class !== $adapter->getClass()) { $pool->replaceArgument($i++, $tags[0][$attr]); } unset($tags[0][$attr]); } if (!empty($tags[0])) { - throw new InvalidArgumentException(sprintf('Invalid "cache.pool" tag for service "%s": accepted attributes are "clearer", "provider", "namespace" and "default_lifetime", found "%s".', $id, implode('", "', array_keys($tags[0])))); + throw new InvalidArgumentException(sprintf('Invalid "cache.pool" tag for service "%s": accepted attributes are "clearer", "provider", "namespace", "default_lifetime" and "reset", found "%s".', $id, implode('", "', array_keys($tags[0])))); } if (null !== $clearer) { diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 489bfed43c..69d8fa64aa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -25,6 +25,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Routing\AnnotatedRouteControllerLoader; use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\ResettableInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Config\Resource\DirectoryResource; @@ -336,6 +337,8 @@ class FrameworkExtension extends Extension ->addTag('kernel.cache_warmer'); $container->registerForAutoconfiguration(EventSubscriberInterface::class) ->addTag('kernel.event_subscriber'); + $container->registerForAutoconfiguration(ResettableInterface::class) + ->addTag('kernel.reset', array('method' => 'reset')); $container->registerForAutoconfiguration(PropertyListExtractorInterface::class) ->addTag('property_info.list_extractor'); $container->registerForAutoconfiguration(PropertyTypeExtractorInterface::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml index cbed70e4e1..16b15a6617 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml @@ -8,7 +8,7 @@ - + @@ -90,7 +90,7 @@ - + diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index 6b38991b77..a2fae2fc2f 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -17,12 +17,13 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\ResettableInterface; use Symfony\Component\Cache\Traits\AbstractTrait; /** * @author Nicolas Grekas */ -abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface +abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface, ResettableInterface { use AbstractTrait; diff --git a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php index 45c19c7a6c..2118e9c6ff 100644 --- a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php @@ -14,12 +14,13 @@ namespace Symfony\Component\Cache\Adapter; use Psr\Cache\CacheItemInterface; use Psr\Log\LoggerAwareInterface; use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\ResettableInterface; use Symfony\Component\Cache\Traits\ArrayTrait; /** * @author Nicolas Grekas */ -class ArrayAdapter implements AdapterInterface, LoggerAwareInterface +class ArrayAdapter implements AdapterInterface, LoggerAwareInterface, ResettableInterface { use ArrayTrait; diff --git a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php index c38949975d..6bdf6b2d54 100644 --- a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php @@ -16,6 +16,7 @@ use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\Exception\InvalidArgumentException; use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; /** * Chains several adapters together. @@ -25,7 +26,7 @@ use Symfony\Component\Cache\PruneableInterface; * * @author Kévin Dunglas */ -class ChainAdapter implements AdapterInterface, PruneableInterface +class ChainAdapter implements AdapterInterface, PruneableInterface, ResettableInterface { private $adapters = array(); private $adapterCount; @@ -248,4 +249,16 @@ class ChainAdapter implements AdapterInterface, PruneableInterface return $pruned; } + + /** + * {@inheritdoc} + */ + public function reset() + { + foreach ($this->adapters as $adapter) { + if ($adapter instanceof ResettableInterface) { + $adapter->reset(); + } + } + } } diff --git a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php index 7e9686a018..511224f854 100644 --- a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php @@ -15,6 +15,8 @@ use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; use Symfony\Component\Cache\Traits\PhpArrayTrait; /** @@ -24,7 +26,7 @@ use Symfony\Component\Cache\Traits\PhpArrayTrait; * @author Titouan Galopin * @author Nicolas Grekas */ -class PhpArrayAdapter implements AdapterInterface +class PhpArrayAdapter implements AdapterInterface, PruneableInterface, ResettableInterface { use PhpArrayTrait; @@ -37,7 +39,7 @@ class PhpArrayAdapter implements AdapterInterface public function __construct($file, AdapterInterface $fallbackPool) { $this->file = $file; - $this->fallbackPool = $fallbackPool; + $this->pool = $fallbackPool; $this->zendDetectUnicode = ini_get('zend.detect_unicode'); $this->createCacheItem = \Closure::bind( function ($key, $value, $isHit) { @@ -89,7 +91,7 @@ class PhpArrayAdapter implements AdapterInterface $this->initialize(); } if (!isset($this->values[$key])) { - return $this->fallbackPool->getItem($key); + return $this->pool->getItem($key); } $value = $this->values[$key]; @@ -144,7 +146,7 @@ class PhpArrayAdapter implements AdapterInterface $this->initialize(); } - return isset($this->values[$key]) || $this->fallbackPool->hasItem($key); + return isset($this->values[$key]) || $this->pool->hasItem($key); } /** @@ -159,7 +161,7 @@ class PhpArrayAdapter implements AdapterInterface $this->initialize(); } - return !isset($this->values[$key]) && $this->fallbackPool->deleteItem($key); + return !isset($this->values[$key]) && $this->pool->deleteItem($key); } /** @@ -186,7 +188,7 @@ class PhpArrayAdapter implements AdapterInterface } if ($fallbackKeys) { - $deleted = $this->fallbackPool->deleteItems($fallbackKeys) && $deleted; + $deleted = $this->pool->deleteItems($fallbackKeys) && $deleted; } return $deleted; @@ -201,7 +203,7 @@ class PhpArrayAdapter implements AdapterInterface $this->initialize(); } - return !isset($this->values[$item->getKey()]) && $this->fallbackPool->save($item); + return !isset($this->values[$item->getKey()]) && $this->pool->save($item); } /** @@ -213,7 +215,7 @@ class PhpArrayAdapter implements AdapterInterface $this->initialize(); } - return !isset($this->values[$item->getKey()]) && $this->fallbackPool->saveDeferred($item); + return !isset($this->values[$item->getKey()]) && $this->pool->saveDeferred($item); } /** @@ -221,7 +223,7 @@ class PhpArrayAdapter implements AdapterInterface */ public function commit() { - return $this->fallbackPool->commit(); + return $this->pool->commit(); } /** @@ -259,7 +261,7 @@ class PhpArrayAdapter implements AdapterInterface } if ($fallbackKeys) { - foreach ($this->fallbackPool->getItems($fallbackKeys) as $key => $item) { + foreach ($this->pool->getItems($fallbackKeys) as $key => $item) { yield $key => $item; } } diff --git a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php index cd310be062..82c95c5b04 100644 --- a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php @@ -14,13 +14,17 @@ namespace Symfony\Component\Cache\Adapter; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\ProxyTrait; /** * @author Nicolas Grekas */ -class ProxyAdapter implements AdapterInterface +class ProxyAdapter implements AdapterInterface, PruneableInterface, ResettableInterface { - private $pool; + use ProxyTrait; + private $namespace; private $namespaceLen; private $createCacheItem; diff --git a/src/Symfony/Component/Cache/Adapter/SimpleCacheAdapter.php b/src/Symfony/Component/Cache/Adapter/SimpleCacheAdapter.php index f176624410..24db5d504a 100644 --- a/src/Symfony/Component/Cache/Adapter/SimpleCacheAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/SimpleCacheAdapter.php @@ -12,13 +12,17 @@ namespace Symfony\Component\Cache\Adapter; use Psr\SimpleCache\CacheInterface; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\ProxyTrait; /** * @author Nicolas Grekas */ -class SimpleCacheAdapter extends AbstractAdapter +class SimpleCacheAdapter extends AbstractAdapter implements PruneableInterface, ResettableInterface { - private $pool; + use ProxyTrait; + private $miss; public function __construct(CacheInterface $pool, $namespace = '', $defaultLifetime = 0) diff --git a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php index 96df1128bb..cb71ed3fd8 100644 --- a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php @@ -15,26 +15,29 @@ use Psr\Cache\CacheItemInterface; use Psr\Cache\InvalidArgumentException; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\ProxyTrait; /** * @author Nicolas Grekas */ -class TagAwareAdapter implements TagAwareAdapterInterface, PruneableInterface +class TagAwareAdapter implements TagAwareAdapterInterface, PruneableInterface, ResettableInterface { const TAGS_PREFIX = "\0tags\0"; - private $itemsAdapter; + use ProxyTrait; + private $deferred = array(); private $createCacheItem; private $setCacheItemTags; private $getTagsByKey; private $invalidateTags; - private $tagsAdapter; + private $tagsPool; - public function __construct(AdapterInterface $itemsAdapter, AdapterInterface $tagsAdapter = null) + public function __construct(AdapterInterface $itemsPool, AdapterInterface $tagsPool = null) { - $this->itemsAdapter = $itemsAdapter; - $this->tagsAdapter = $tagsAdapter ?: $itemsAdapter; + $this->pool = $itemsPool; + $this->tags = $tagsPool ?: $itemsPool; $this->createCacheItem = \Closure::bind( function ($key, $value, CacheItem $protoItem) { $item = new CacheItem(); @@ -110,7 +113,7 @@ class TagAwareAdapter implements TagAwareAdapterInterface, PruneableInterface } $f = $this->invalidateTags; - return $f($this->tagsAdapter, $tags); + return $f($this->tags, $tags); } /** @@ -121,10 +124,10 @@ class TagAwareAdapter implements TagAwareAdapterInterface, PruneableInterface if ($this->deferred) { $this->commit(); } - if (!$this->itemsAdapter->hasItem($key)) { + if (!$this->pool->hasItem($key)) { return false; } - if (!$itemTags = $this->itemsAdapter->getItem(static::TAGS_PREFIX.$key)->get()) { + if (!$itemTags = $this->pool->getItem(static::TAGS_PREFIX.$key)->get()) { return true; } @@ -165,9 +168,9 @@ class TagAwareAdapter implements TagAwareAdapterInterface, PruneableInterface } try { - $items = $this->itemsAdapter->getItems($tagKeys + $keys); + $items = $this->pool->getItems($tagKeys + $keys); } catch (InvalidArgumentException $e) { - $this->itemsAdapter->getItems($keys); // Should throw an exception + $this->pool->getItems($keys); // Should throw an exception throw $e; } @@ -182,7 +185,7 @@ class TagAwareAdapter implements TagAwareAdapterInterface, PruneableInterface { $this->deferred = array(); - return $this->itemsAdapter->clear(); + return $this->pool->clear(); } /** @@ -204,7 +207,7 @@ class TagAwareAdapter implements TagAwareAdapterInterface, PruneableInterface } } - return $this->itemsAdapter->deleteItems($keys); + return $this->pool->deleteItems($keys); } /** @@ -243,7 +246,7 @@ class TagAwareAdapter implements TagAwareAdapterInterface, PruneableInterface if ($this->deferred) { $items = $this->deferred; foreach ($items as $key => $item) { - if (!$this->itemsAdapter->saveDeferred($item)) { + if (!$this->pool->saveDeferred($item)) { unset($this->deferred[$key]); $ok = false; } @@ -257,17 +260,17 @@ class TagAwareAdapter implements TagAwareAdapterInterface, PruneableInterface foreach ($tagsByKey as $key => $tags) { if ($tags) { - $this->itemsAdapter->saveDeferred($f(static::TAGS_PREFIX.$key, array_intersect_key($tagVersions, $tags), $items[$key])); + $this->pool->saveDeferred($f(static::TAGS_PREFIX.$key, array_intersect_key($tagVersions, $tags), $items[$key])); } else { $deletedTags[] = static::TAGS_PREFIX.$key; } } if ($deletedTags) { - $this->itemsAdapter->deleteItems($deletedTags); + $this->pool->deleteItems($deletedTags); } } - return $this->itemsAdapter->commit() && $ok; + return $this->pool->commit() && $ok; } public function __destruct() @@ -328,23 +331,11 @@ class TagAwareAdapter implements TagAwareAdapterInterface, PruneableInterface $tagVersions[$tag] = $tag.static::TAGS_PREFIX; $tags[$tag.static::TAGS_PREFIX] = $tag; } - foreach ($this->tagsAdapter->getItems($tagVersions) as $tag => $version) { + foreach ($this->tags->getItems($tagVersions) as $tag => $version) { $tagVersions[$tags[$tag]] = $version->get() ?: 0; } } return $tagVersions; } - - /** - * {@inheritdoc} - */ - public function prune() - { - if ($this->itemsAdapter instanceof PruneableInterface) { - return $this->itemsAdapter->prune(); - } - - return false; - } } diff --git a/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php b/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php index 9959199f67..e8563521ba 100644 --- a/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Cache\Adapter; use Psr\Cache\CacheItemInterface; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; /** * An adapter that collects data about all cache calls. @@ -20,7 +22,7 @@ use Psr\Cache\CacheItemInterface; * @author Tobias Nyholm * @author Nicolas Grekas */ -class TraceableAdapter implements AdapterInterface +class TraceableAdapter implements AdapterInterface, PruneableInterface, ResettableInterface { protected $pool; private $calls = array(); @@ -168,6 +170,38 @@ class TraceableAdapter implements AdapterInterface } } + /** + * {@inheritdoc} + */ + public function prune() + { + if (!$this->pool instanceof PruneableInterface) { + return false; + } + $event = $this->start(__FUNCTION__); + try { + return $event->result = $this->pool->prune(); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function reset() + { + if (!$this->pool instanceof ResettableInterface) { + return; + } + $event = $this->start(__FUNCTION__); + try { + $this->pool->reset(); + } finally { + $event->end = microtime(true); + } + } + public function getCalls() { try { diff --git a/src/Symfony/Component/Cache/DoctrineProvider.php b/src/Symfony/Component/Cache/DoctrineProvider.php index 5d9c2faed7..cebe95fbc7 100644 --- a/src/Symfony/Component/Cache/DoctrineProvider.php +++ b/src/Symfony/Component/Cache/DoctrineProvider.php @@ -17,7 +17,7 @@ use Psr\Cache\CacheItemPoolInterface; /** * @author Nicolas Grekas */ -class DoctrineProvider extends CacheProvider +class DoctrineProvider extends CacheProvider implements PruneableInterface, ResettableInterface { private $pool; @@ -26,6 +26,25 @@ class DoctrineProvider extends CacheProvider $this->pool = $pool; } + /** + * {@inheritdoc} + */ + public function prune() + { + return $this->pool instanceof PruneableInterface && $this->pool->prune(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + if ($this->pool instanceof ResettableInterface) { + $this->pool->reset(); + } + $this->setNamespace($this->getNamespace()); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Cache/ResettableInterface.php b/src/Symfony/Component/Cache/ResettableInterface.php new file mode 100644 index 0000000000..6be72861e7 --- /dev/null +++ b/src/Symfony/Component/Cache/ResettableInterface.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\Cache; + +/** + * Resets a pool's local state. + */ +interface ResettableInterface +{ + public function reset(); +} diff --git a/src/Symfony/Component/Cache/Simple/AbstractCache.php b/src/Symfony/Component/Cache/Simple/AbstractCache.php index 264eb60653..e666effaf9 100644 --- a/src/Symfony/Component/Cache/Simple/AbstractCache.php +++ b/src/Symfony/Component/Cache/Simple/AbstractCache.php @@ -16,11 +16,12 @@ use Psr\SimpleCache\CacheInterface; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\Exception\InvalidArgumentException; use Symfony\Component\Cache\Traits\AbstractTrait; +use Symfony\Component\Cache\ResettableInterface; /** * @author Nicolas Grekas */ -abstract class AbstractCache implements CacheInterface, LoggerAwareInterface +abstract class AbstractCache implements CacheInterface, LoggerAwareInterface, ResettableInterface { use AbstractTrait { deleteItems as private; diff --git a/src/Symfony/Component/Cache/Simple/ArrayCache.php b/src/Symfony/Component/Cache/Simple/ArrayCache.php index a89768b0e2..8d027cd2a3 100644 --- a/src/Symfony/Component/Cache/Simple/ArrayCache.php +++ b/src/Symfony/Component/Cache/Simple/ArrayCache.php @@ -15,12 +15,13 @@ use Psr\Log\LoggerAwareInterface; use Psr\SimpleCache\CacheInterface; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\ResettableInterface; use Symfony\Component\Cache\Traits\ArrayTrait; /** * @author Nicolas Grekas */ -class ArrayCache implements CacheInterface, LoggerAwareInterface +class ArrayCache implements CacheInterface, LoggerAwareInterface, ResettableInterface { use ArrayTrait { ArrayTrait::deleteItem as delete; diff --git a/src/Symfony/Component/Cache/Simple/ChainCache.php b/src/Symfony/Component/Cache/Simple/ChainCache.php index 8bb944fd47..9d0c75870e 100644 --- a/src/Symfony/Component/Cache/Simple/ChainCache.php +++ b/src/Symfony/Component/Cache/Simple/ChainCache.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Cache\Simple; use Psr\SimpleCache\CacheInterface; use Symfony\Component\Cache\Exception\InvalidArgumentException; use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; /** * Chains several caches together. @@ -23,7 +24,7 @@ use Symfony\Component\Cache\PruneableInterface; * * @author Nicolas Grekas */ -class ChainCache implements CacheInterface, PruneableInterface +class ChainCache implements CacheInterface, PruneableInterface, ResettableInterface { private $miss; private $caches = array(); @@ -236,4 +237,16 @@ class ChainCache implements CacheInterface, PruneableInterface return $pruned; } + + /** + * {@inheritdoc} + */ + public function reset() + { + foreach ($this->caches as $cache) { + if ($cache instanceof ResettableInterface) { + $cache->reset(); + } + } + } } diff --git a/src/Symfony/Component/Cache/Simple/PhpArrayCache.php b/src/Symfony/Component/Cache/Simple/PhpArrayCache.php index 92180e6316..3db8a2ac1c 100644 --- a/src/Symfony/Component/Cache/Simple/PhpArrayCache.php +++ b/src/Symfony/Component/Cache/Simple/PhpArrayCache.php @@ -14,6 +14,8 @@ namespace Symfony\Component\Cache\Simple; use Psr\SimpleCache\CacheInterface; use Symfony\Component\Cache\Exception\InvalidArgumentException; use Symfony\Component\Cache\Traits\PhpArrayTrait; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; /** * Caches items at warm up time using a PHP array that is stored in shared memory by OPCache since PHP 7.0. @@ -22,7 +24,7 @@ use Symfony\Component\Cache\Traits\PhpArrayTrait; * @author Titouan Galopin * @author Nicolas Grekas */ -class PhpArrayCache implements CacheInterface +class PhpArrayCache implements CacheInterface, PruneableInterface, ResettableInterface { use PhpArrayTrait; @@ -33,7 +35,7 @@ class PhpArrayCache implements CacheInterface public function __construct($file, CacheInterface $fallbackPool) { $this->file = $file; - $this->fallbackPool = $fallbackPool; + $this->pool = $fallbackPool; $this->zendDetectUnicode = ini_get('zend.detect_unicode'); } @@ -68,7 +70,7 @@ class PhpArrayCache implements CacheInterface $this->initialize(); } if (!isset($this->values[$key])) { - return $this->fallbackPool->get($key, $default); + return $this->pool->get($key, $default); } $value = $this->values[$key]; @@ -124,7 +126,7 @@ class PhpArrayCache implements CacheInterface $this->initialize(); } - return isset($this->values[$key]) || $this->fallbackPool->has($key); + return isset($this->values[$key]) || $this->pool->has($key); } /** @@ -139,7 +141,7 @@ class PhpArrayCache implements CacheInterface $this->initialize(); } - return !isset($this->values[$key]) && $this->fallbackPool->delete($key); + return !isset($this->values[$key]) && $this->pool->delete($key); } /** @@ -170,7 +172,7 @@ class PhpArrayCache implements CacheInterface } if ($fallbackKeys) { - $deleted = $this->fallbackPool->deleteMultiple($fallbackKeys) && $deleted; + $deleted = $this->pool->deleteMultiple($fallbackKeys) && $deleted; } return $deleted; @@ -188,7 +190,7 @@ class PhpArrayCache implements CacheInterface $this->initialize(); } - return !isset($this->values[$key]) && $this->fallbackPool->set($key, $value, $ttl); + return !isset($this->values[$key]) && $this->pool->set($key, $value, $ttl); } /** @@ -216,7 +218,7 @@ class PhpArrayCache implements CacheInterface } if ($fallbackValues) { - $saved = $this->fallbackPool->setMultiple($fallbackValues, $ttl) && $saved; + $saved = $this->pool->setMultiple($fallbackValues, $ttl) && $saved; } return $saved; @@ -249,7 +251,7 @@ class PhpArrayCache implements CacheInterface } if ($fallbackKeys) { - foreach ($this->fallbackPool->getMultiple($fallbackKeys, $default) as $key => $item) { + foreach ($this->pool->getMultiple($fallbackKeys, $default) as $key => $item) { yield $key => $item; } } diff --git a/src/Symfony/Component/Cache/Simple/Psr6Cache.php b/src/Symfony/Component/Cache/Simple/Psr6Cache.php index 55fa98da12..81f14d4a70 100644 --- a/src/Symfony/Component/Cache/Simple/Psr6Cache.php +++ b/src/Symfony/Component/Cache/Simple/Psr6Cache.php @@ -18,13 +18,17 @@ use Psr\SimpleCache\CacheException as SimpleCacheException; use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\ProxyTrait; /** * @author Nicolas Grekas */ -class Psr6Cache implements CacheInterface +class Psr6Cache implements CacheInterface, PruneableInterface, ResettableInterface { - private $pool; + use ProxyTrait; + private $createCacheItem; public function __construct(CacheItemPoolInterface $pool) diff --git a/src/Symfony/Component/Cache/Simple/TraceableCache.php b/src/Symfony/Component/Cache/Simple/TraceableCache.php index 29cc10bbb2..756403bf14 100644 --- a/src/Symfony/Component/Cache/Simple/TraceableCache.php +++ b/src/Symfony/Component/Cache/Simple/TraceableCache.php @@ -12,13 +12,15 @@ namespace Symfony\Component\Cache\Simple; use Psr\SimpleCache\CacheInterface; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; /** * An adapter that collects data about all cache calls. * * @author Nicolas Grekas */ -class TraceableCache implements CacheInterface +class TraceableCache implements CacheInterface, PruneableInterface, ResettableInterface { private $pool; private $miss; @@ -177,6 +179,38 @@ class TraceableCache implements CacheInterface } } + /** + * {@inheritdoc} + */ + public function prune() + { + if (!$this->pool instanceof PruneableInterface) { + return false; + } + $event = $this->start(__FUNCTION__); + try { + return $event->result = $this->pool->prune(); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function reset() + { + if (!$this->pool instanceof ResettableInterface) { + return; + } + $event = $this->start(__FUNCTION__); + try { + $this->pool->reset(); + } finally { + $event->end = microtime(true); + } + } + public function getCalls() { try { diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php index 134dba7c90..14b61263c5 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php @@ -50,6 +50,7 @@ class PhpArrayAdapterTest extends AdapterTestCase 'testDeleteItemsInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', 'testDefaultLifeTime' => 'PhpArrayAdapter does not allow configuring a default lifetime.', + 'testPrune' => 'PhpArrayAdapter just proxies', ); protected static $file; diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php index 45a50d2323..1a23198c2f 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php @@ -25,6 +25,7 @@ class PhpArrayAdapterWithFallbackTest extends AdapterTestCase 'testHasItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', 'testDeleteItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', 'testDeleteItemsInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', + 'testPrune' => 'PhpArrayAdapter just proxies', ); protected static $file; diff --git a/src/Symfony/Component/Cache/Tests/Adapter/ProxyAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/ProxyAdapterTest.php index c0174dd248..5e6abd16c1 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/ProxyAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/ProxyAdapterTest.php @@ -24,6 +24,7 @@ class ProxyAdapterTest extends AdapterTestCase protected $skippedTests = array( 'testDeferredSaveWithoutCommit' => 'Assumes a shared cache which ArrayAdapter is not.', 'testSaveWithoutExpire' => 'Assumes a shared cache which ArrayAdapter is not.', + 'testPrune' => 'ProxyAdapter just proxies', ); public function createCachePool($defaultLifetime = 0) diff --git a/src/Symfony/Component/Cache/Tests/Adapter/SimpleCacheAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/SimpleCacheAdapterTest.php index 1e0297c69e..d5795d52e7 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/SimpleCacheAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/SimpleCacheAdapterTest.php @@ -19,6 +19,10 @@ use Symfony\Component\Cache\Adapter\SimpleCacheAdapter; */ class SimpleCacheAdapterTest extends AdapterTestCase { + protected $skippedTests = array( + 'testPrune' => 'SimpleCache just proxies', + ); + public function createCachePool($defaultLifetime = 0) { return new SimpleCacheAdapter(new FilesystemCache(), '', $defaultLifetime); diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TraceableAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TraceableAdapterTest.php index dec2f25555..3755e88db5 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/TraceableAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/TraceableAdapterTest.php @@ -19,6 +19,10 @@ use Symfony\Component\Cache\Adapter\TraceableAdapter; */ class TraceableAdapterTest extends AdapterTestCase { + protected $skippedTests = array( + 'testPrune' => 'TraceableAdapter just proxies', + ); + public function createCachePool($defaultLifetime = 0) { return new TraceableAdapter(new FilesystemAdapter('', $defaultLifetime)); diff --git a/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheTest.php index 57361905f8..1bd0ca2778 100644 --- a/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheTest.php +++ b/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheTest.php @@ -44,6 +44,7 @@ class PhpArrayCacheTest extends CacheTestCase 'testSetValidData' => 'PhpArrayCache does no validation', 'testDefaultLifeTime' => 'PhpArrayCache does not allow configuring a default lifetime.', + 'testPrune' => 'PhpArrayCache just proxies', ); protected static $file; diff --git a/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheWithFallbackTest.php b/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheWithFallbackTest.php index a624fa73e7..4b6a94f709 100644 --- a/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheWithFallbackTest.php +++ b/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheWithFallbackTest.php @@ -31,6 +31,7 @@ class PhpArrayCacheWithFallbackTest extends CacheTestCase 'testSetMultipleInvalidKeys' => 'PhpArrayCache does no validation', 'testSetMultipleInvalidTtl' => 'PhpArrayCache does no validation', 'testHasInvalidKeys' => 'PhpArrayCache does no validation', + 'testPrune' => 'PhpArrayCache just proxies', ); protected static $file; diff --git a/src/Symfony/Component/Cache/Tests/Simple/Psr6CacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/Psr6CacheTest.php index 16e21d0c0b..78582894fb 100644 --- a/src/Symfony/Component/Cache/Tests/Simple/Psr6CacheTest.php +++ b/src/Symfony/Component/Cache/Tests/Simple/Psr6CacheTest.php @@ -19,6 +19,10 @@ use Symfony\Component\Cache\Simple\Psr6Cache; */ class Psr6CacheTest extends CacheTestCase { + protected $skippedTests = array( + 'testPrune' => 'Psr6Cache just proxies', + ); + public function createSimpleCache($defaultLifetime = 0) { return new Psr6Cache(new FilesystemAdapter('', $defaultLifetime)); diff --git a/src/Symfony/Component/Cache/Tests/Simple/TraceableCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/TraceableCacheTest.php index 7feccba1af..535f93da4b 100644 --- a/src/Symfony/Component/Cache/Tests/Simple/TraceableCacheTest.php +++ b/src/Symfony/Component/Cache/Tests/Simple/TraceableCacheTest.php @@ -19,6 +19,10 @@ use Symfony\Component\Cache\Simple\TraceableCache; */ class TraceableCacheTest extends CacheTestCase { + protected $skippedTests = array( + 'testPrune' => 'TraceableCache just proxies', + ); + public function createSimpleCache($defaultLifetime = 0) { return new TraceableCache(new FilesystemCache('', $defaultLifetime)); diff --git a/src/Symfony/Component/Cache/Traits/AbstractTrait.php b/src/Symfony/Component/Cache/Traits/AbstractTrait.php index 108ac67c8e..d7af3b559e 100644 --- a/src/Symfony/Component/Cache/Traits/AbstractTrait.php +++ b/src/Symfony/Component/Cache/Traits/AbstractTrait.php @@ -189,6 +189,17 @@ trait AbstractTrait return $wasEnabled; } + /** + * {@inheritdoc} + */ + public function reset() + { + if ($this->deferred) { + $this->commit(); + } + $this->namespaceVersion = ''; + } + /** * Like the native unserialize() function but throws an exception if anything goes wrong. * diff --git a/src/Symfony/Component/Cache/Traits/ArrayTrait.php b/src/Symfony/Component/Cache/Traits/ArrayTrait.php index 3fb5fa36be..b7d2ad6d62 100644 --- a/src/Symfony/Component/Cache/Traits/ArrayTrait.php +++ b/src/Symfony/Component/Cache/Traits/ArrayTrait.php @@ -69,6 +69,14 @@ trait ArrayTrait return true; } + /** + * {@inheritdoc} + */ + public function reset() + { + $this->clear(); + } + private function generateItems(array $keys, $now, $f) { foreach ($keys as $i => $key) { diff --git a/src/Symfony/Component/Cache/Traits/DoctrineTrait.php b/src/Symfony/Component/Cache/Traits/DoctrineTrait.php index be351cf53a..c87ecabafc 100644 --- a/src/Symfony/Component/Cache/Traits/DoctrineTrait.php +++ b/src/Symfony/Component/Cache/Traits/DoctrineTrait.php @@ -20,6 +20,15 @@ trait DoctrineTrait { private $provider; + /** + * {@inheritdoc} + */ + public function reset() + { + parent::reset(); + $this->provider->setNamespace($this->provider->getNamespace()); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php b/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php index ccc48886d0..ae634d6baa 100644 --- a/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php +++ b/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php @@ -22,9 +22,10 @@ use Symfony\Component\Cache\Exception\InvalidArgumentException; */ trait PhpArrayTrait { + use ProxyTrait; + private $file; private $values; - private $fallbackPool; private $zendDetectUnicode; /** @@ -119,7 +120,7 @@ EOF; $cleared = @unlink($this->file) || !file_exists($this->file); - return $this->fallbackPool->clear() && $cleared; + return $this->pool->clear() && $cleared; } /** diff --git a/src/Symfony/Component/Cache/Traits/ProxyTrait.php b/src/Symfony/Component/Cache/Traits/ProxyTrait.php new file mode 100644 index 0000000000..06dba7e59b --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/ProxyTrait.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; + +/** + * @author Nicolas Grekas + */ +trait ProxyTrait +{ + private $pool; + + /** + * {@inheritdoc} + */ + public function prune() + { + return $this->pool instanceof PruneableInterface && $this->pool->prune(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + if ($this->pool instanceof ResettableInterface) { + $this->pool->reset(); + } + } +} From dc55dd2c99a337d9baf803f017f139f1b280124b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 24 Sep 2017 00:04:06 +0200 Subject: [PATCH 10/13] [DI] Add AutowireRequiredMethodsPass to fix bindings for `@required` methods --- .../Compiler/AbstractRecursivePass.php | 18 +- .../Compiler/AutowirePass.php | 60 +-- .../Compiler/AutowireRequiredMethodsPass.php | 70 ++++ .../Compiler/PassConfig.php | 1 + .../Compiler/ResolveNamedArgumentsPass.php | 6 +- .../Tests/Compiler/AutowirePassTest.php | 342 +---------------- .../AutowireRequiredMethodsPassTest.php | 80 ++++ .../Compiler/ResolveBindingsPassTest.php | 16 + .../Fixtures/includes/autowiring_classes.php | 343 ++++++++++++++++++ 9 files changed, 535 insertions(+), 401 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Compiler/AutowireRequiredMethodsPass.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowireRequiredMethodsPassTest.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php index bbe869b935..ddc7d004da 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php @@ -90,7 +90,7 @@ abstract class AbstractRecursivePass implements CompilerPassInterface { if (is_string($factory = $definition->getFactory())) { if (!function_exists($factory)) { - throw new RuntimeException(sprintf('Unable to resolve service "%s": function "%s" does not exist.', $this->currentId, $factory)); + throw new RuntimeException(sprintf('Invalid service "%s": function "%s" does not exist.', $this->currentId, $factory)); } $r = new \ReflectionFunction($factory); if (false !== $r->getFileName() && file_exists($r->getFileName())) { @@ -108,7 +108,7 @@ abstract class AbstractRecursivePass implements CompilerPassInterface $class = $definition->getClass(); } if ('__construct' === $method) { - throw new RuntimeException(sprintf('Unable to resolve service "%s": "__construct()" cannot be used as a factory method.', $this->currentId)); + throw new RuntimeException(sprintf('Invalid service "%s": "__construct()" cannot be used as a factory method.', $this->currentId)); } return $this->getReflectionMethod(new Definition($class), $method); @@ -117,14 +117,14 @@ abstract class AbstractRecursivePass implements CompilerPassInterface $class = $definition->getClass(); if (!$r = $this->container->getReflectionClass($class)) { - throw new RuntimeException(sprintf('Unable to resolve service "%s": class "%s" does not exist.', $this->currentId, $class)); + throw new RuntimeException(sprintf('Invalid service "%s": class "%s" does not exist.', $this->currentId, $class)); } if (!$r = $r->getConstructor()) { if ($required) { - throw new RuntimeException(sprintf('Unable to resolve service "%s": class%s has no constructor.', $this->currentId, sprintf($class !== $this->currentId ? ' "%s"' : '', $class))); + throw new RuntimeException(sprintf('Invalid service "%s": class%s has no constructor.', $this->currentId, sprintf($class !== $this->currentId ? ' "%s"' : '', $class))); } } elseif (!$r->isPublic()) { - throw new RuntimeException(sprintf('Unable to resolve service "%s": %s must be public.', $this->currentId, sprintf($class !== $this->currentId ? 'constructor of class "%s"' : 'its constructor', $class))); + throw new RuntimeException(sprintf('Invalid service "%s": %s must be public.', $this->currentId, sprintf($class !== $this->currentId ? 'constructor of class "%s"' : 'its constructor', $class))); } return $r; @@ -145,20 +145,20 @@ abstract class AbstractRecursivePass implements CompilerPassInterface } if (!$class = $definition->getClass()) { - throw new RuntimeException(sprintf('Unable to resolve service "%s": the class is not set.', $this->currentId)); + throw new RuntimeException(sprintf('Invalid service "%s": the class is not set.', $this->currentId)); } if (!$r = $this->container->getReflectionClass($class)) { - throw new RuntimeException(sprintf('Unable to resolve service "%s": class "%s" does not exist.', $this->currentId, $class)); + throw new RuntimeException(sprintf('Invalid service "%s": class "%s" does not exist.', $this->currentId, $class)); } if (!$r->hasMethod($method)) { - throw new RuntimeException(sprintf('Unable to resolve service "%s": method "%s()" does not exist.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method)); + throw new RuntimeException(sprintf('Invalid service "%s": method "%s()" does not exist.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method)); } $r = $r->getMethod($method); if (!$r->isPublic()) { - throw new RuntimeException(sprintf('Unable to resolve service "%s": method "%s()" must be public.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method)); + throw new RuntimeException(sprintf('Invalid service "%s": method "%s()" must be public.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method)); } return $r; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index 8cb1978ef6..ace116fb9b 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -130,7 +130,6 @@ class AutowirePass extends AbstractRecursivePass return $value; } - $autowiredMethods = $this->getMethodsToAutowire($reflectionClass); $methodCalls = $value->getMethodCalls(); try { @@ -143,7 +142,7 @@ class AutowirePass extends AbstractRecursivePass array_unshift($methodCalls, array($constructor, $value->getArguments())); } - $methodCalls = $this->autowireCalls($reflectionClass, $methodCalls, $autowiredMethods); + $methodCalls = $this->autowireCalls($reflectionClass, $methodCalls); if ($constructor) { list(, $arguments) = array_shift($methodCalls); @@ -161,61 +160,18 @@ class AutowirePass extends AbstractRecursivePass } /** - * Gets the list of methods to autowire. - * * @param \ReflectionClass $reflectionClass - * - * @return \ReflectionMethod[] - */ - private function getMethodsToAutowire(\ReflectionClass $reflectionClass) - { - $methodsToAutowire = array(); - - foreach ($reflectionClass->getMethods() as $reflectionMethod) { - $r = $reflectionMethod; - - if ($r->isConstructor()) { - continue; - } - - while (true) { - if (false !== $doc = $r->getDocComment()) { - if (false !== stripos($doc, '@required') && preg_match('#(?:^/\*\*|\n\s*+\*)\s*+@required(?:\s|\*/$)#i', $doc)) { - $methodsToAutowire[strtolower($reflectionMethod->name)] = $reflectionMethod; - break; - } - if (false === stripos($doc, '@inheritdoc') || !preg_match('#(?:^/\*\*|\n\s*+\*)\s*+(?:\{@inheritdoc\}|@inheritdoc)(?:\s|\*/$)#i', $doc)) { - break; - } - } - try { - $r = $r->getPrototype(); - } catch (\ReflectionException $e) { - break; // method has no prototype - } - } - } - - return $methodsToAutowire; - } - - /** - * @param \ReflectionClass $reflectionClass - * @param array $methodCalls - * @param \ReflectionMethod[] $autowiredMethods + * @param array $methodCalls * * @return array */ - private function autowireCalls(\ReflectionClass $reflectionClass, array $methodCalls, array $autowiredMethods) + private function autowireCalls(\ReflectionClass $reflectionClass, array $methodCalls) { foreach ($methodCalls as $i => $call) { list($method, $arguments) = $call; if ($method instanceof \ReflectionFunctionAbstract) { $reflectionMethod = $method; - } elseif (isset($autowiredMethods[$lcMethod = strtolower($method)]) && $autowiredMethods[$lcMethod]->isPublic()) { - $reflectionMethod = $autowiredMethods[$lcMethod]; - unset($autowiredMethods[$lcMethod]); } else { $reflectionMethod = $this->getReflectionMethod(new Definition($reflectionClass->name), $method); } @@ -227,16 +183,6 @@ class AutowirePass extends AbstractRecursivePass } } - foreach ($autowiredMethods as $lcMethod => $reflectionMethod) { - $method = $reflectionMethod->name; - - if (!$reflectionMethod->isPublic()) { - $class = $reflectionClass->name; - throw new AutowiringFailedException($this->currentId, sprintf('Cannot autowire service "%s": method "%s()" must be public.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method)); - } - $methodCalls[] = array($method, $this->autowireMethod($reflectionMethod, array())); - } - return $methodCalls; } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowireRequiredMethodsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowireRequiredMethodsPass.php new file mode 100644 index 0000000000..6c744f88f6 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowireRequiredMethodsPass.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Definition; + +/** + * Looks for definitions with autowiring enabled and registers their corresponding "@required" methods as setters. + * + * @author Nicolas Grekas + */ +class AutowireRequiredMethodsPass extends AbstractRecursivePass +{ + /** + * {@inheritdoc} + */ + protected function processValue($value, $isRoot = false) + { + $value = parent::processValue($value, $isRoot); + + if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) { + return $value; + } + if (!$reflectionClass = $this->container->getReflectionClass($value->getClass(), false)) { + return $value; + } + + $alreadyCalledMethods = array(); + + foreach ($value->getMethodCalls() as list($method)) { + $alreadyCalledMethods[strtolower($method)] = true; + } + + foreach ($reflectionClass->getMethods() as $reflectionMethod) { + $r = $reflectionMethod; + + if ($r->isConstructor() || isset($alreadyCalledMethods[strtolower($r->name)])) { + continue; + } + + while (true) { + if (false !== $doc = $r->getDocComment()) { + if (false !== stripos($doc, '@required') && preg_match('#(?:^/\*\*|\n\s*+\*)\s*+@required(?:\s|\*/$)#i', $doc)) { + $value->addMethodCall($reflectionMethod->name); + break; + } + if (false === stripos($doc, '@inheritdoc') || !preg_match('#(?:^/\*\*|\n\s*+\*)\s*+(?:\{@inheritdoc\}|@inheritdoc)(?:\s|\*/$)#i', $doc)) { + break; + } + } + try { + $r = $r->getPrototype(); + } catch (\ReflectionException $e) { + break; // method has no prototype + } + } + } + + return $value; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index 79792bee4a..e80b8e6cc8 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -58,6 +58,7 @@ class PassConfig new CheckDefinitionValidityPass(), new RegisterServiceSubscribersPass(), new ResolveNamedArgumentsPass(), + new AutowireRequiredMethodsPass(), new ResolveBindingsPass(), $autowirePass = new AutowirePass(false), new ResolveServiceSubscribersPass(), diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php index e4d592d0c6..a96f981d07 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php @@ -61,11 +61,11 @@ class ResolveNamedArgumentsPass extends AbstractRecursivePass } } - throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s()" has no argument named "%s". Check your service definition.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method, $key)); + throw new InvalidArgumentException(sprintf('Invalid service "%s": method "%s()" has no argument named "%s". Check your service definition.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method, $key)); } if (null !== $argument && !$argument instanceof Reference && !$argument instanceof Definition) { - throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": the value of argument "%s" of method "%s()" must be null, an instance of %s or an instance of %s, %s given.', $this->currentId, $key, $class !== $this->currentId ? $class.'::'.$method : $method, Reference::class, Definition::class, gettype($argument))); + throw new InvalidArgumentException(sprintf('Invalid service "%s": the value of argument "%s" of method "%s()" must be null, an instance of %s or an instance of %s, %s given.', $this->currentId, $key, $class !== $this->currentId ? $class.'::'.$method : $method, Reference::class, Definition::class, gettype($argument))); } foreach ($parameters as $j => $p) { @@ -76,7 +76,7 @@ class ResolveNamedArgumentsPass extends AbstractRecursivePass } } - throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s()" has no argument type-hinted as "%s". Check your service definition.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method, $key)); + throw new InvalidArgumentException(sprintf('Invalid service "%s": method "%s()" has no argument type-hinted as "%s". Check your service definition.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method, $key)); } if ($resolvedArguments !== $call[1]) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index 94666df608..dbf839b4ea 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Compiler\AutowireRequiredMethodsPass; use Symfony\Component\DependencyInjection\Compiler\AutowirePass; use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -20,6 +21,8 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\FooVariadic; use Symfony\Component\DependencyInjection\TypedReference; +require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php'; + /** * @author Kévin Dunglas */ @@ -174,7 +177,7 @@ class AutowirePassTest extends TestCase /** * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException - * @expectedExceptionMessage Unable to resolve service "private_service": constructor of class "Symfony\Component\DependencyInjection\Tests\Compiler\PrivateConstructor" must be public. + * @expectedExceptionMessage Invalid service "private_service": constructor of class "Symfony\Component\DependencyInjection\Tests\Compiler\PrivateConstructor" must be public. */ public function testPrivateConstructorThrowsAutowireException() { @@ -554,6 +557,7 @@ class AutowirePassTest extends TestCase ; (new ResolveClassPass())->process($container); + (new AutowireRequiredMethodsPass())->process($container); (new AutowirePass())->process($container); $methodCalls = $container->getDefinition('setter_injection')->getMethodCalls(); @@ -590,6 +594,7 @@ class AutowirePassTest extends TestCase ; (new ResolveClassPass())->process($container); + (new AutowireRequiredMethodsPass())->process($container); (new AutowirePass())->process($container); $methodCalls = $container->getDefinition('setter_injection')->getMethodCalls(); @@ -687,6 +692,8 @@ class AutowirePassTest extends TestCase $aDefinition = $container->register('setter_injection_collision', SetterInjectionCollision::class); $aDefinition->setAutowired(true); + (new AutowireRequiredMethodsPass())->process($container); + $pass = new AutowirePass(); $pass->process($container); } @@ -770,6 +777,7 @@ class AutowirePassTest extends TestCase } (new ResolveClassPass())->process($container); + (new AutowireRequiredMethodsPass())->process($container); (new AutowirePass())->process($container); } @@ -778,7 +786,7 @@ class AutowirePassTest extends TestCase return array( array('setNotAutowireable', 'Cannot autowire service "foo": argument "$n" of method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setNotAutowireable()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\NotARealClass" but this class cannot be loaded.'), array('setDifferentNamespace', 'Cannot autowire service "foo": argument "$n" of method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setDifferentNamespace()" references class "stdClass" but no such service exists. It cannot be auto-registered because it is from a different root namespace.'), - array(null, 'Cannot autowire service "foo": method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setProtectedMethod()" must be public.'), + array(null, 'Invalid service "foo": method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setProtectedMethod()" must be public.'), ); } @@ -859,333 +867,3 @@ class AutowirePassTest extends TestCase $pass->process($container); } } - -class Foo -{ -} - -class Bar -{ - public function __construct(Foo $foo) - { - } -} - -interface AInterface -{ -} - -class A implements AInterface -{ - public static function create(Foo $foo) - { - } -} - -class B extends A -{ -} - -class C -{ - public function __construct(A $a) - { - } -} - -interface DInterface -{ -} - -interface EInterface extends DInterface -{ -} - -interface IInterface -{ -} - -class I implements IInterface -{ -} - -class F extends I implements EInterface -{ -} - -class G -{ - public function __construct(DInterface $d, EInterface $e, IInterface $i) - { - } -} - -class H -{ - public function __construct(B $b, DInterface $d) - { - } -} - -class D -{ - public function __construct(A $a, DInterface $d) - { - } -} - -class E -{ - public function __construct(D $d = null) - { - } -} - -class J -{ - public function __construct(I $i) - { - } -} - -interface CollisionInterface -{ -} - -class CollisionA implements CollisionInterface -{ -} - -class CollisionB implements CollisionInterface -{ -} - -class CannotBeAutowired -{ - public function __construct(CollisionInterface $collision) - { - } -} - -class CannotBeAutowiredForwardOrder -{ - public function __construct(CollisionA $a, CollisionInterface $b, CollisionB $c) - { - } -} - -class CannotBeAutowiredReverseOrder -{ - public function __construct(CollisionA $a, CollisionB $c, CollisionInterface $b) - { - } -} - -class Lille -{ -} - -class Dunglas -{ - public function __construct(Lille $l) - { - } -} - -class LesTilleuls -{ - public function __construct(Dunglas $j, Dunglas $k) - { - } -} - -class OptionalParameter -{ - public function __construct(CollisionInterface $c = null, A $a, Foo $f = null) - { - } -} - -class BadTypeHintedArgument -{ - public function __construct(Dunglas $k, NotARealClass $r) - { - } -} -class BadParentTypeHintedArgument -{ - public function __construct(Dunglas $k, OptionalServiceClass $r) - { - } -} -class NotGuessableArgument -{ - public function __construct(Foo $k) - { - } -} -class NotGuessableArgumentForSubclass -{ - public function __construct(A $k) - { - } -} -class MultipleArguments -{ - public function __construct(A $k, $foo, Dunglas $dunglas) - { - } -} - -class MultipleArgumentsOptionalScalar -{ - public function __construct(A $a, $foo = 'default_val', Lille $lille = null) - { - } -} -class MultipleArgumentsOptionalScalarLast -{ - public function __construct(A $a, Lille $lille, $foo = 'some_val') - { - } -} -class MultipleArgumentsOptionalScalarNotReallyOptional -{ - public function __construct(A $a, $foo = 'default_val', Lille $lille) - { - } -} - -/* - * Classes used for testing createResourceForClass - */ -class ClassForResource -{ - public function __construct($foo, Bar $bar = null) - { - } - - public function setBar(Bar $bar) - { - } -} -class IdenticalClassResource extends ClassForResource -{ -} - -class ClassChangedConstructorArgs extends ClassForResource -{ - public function __construct($foo, Bar $bar, $baz) - { - } -} - -class SetterInjection extends SetterInjectionParent -{ - /** - * @required - */ - public function setFoo(Foo $foo) - { - // should be called - } - - /** @inheritdoc*/ - public function setDependencies(Foo $foo, A $a) - { - // should be called - } - - /** {@inheritdoc} */ - public function setWithCallsConfigured(A $a) - { - // this method has a calls configured on it - } - - public function notASetter(A $a) - { - // should be called only when explicitly specified - } - - /** - * @required*/ - public function setChildMethodWithoutDocBlock(A $a) - { - } -} - -class SetterInjectionParent -{ - /** @required*/ - public function setDependencies(Foo $foo, A $a) - { - // should be called - } - - public function notASetter(A $a) - { - // @required should be ignored when the child does not add @inheritdoc - } - - /** @required prefix is on purpose */ - public function setWithCallsConfigured(A $a) - { - } - - /** @required */ - public function setChildMethodWithoutDocBlock(A $a) - { - } -} - -class SetterInjectionCollision -{ - /** - * @required - */ - public function setMultipleInstancesForOneArg(CollisionInterface $collision) - { - // The CollisionInterface cannot be autowired - there are multiple - - // should throw an exception - } -} - -class NotWireable -{ - public function setNotAutowireable(NotARealClass $n) - { - } - - public function setBar() - { - } - - public function setOptionalNotAutowireable(NotARealClass $n = null) - { - } - - public function setDifferentNamespace(\stdClass $n) - { - } - - public function setOptionalNoTypeHint($foo = null) - { - } - - public function setOptionalArgNoAutowireable($other = 'default_val') - { - } - - /** @required */ - protected function setProtectedMethod(A $a) - { - } -} - -class PrivateConstructor -{ - private function __construct() - { - } -} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowireRequiredMethodsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowireRequiredMethodsPassTest.php new file mode 100644 index 0000000000..3b067150ff --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowireRequiredMethodsPassTest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass; +use Symfony\Component\DependencyInjection\Compiler\AutowireRequiredMethodsPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php'; + +class AutowireRequiredMethodsPassTest extends TestCase +{ + public function testSetterInjection() + { + $container = new ContainerBuilder(); + $container->register(Foo::class); + $container->register(A::class); + $container->register(CollisionA::class); + $container->register(CollisionB::class); + + // manually configure *one* call, to override autowiring + $container + ->register('setter_injection', SetterInjection::class) + ->setAutowired(true) + ->addMethodCall('setWithCallsConfigured', array('manual_arg1', 'manual_arg2')); + + (new ResolveClassPass())->process($container); + (new AutowireRequiredMethodsPass())->process($container); + + $methodCalls = $container->getDefinition('setter_injection')->getMethodCalls(); + + $this->assertEquals( + array('setWithCallsConfigured', 'setFoo', 'setDependencies', 'setChildMethodWithoutDocBlock'), + array_column($methodCalls, 0) + ); + + // test setWithCallsConfigured args + $this->assertEquals( + array('manual_arg1', 'manual_arg2'), + $methodCalls[0][1] + ); + // test setFoo args + $this->assertEquals(array(), $methodCalls[1][1]); + } + + public function testExplicitMethodInjection() + { + $container = new ContainerBuilder(); + $container->register(Foo::class); + $container->register(A::class); + $container->register(CollisionA::class); + $container->register(CollisionB::class); + + $container + ->register('setter_injection', SetterInjection::class) + ->setAutowired(true) + ->addMethodCall('notASetter', array()); + + (new ResolveClassPass())->process($container); + (new AutowireRequiredMethodsPass())->process($container); + + $methodCalls = $container->getDefinition('setter_injection')->getMethodCalls(); + + $this->assertEquals( + array('notASetter', 'setFoo', 'setDependencies', 'setWithCallsConfigured', 'setChildMethodWithoutDocBlock'), + array_column($methodCalls, 0) + ); + $this->assertEquals(array(), $methodCalls[0][1]); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php index f7e29d2110..16e486afaf 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php @@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\BoundArgument; +use Symfony\Component\DependencyInjection\Compiler\AutowireRequiredMethodsPass; use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -20,6 +21,8 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy; use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; use Symfony\Component\DependencyInjection\TypedReference; +require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php'; + class ResolveBindingsPassTest extends TestCase { public function testProcess() @@ -79,4 +82,17 @@ class ResolveBindingsPassTest extends TestCase $this->assertEquals(array($typedRef), $container->getDefinition('def1')->getArguments()); $this->assertEquals(array(new Reference('foo')), $container->getDefinition('def2')->getArguments()); } + + public function testScalarSetter() + { + $container = new ContainerBuilder(); + + $definition = $container->autowire('foo', ScalarSetter::class); + $definition->setBindings(array('$defaultLocale' => 'fr')); + + (new AutowireRequiredMethodsPass())->process($container); + (new ResolveBindingsPass())->process($container); + + $this->assertEquals(array(array('setDefaultLocale', array('fr'))), $definition->getMethodCalls()); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php new file mode 100644 index 0000000000..bf99eff6a2 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php @@ -0,0 +1,343 @@ + prefix is on purpose */ + public function setWithCallsConfigured(A $a) + { + } + + /** @required */ + public function setChildMethodWithoutDocBlock(A $a) + { + } +} + +class NotWireable +{ + public function setNotAutowireable(NotARealClass $n) + { + } + + public function setBar() + { + } + + public function setOptionalNotAutowireable(NotARealClass $n = null) + { + } + + public function setDifferentNamespace(\stdClass $n) + { + } + + public function setOptionalNoTypeHint($foo = null) + { + } + + public function setOptionalArgNoAutowireable($other = 'default_val') + { + } + + /** @required */ + protected function setProtectedMethod(A $a) + { + } +} + +class PrivateConstructor +{ + private function __construct() + { + } +} + +class ScalarSetter +{ + /** + * @required + */ + public function setDefaultLocale($defaultLocale) + { + } +} From a85b37a0e68e4a5b23113f41f79d6a88b53a61b8 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Fri, 22 Sep 2017 13:54:05 -0400 Subject: [PATCH 11/13] Adding Definition::addError() and a compiler pass to throw errors as exceptions --- .../Compiler/AbstractRecursivePass.php | 3 ++ .../Compiler/AutowireExceptionPass.php | 4 ++ .../Compiler/AutowirePass.php | 7 ++- .../Compiler/CheckArgumentsValidityPass.php | 35 ++++++++++-- .../Compiler/DefinitionErrorExceptionPass.php | 39 ++++++++++++++ .../Compiler/InlineServiceDefinitionsPass.php | 4 ++ .../Compiler/PassConfig.php | 8 +-- .../DependencyInjection/Definition.php | 21 ++++++++ .../Compiler/AutowireExceptionPassTest.php | 3 ++ .../Tests/Compiler/AutowirePassTest.php | 3 ++ .../CheckArgumentsValidityPassTest.php | 11 ++++ .../DefinitionErrorExceptionPassTest.php | 53 +++++++++++++++++++ .../InlineServiceDefinitionsPassTest.php | 3 ++ .../Tests/DefinitionTest.php | 9 ++++ 14 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Compiler/DefinitionErrorExceptionPass.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionErrorExceptionPassTest.php diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php index bbe869b935..0851d33e7e 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php @@ -22,6 +22,9 @@ use Symfony\Component\DependencyInjection\Reference; */ abstract class AbstractRecursivePass implements CompilerPassInterface { + /** + * @var ContainerBuilder + */ protected $container; protected $currentId; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowireExceptionPass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowireExceptionPass.php index 12be3d915f..f01617dd63 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowireExceptionPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowireExceptionPass.php @@ -11,11 +11,15 @@ namespace Symfony\Component\DependencyInjection\Compiler; +@trigger_error('The '.__NAMESPACE__.'\AutowireExceptionPass class is deprecated since version 3.4 and will be removed in 4.0. Use the DefinitionErrorExceptionPass class instead.', E_USER_DEPRECATED); + use Symfony\Component\DependencyInjection\ContainerBuilder; /** * Throws autowire exceptions from AutowirePass for definitions that still exist. * + * @deprecated since version 3.4, will be removed in 4.0. + * * @author Ryan Weaver */ class AutowireExceptionPass implements CompilerPassInterface diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index 8cb1978ef6..607b70c85f 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -36,7 +36,7 @@ class AutowirePass extends AbstractRecursivePass private $autowiringExceptions = array(); /** - * @param bool $throwOnAutowireException If false, retrieved errors via getAutowiringExceptions + * @param bool $throwOnAutowireException Errors can be retrieved via Definition::getErrors() */ public function __construct($throwOnAutowireException = true) { @@ -44,10 +44,14 @@ class AutowirePass extends AbstractRecursivePass } /** + * @deprecated since version 3.4, to be removed in 4.0. + * * @return AutowiringFailedException[] */ public function getAutowiringExceptions() { + @trigger_error('Calling AutowirePass::getAutowiringExceptions() is deprecated since Symfony 3.4 and will be removed in 4.0. Use Definition::getErrors() instead.', E_USER_DEPRECATED); + return $this->autowiringExceptions; } @@ -106,6 +110,7 @@ class AutowirePass extends AbstractRecursivePass } $this->autowiringExceptions[] = $e; + $this->container->getDefinition($this->currentId)->addError($e->getMessage()); return parent::processValue($value, $isRoot); } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckArgumentsValidityPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckArgumentsValidityPass.php index 6b48a15691..7f032058ab 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckArgumentsValidityPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckArgumentsValidityPass.php @@ -22,6 +22,13 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException; */ class CheckArgumentsValidityPass extends AbstractRecursivePass { + private $throwExceptions; + + public function __construct($throwExceptions = true) + { + $this->throwExceptions = $throwExceptions; + } + /** * {@inheritdoc} */ @@ -35,10 +42,20 @@ class CheckArgumentsValidityPass extends AbstractRecursivePass foreach ($value->getArguments() as $k => $v) { if ($k !== $i++) { if (!is_int($k)) { - throw new RuntimeException(sprintf('Invalid constructor argument for service "%s": integer expected but found string "%s". Check your service definition.', $this->currentId, $k)); + $msg = sprintf('Invalid constructor argument for service "%s": integer expected but found string "%s". Check your service definition.', $this->currentId, $k); + $value->addError($msg); + if ($this->throwExceptions) { + throw new RuntimeException($msg); + } + + break; } - throw new RuntimeException(sprintf('Invalid constructor argument %d for service "%s": argument %d must be defined before. Check your service definition.', 1 + $k, $this->currentId, $i)); + $msg = sprintf('Invalid constructor argument %d for service "%s": argument %d must be defined before. Check your service definition.', 1 + $k, $this->currentId, $i); + $value->addError($msg); + if ($this->throwExceptions) { + throw new RuntimeException($msg); + } } } @@ -47,10 +64,20 @@ class CheckArgumentsValidityPass extends AbstractRecursivePass foreach ($methodCall[1] as $k => $v) { if ($k !== $i++) { if (!is_int($k)) { - throw new RuntimeException(sprintf('Invalid argument for method call "%s" of service "%s": integer expected but found string "%s". Check your service definition.', $methodCall[0], $this->currentId, $k)); + $msg = sprintf('Invalid argument for method call "%s" of service "%s": integer expected but found string "%s". Check your service definition.', $methodCall[0], $this->currentId, $k); + $value->addError($msg); + if ($this->throwExceptions) { + throw new RuntimeException($msg); + } + + break; } - throw new RuntimeException(sprintf('Invalid argument %d for method call "%s" of service "%s": argument %d must be defined before. Check your service definition.', 1 + $k, $methodCall[0], $this->currentId, $i)); + $msg = sprintf('Invalid argument %d for method call "%s" of service "%s": argument %d must be defined before. Check your service definition.', 1 + $k, $methodCall[0], $this->currentId, $i); + $value->addError($msg); + if ($this->throwExceptions) { + throw new RuntimeException($msg); + } } } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/DefinitionErrorExceptionPass.php b/src/Symfony/Component/DependencyInjection/Compiler/DefinitionErrorExceptionPass.php new file mode 100644 index 0000000000..69cd899f2a --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/DefinitionErrorExceptionPass.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; + +/** + * Throws an exception for any Definitions that have errors and still exist. + * + * @author Ryan Weaver + */ +class DefinitionErrorExceptionPass extends AbstractRecursivePass +{ + /** + * {@inheritdoc} + */ + protected function processValue($value, $isRoot = false) + { + if (!$value instanceof Definition || empty($value->getErrors())) { + return parent::processValue($value, $isRoot); + } + + // only show the first error so they user can focus on it + $errors = $value->getErrors(); + $message = reset($errors); + + throw new RuntimeException($message); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php index 73f75b30a9..05e7786798 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php @@ -38,10 +38,14 @@ class InlineServiceDefinitionsPass extends AbstractRecursivePass implements Repe * * The key is the inlined service id and its value is the list of services it was inlined into. * + * @deprecated since version 3.4, to be removed in 4.0. + * * @return array */ public function getInlinedServiceIds() { + @trigger_error('Calling InlineServiceDefinitionsPass::getInlinedServiceIds() is deprecated since Symfony 3.4 and will be removed in 4.0.', E_USER_DEPRECATED); + return $this->inlinedServiceIds; } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index 79792bee4a..1edc4c21b1 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -59,14 +59,14 @@ class PassConfig new RegisterServiceSubscribersPass(), new ResolveNamedArgumentsPass(), new ResolveBindingsPass(), - $autowirePass = new AutowirePass(false), + new AutowirePass(false), new ResolveServiceSubscribersPass(), new ResolveReferencesToAliasesPass(), new ResolveInvalidReferencesPass(), new AnalyzeServiceReferencesPass(true), new CheckCircularReferencesPass(), new CheckReferenceValidityPass(), - new CheckArgumentsValidityPass(), + new CheckArgumentsValidityPass(false), )); $this->removingPasses = array(array( @@ -75,11 +75,11 @@ class PassConfig new RemoveAbstractDefinitionsPass(), new RepeatedPass(array( new AnalyzeServiceReferencesPass(), - $inlinedServicePass = new InlineServiceDefinitionsPass(), + new InlineServiceDefinitionsPass(), new AnalyzeServiceReferencesPass(), new RemoveUnusedDefinitionsPass(), )), - new AutowireExceptionPass($autowirePass, $inlinedServicePass), + new DefinitionErrorExceptionPass(), new CheckExceptionOnInvalidReferenceBehaviorPass(), )); } diff --git a/src/Symfony/Component/DependencyInjection/Definition.php b/src/Symfony/Component/DependencyInjection/Definition.php index 34d6f46cac..464932fc0a 100644 --- a/src/Symfony/Component/DependencyInjection/Definition.php +++ b/src/Symfony/Component/DependencyInjection/Definition.php @@ -44,6 +44,7 @@ class Definition private $autowiringTypes = array(); private $changes = array(); private $bindings = array(); + private $errors = array(); protected $arguments = array(); @@ -959,4 +960,24 @@ class Definition return $this; } + + /** + * Add an error that occurred when building this Definition. + * + * @param string $error + */ + public function addError($error) + { + $this->errors[] = $error; + } + + /** + * Returns any errors that occurred while building this Definition. + * + * @return array + */ + public function getErrors() + { + return $this->errors; + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowireExceptionPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowireExceptionPassTest.php index 4f0b3d6c25..a9c3445cef 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowireExceptionPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowireExceptionPassTest.php @@ -18,6 +18,9 @@ use Symfony\Component\DependencyInjection\Compiler\InlineServiceDefinitionsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException; +/** + * @group legacy + */ class AutowireExceptionPassTest extends TestCase { public function testThrowsException() diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index 94666df608..3a48cfae8d 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -157,6 +157,9 @@ class AutowirePassTest extends TestCase $this->assertEquals(DInterface::class, (string) $container->getDefinition('h')->getArgument(1)); } + /** + * @group legacy + */ public function testExceptionsAreStored() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckArgumentsValidityPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckArgumentsValidityPassTest.php index 891acbeee0..d121689ff9 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckArgumentsValidityPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckArgumentsValidityPassTest.php @@ -64,4 +64,15 @@ class CheckArgumentsValidityPassTest extends TestCase array(array(), array(array('baz', array(1 => 1)))), ); } + + public function testNoException() + { + $container = new ContainerBuilder(); + $definition = $container->register('foo'); + $definition->setArguments(array(null, 'a' => 'a')); + + $pass = new CheckArgumentsValidityPass(false); + $pass->process($container); + $this->assertCount(1, $definition->getErrors()); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionErrorExceptionPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionErrorExceptionPassTest.php new file mode 100644 index 0000000000..e0585e2132 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionErrorExceptionPassTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Compiler\DefinitionErrorExceptionPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; + +class DefinitionErrorExceptionPassTest extends TestCase +{ + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Things went wrong! + */ + public function testThrowsException() + { + $container = new ContainerBuilder(); + $def = new Definition(); + $def->addError('Things went wrong!'); + $def->addError('Now something else!'); + $container->register('foo_service_id') + ->setArguments(array( + $def, + )); + + $pass = new DefinitionErrorExceptionPass(); + $pass->process($container); + } + + public function testNoExceptionThrown() + { + $container = new ContainerBuilder(); + $def = new Definition(); + $container->register('foo_service_id') + ->setArguments(array( + $def, + )); + + $pass = new DefinitionErrorExceptionPass(); + $pass->process($container); + $this->assertSame($def, $container->getDefinition('foo_service_id')->getArgument(0)); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php index 26b24fa713..6154080c8b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php @@ -252,6 +252,9 @@ class InlineServiceDefinitionsPassTest extends TestCase $this->assertSame('inline', (string) $values[0]); } + /** + * @group legacy + */ public function testGetInlinedServiceIdData() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php b/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php index 2821dc17c0..5fe4236ad9 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php @@ -387,4 +387,13 @@ class DefinitionTest extends TestCase $def->setAutoconfigured(true); $this->assertTrue($def->isAutoconfigured()); } + + public function testAddError() + { + $def = new Definition('stdClass'); + $this->assertEmpty($def->getErrors()); + $def->addError('First error'); + $def->addError('Second error'); + $this->assertSame(array('First error', 'Second error'), $def->getErrors()); + } } From 9becb8ae34b70208f0f47968309ad6612243bd3d Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 19 Sep 2017 10:04:34 +0100 Subject: [PATCH 12/13] [Yaml] support parsing files --- src/Symfony/Component/Yaml/CHANGELOG.md | 2 + src/Symfony/Component/Yaml/Inline.php | 56 ++++++++-------- src/Symfony/Component/Yaml/Parser.php | 64 ++++++++++++++----- .../Yaml/Tests/Fixtures/not_readable.yml | 18 ++++++ .../Component/Yaml/Tests/InlineTest.php | 23 +++++-- .../Component/Yaml/Tests/ParserTest.php | 41 +++++++++++- src/Symfony/Component/Yaml/Yaml.php | 23 +++++++ 7 files changed, 177 insertions(+), 50 deletions(-) create mode 100644 src/Symfony/Component/Yaml/Tests/Fixtures/not_readable.yml diff --git a/src/Symfony/Component/Yaml/CHANGELOG.md b/src/Symfony/Component/Yaml/CHANGELOG.md index cdd49352d5..1b07b239cd 100644 --- a/src/Symfony/Component/Yaml/CHANGELOG.md +++ b/src/Symfony/Component/Yaml/CHANGELOG.md @@ -4,6 +4,8 @@ CHANGELOG 3.4.0 ----- + * added support for parsing YAML files using the `Yaml::parseFile()` or `Parser::parseFile()` method + * the `Dumper`, `Parser`, and `Yaml` classes are marked as final * Deprecated the `!php/object:` tag which will be replaced by the diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index 4410d221c7..b1653137fc 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -26,7 +26,8 @@ class Inline { const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+)"|\'([^\']*+(?:\'\'[^\']*+)*+)\')'; - public static $parsedLineNumber; + public static $parsedLineNumber = -1; + public static $parsedFilename; private static $exceptionOnInvalidType = false; private static $objectSupport = false; @@ -34,19 +35,18 @@ class Inline private static $constantSupport = false; /** - * @param int $flags - * @param int|null $parsedLineNumber + * @param int $flags + * @param int|null $parsedLineNumber + * @param string|null $parsedFilename */ - public static function initialize($flags, $parsedLineNumber = null) + public static function initialize($flags, $parsedLineNumber = null, $parsedFilename = null) { self::$exceptionOnInvalidType = (bool) (Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE & $flags); self::$objectSupport = (bool) (Yaml::PARSE_OBJECT & $flags); self::$objectForMap = (bool) (Yaml::PARSE_OBJECT_FOR_MAP & $flags); self::$constantSupport = (bool) (Yaml::PARSE_CONSTANT & $flags); - - if (null !== $parsedLineNumber) { - self::$parsedLineNumber = $parsedLineNumber; - } + self::$parsedFilename = $parsedFilename; + self::$parsedLineNumber = null !== $parsedLineNumber ? $parsedLineNumber : -1; } /** @@ -128,7 +128,7 @@ class Inline // some comments are allowed at the end if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) { - throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i))); + throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i)), self::$parsedLineNumber, $value, self::$parsedFilename); } if (isset($mbEncoding)) { @@ -322,7 +322,7 @@ class Inline if (null !== $delimiters) { $tmp = ltrim(substr($scalar, $i), ' '); if (!in_array($tmp[0], $delimiters)) { - throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i))); + throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i)), self::$parsedLineNumber, $scalar, self::$parsedFilename); } } } else { @@ -339,12 +339,12 @@ class Inline $output = $match[1]; $i += strlen($output); } else { - throw new ParseException(sprintf('Malformed inline YAML string: %s.', $scalar)); + throw new ParseException(sprintf('Malformed inline YAML string: %s.', $scalar), self::$parsedLineNumber, null, self::$parsedFilename); } // a non-quoted string cannot start with @ or ` (reserved) nor with a scalar indicator (| or >) if ($output && ('@' === $output[0] || '`' === $output[0] || '|' === $output[0] || '>' === $output[0])) { - throw new ParseException(sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $output[0])); + throw new ParseException(sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $output[0]), self::$parsedLineNumber, $output, self::$parsedFilename); } if ($output && '%' === $output[0]) { @@ -372,7 +372,7 @@ class Inline private static function parseQuotedScalar($scalar, &$i) { if (!Parser::preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) { - throw new ParseException(sprintf('Malformed inline YAML string: %s.', substr($scalar, $i))); + throw new ParseException(sprintf('Malformed inline YAML string: %s.', substr($scalar, $i)), self::$parsedLineNumber, $scalar, self::$parsedFilename); } $output = substr($match[0], 1, strlen($match[0]) - 2); @@ -455,7 +455,7 @@ class Inline ++$i; } - throw new ParseException(sprintf('Malformed inline YAML string: %s.', $sequence)); + throw new ParseException(sprintf('Malformed inline YAML string: %s.', $sequence), self::$parsedLineNumber, null, self::$parsedFilename); } /** @@ -572,7 +572,7 @@ class Inline } } - throw new ParseException(sprintf('Malformed inline YAML string: %s.', $mapping)); + throw new ParseException(sprintf('Malformed inline YAML string: %s.', $mapping), self::$parsedLineNumber, null, self::$parsedFilename); } /** @@ -600,11 +600,11 @@ class Inline // an unquoted * if (false === $value || '' === $value) { - throw new ParseException('A reference must contain at least one character.'); + throw new ParseException('A reference must contain at least one character.', self::$parsedLineNumber, $value, self::$parsedFilename); } if (!array_key_exists($value, $references)) { - throw new ParseException(sprintf('Reference "%s" does not exist.', $value)); + throw new ParseException(sprintf('Reference "%s" does not exist.', $value), self::$parsedLineNumber, $value, self::$parsedFilename); } return $references[$value]; @@ -639,7 +639,7 @@ class Inline } if (self::$exceptionOnInvalidType) { - throw new ParseException('Object support when parsing a YAML file has been disabled.'); + throw new ParseException('Object support when parsing a YAML file has been disabled.', self::$parsedLineNumber, $scalar, self::$parsedFilename); } return; @@ -651,7 +651,7 @@ class Inline } if (self::$exceptionOnInvalidType) { - throw new ParseException('Object support when parsing a YAML file has been disabled.'); + throw new ParseException('Object support when parsing a YAML file has been disabled.', self::$parsedLineNumber, $scalar, self::$parsedFilename); } return; @@ -661,7 +661,7 @@ class Inline } if (self::$exceptionOnInvalidType) { - throw new ParseException('Object support when parsing a YAML file has been disabled.'); + throw new ParseException('Object support when parsing a YAML file has been disabled.', self::$parsedLineNumber, $scalar, self::$parsedFilename); } return; @@ -673,10 +673,10 @@ class Inline return constant($const); } - throw new ParseException(sprintf('The constant "%s" is not defined.', $const)); + throw new ParseException(sprintf('The constant "%s" is not defined.', $const), self::$parsedLineNumber, $scalar, self::$parsedFilename); } if (self::$exceptionOnInvalidType) { - throw new ParseException(sprintf('The string "%s" could not be parsed as a constant. Have you forgotten to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar)); + throw new ParseException(sprintf('The string "%s" could not be parsed as a constant. Have you forgotten to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar), self::$parsedLineNumber, $scalar, self::$parsedFilename); } return; @@ -686,10 +686,10 @@ class Inline return constant($const); } - throw new ParseException(sprintf('The constant "%s" is not defined.', $const)); + throw new ParseException(sprintf('The constant "%s" is not defined.', $const), self::$parsedLineNumber, $scalar, self::$parsedFilename); } if (self::$exceptionOnInvalidType) { - throw new ParseException(sprintf('The string "%s" could not be parsed as a constant. Have you forgotten to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar)); + throw new ParseException(sprintf('The string "%s" could not be parsed as a constant. Have you forgotten to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar), self::$parsedLineNumber, $scalar, self::$parsedFilename); } return; @@ -781,7 +781,7 @@ class Inline // Built-in tags if ($tag && '!' === $tag[0]) { - throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag)); + throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), self::$parsedLineNumber, $value, self::$parsedFilename); } if (Yaml::PARSE_CUSTOM_TAGS & $flags) { @@ -790,7 +790,7 @@ class Inline return $tag; } - throw new ParseException(sprintf('Tags support is not enabled. Enable the `Yaml::PARSE_CUSTOM_TAGS` flag to use "!%s".', $tag)); + throw new ParseException(sprintf('Tags support is not enabled. Enable the `Yaml::PARSE_CUSTOM_TAGS` flag to use "!%s".', $tag), self::$parsedLineNumber, $value, self::$parsedFilename); } /** @@ -805,11 +805,11 @@ class Inline $parsedBinaryData = self::parseScalar(preg_replace('/\s/', '', $scalar)); if (0 !== (strlen($parsedBinaryData) % 4)) { - throw new ParseException(sprintf('The normalized base64 encoded data (data without whitespace characters) length must be a multiple of four (%d bytes given).', strlen($parsedBinaryData))); + throw new ParseException(sprintf('The normalized base64 encoded data (data without whitespace characters) length must be a multiple of four (%d bytes given).', strlen($parsedBinaryData)), self::$parsedLineNumber, $scalar, self::$parsedFilename); } if (!Parser::preg_match('#^[A-Z0-9+/]+={0,2}$#i', $parsedBinaryData)) { - throw new ParseException(sprintf('The base64 encoded data (%s) contains invalid characters.', $parsedBinaryData)); + throw new ParseException(sprintf('The base64 encoded data (%s) contains invalid characters.', $parsedBinaryData), self::$parsedLineNumber, $scalar, self::$parsedFilename); } return base64_decode($parsedBinaryData, true); diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index 49eae8f0cc..be1c13cddb 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -26,6 +26,7 @@ class Parser const TAG_PATTERN = '(?P![\w!.\/:-]+)'; const BLOCK_SCALAR_HEADER_PATTERN = '(?P\||>)(?P\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P +#.*)?'; + private $filename; private $offset = 0; private $totalNumberOfLines; private $lines = array(); @@ -50,6 +51,35 @@ class Parser } } + /** + * Parses a YAML file into a PHP value. + * + * @param string $filename The path to the YAML file to be parsed + * @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior + * + * @return mixed The YAML converted to a PHP value + * + * @throws ParseException If the file could not be read or the YAML is not valid + */ + public function parseFile($filename, $flags = 0) + { + if (!is_file($filename)) { + throw new ParseException(sprintf('File "%s" does not exist.', $filename)); + } + + if (!is_readable($filename)) { + throw new ParseException(sprintf('File "%s" cannot be read.', $filename)); + } + + $this->filename = $filename; + + try { + return $this->parse(file_get_contents($filename), $flags); + } finally { + $this->filename = null; + } + } + /** * Parses a YAML string to a PHP value. * @@ -93,7 +123,7 @@ class Parser } if (false === preg_match('//u', $value)) { - throw new ParseException('The YAML value does not appear to be valid UTF-8.'); + throw new ParseException('The YAML value does not appear to be valid UTF-8.', -1, null, $this->filename); } $this->refs = array(); @@ -168,13 +198,13 @@ class Parser // tab? if ("\t" === $this->currentLine[0]) { - throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine); + throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } $isRef = $mergeNode = false; if (self::preg_match('#^\-((?P\s+)(?P.+))?$#u', rtrim($this->currentLine), $values)) { if ($context && 'mapping' == $context) { - throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine); + throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } $context = 'sequence'; @@ -218,11 +248,11 @@ class Parser && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'"))) ) { if ($context && 'sequence' == $context) { - throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine); + throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine, $this->filename); } $context = 'mapping'; - Inline::initialize($flags, $this->getRealCurrentLineNb()); + Inline::initialize($flags, $this->getRealCurrentLineNb(), $this->filename); try { $i = 0; $evaluateKey = !(Yaml::PARSE_KEYS_AS_STRINGS & $flags); @@ -256,13 +286,13 @@ class Parser if (isset($values['value']) && 0 === strpos($values['value'], '*')) { $refName = substr(rtrim($values['value']), 1); if (!array_key_exists($refName, $this->refs)) { - throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine); + throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } $refValue = $this->refs[$refName]; if (!is_array($refValue)) { - throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine); + throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } $data += $refValue; // array union @@ -275,7 +305,7 @@ class Parser $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $flags); if (!is_array($parsed)) { - throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine); + throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } if (isset($parsed[0])) { @@ -284,7 +314,7 @@ class Parser // in the sequence override keys specified in later mapping nodes. foreach ($parsed as $parsedItem) { if (!is_array($parsedItem)) { - throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem); + throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem, $this->filename); } $data += $parsedItem; // array union @@ -350,7 +380,7 @@ class Parser } else { // multiple documents are not supported if ('---' === $this->currentLine) { - throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine); + throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine, $this->filename); } if (isset($this->currentLine[1]) && '?' === $this->currentLine[0] && ' ' === $this->currentLine[1]) { @@ -412,7 +442,7 @@ class Parser } } - throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine); + throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } } while ($this->moveToNextLine()); @@ -515,7 +545,7 @@ class Parser $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem(); if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) { - throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine); + throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } } else { $newIndent = $indentation; @@ -594,7 +624,7 @@ class Parser break; } else { - throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine); + throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } } @@ -654,7 +684,7 @@ class Parser } if (!array_key_exists($value, $this->refs)) { - throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine); + throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine, $this->filename); } return $this->refs[$value]; @@ -704,7 +734,7 @@ class Parser $parsedValue = Inline::parse($value, $flags, $this->refs); if ('mapping' === $context && is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) { - throw new ParseException('A colon cannot be used in an unquoted mapping value.'); + throw new ParseException('A colon cannot be used in an unquoted mapping value.', $this->getRealCurrentLineNb() + 1, $value, $this->filename); } return $parsedValue; @@ -1044,13 +1074,13 @@ class Parser // Built-in tags if ($tag && '!' === $tag[0]) { - throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag)); + throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), $this->getRealCurrentLineNb() + 1, $value, $this->filename); } if (Yaml::PARSE_CUSTOM_TAGS & $flags) { return $tag; } - throw new ParseException(sprintf('Tags support is not enabled. You must use the flag `Yaml::PARSE_CUSTOM_TAGS` to use "%s".', $matches['tag'])); + throw new ParseException(sprintf('Tags support is not enabled. You must use the flag `Yaml::PARSE_CUSTOM_TAGS` to use "%s".', $matches['tag']), $this->getRealCurrentLineNb() + 1, $value, $this->filename); } } diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/not_readable.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/not_readable.yml new file mode 100644 index 0000000000..3216a89ebb --- /dev/null +++ b/src/Symfony/Component/Yaml/Tests/Fixtures/not_readable.yml @@ -0,0 +1,18 @@ +- escapedCharacters +- sfComments +- sfCompact +- sfTests +- sfObjects +- sfMergeKey +- sfQuotes +- YtsAnchorAlias +- YtsBasicTests +- YtsBlockMapping +- YtsDocumentSeparator +- YtsErrorTests +- YtsFlowCollections +- YtsFoldedScalars +- YtsNullsAndEmpties +- YtsSpecificationExamples +- YtsTypeTransfers +- unindentedCollections diff --git a/src/Symfony/Component/Yaml/Tests/InlineTest.php b/src/Symfony/Component/Yaml/Tests/InlineTest.php index 2933ee67ce..5fdf2957fb 100644 --- a/src/Symfony/Component/Yaml/Tests/InlineTest.php +++ b/src/Symfony/Component/Yaml/Tests/InlineTest.php @@ -18,6 +18,11 @@ use Symfony\Component\Yaml\Yaml; class InlineTest extends TestCase { + protected function setUp() + { + Inline::initialize(0); + } + /** * @dataProvider getTestsForParse */ @@ -283,11 +288,16 @@ class InlineTest extends TestCase /** * @dataProvider getReservedIndicators - * @expectedException \Symfony\Component\Yaml\Exception\ParseException - * @expectedExceptionMessage cannot start a plain scalar; you need to quote the scalar. */ public function testParseUnquotedScalarStartingWithReservedIndicator($indicator) { + if (method_exists($this, 'expectExceptionMessage')) { + $this->expectException(ParseException::class); + $this->expectExceptionMessage(sprintf('cannot start a plain scalar; you need to quote the scalar (near "%sfoo ").', $indicator)); + } else { + $this->setExpectedException(ParseException::class, sprintf('cannot start a plain scalar; you need to quote the scalar (near "%sfoo ").', $indicator)); + } + Inline::parse(sprintf('{ foo: %sfoo }', $indicator)); } @@ -298,11 +308,16 @@ class InlineTest extends TestCase /** * @dataProvider getScalarIndicators - * @expectedException \Symfony\Component\Yaml\Exception\ParseException - * @expectedExceptionMessage cannot start a plain scalar; you need to quote the scalar. */ public function testParseUnquotedScalarStartingWithScalarIndicator($indicator) { + if (method_exists($this, 'expectExceptionMessage')) { + $this->expectException(ParseException::class); + $this->expectExceptionMessage(sprintf('cannot start a plain scalar; you need to quote the scalar (near "%sfoo ").', $indicator)); + } else { + $this->setExpectedException(ParseException::class, sprintf('cannot start a plain scalar; you need to quote the scalar (near "%sfoo ").', $indicator)); + } + Inline::parse(sprintf('{ foo: %sfoo }', $indicator)); } diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index 59e7b4986a..553b762384 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -30,6 +30,8 @@ class ParserTest extends TestCase protected function tearDown() { $this->parser = null; + + chmod(__DIR__.'/Fixtures/not_readable.yml', 0644); } /** @@ -1696,7 +1698,7 @@ YAML /** * @expectedException \Symfony\Component\Yaml\Exception\ParseException - * @expectedExceptionMessage The built-in tag "!!foo" is not implemented. + * @expectedExceptionMessage The built-in tag "!!foo" is not implemented at line 1 (near "!!foo"). */ public function testExceptionWhenUsingUnsuportedBuiltInTags() { @@ -1903,6 +1905,43 @@ YAML; $this->assertSame($expected, $this->parser->parse($yaml, Yaml::PARSE_CONSTANT | Yaml::PARSE_KEYS_AS_STRINGS)); } + + public function testFilenamesAreParsedAsStringsWithoutFlag() + { + $file = __DIR__.'/Fixtures/index.yml'; + + $this->assertSame($file, $this->parser->parse($file)); + } + + public function testParseFile() + { + $this->assertInternalType('array', $this->parser->parseFile(__DIR__.'/Fixtures/index.yml')); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + * @expectedExceptionMessageRegExp #^File ".+/Fixtures/nonexistent.yml" does not exist\.$# + */ + public function testParsingNonExistentFilesThrowsException() + { + $this->parser->parseFile(__DIR__.'/Fixtures/nonexistent.yml'); + } + + /** + * @expectedException \Symfony\Component\Yaml\Exception\ParseException + * @expectedExceptionMessageRegExp #^File ".+/Fixtures/not_readable.yml" cannot be read\.$# + */ + public function testParsingNotReadableFilesThrowsException() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('chmod is not supported on Windows'); + } + + $file = __DIR__.'/Fixtures/not_readable.yml'; + chmod($file, 0200); + + $this->parser->parseFile($file); + } } class B diff --git a/src/Symfony/Component/Yaml/Yaml.php b/src/Symfony/Component/Yaml/Yaml.php index 52a064f70b..24e3e95cba 100644 --- a/src/Symfony/Component/Yaml/Yaml.php +++ b/src/Symfony/Component/Yaml/Yaml.php @@ -39,6 +39,29 @@ class Yaml */ const PARSE_KEYS_AS_STRINGS = 2048; + /** + * Parses a YAML file into a PHP value. + * + * Usage: + * + * $array = Yaml::parseFile('config.yml'); + * print_r($array); + * + * + * @param string $filename The path to the YAML file to be parsed + * @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior + * + * @return mixed The YAML converted to a PHP value + * + * @throws ParseException If the file could not be read or the YAML is not valid + */ + public static function parseFile($filename, $flags = 0) + { + $yaml = new Parser(); + + return $yaml->parseFile($filename, $flags); + } + /** * Parses YAML into a PHP value. * From 8bf465f48d3560f97b0a27b37222e812f6a3fd09 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 25 Sep 2017 21:59:34 +0200 Subject: [PATCH 13/13] use the parseFile() method of the YAML parser --- .../Component/DependencyInjection/Loader/YamlFileLoader.php | 2 +- src/Symfony/Component/Routing/Loader/YamlFileLoader.php | 2 +- src/Symfony/Component/Routing/composer.json | 4 ++-- .../Component/Serializer/Mapping/Loader/YamlFileLoader.php | 2 +- src/Symfony/Component/Translation/Loader/YamlFileLoader.php | 2 +- src/Symfony/Component/Translation/composer.json | 4 ++-- .../Component/Validator/Mapping/Loader/YamlFileLoader.php | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index be0c75b2ca..d9fd24c3d7 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -654,7 +654,7 @@ class YamlFileLoader extends FileLoader } try { - $configuration = $this->yamlParser->parse(file_get_contents($file), Yaml::PARSE_CONSTANT | Yaml::PARSE_CUSTOM_TAGS); + $configuration = $this->yamlParser->parseFile($file, Yaml::PARSE_CONSTANT | Yaml::PARSE_CUSTOM_TAGS); } catch (ParseException $e) { throw new InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $file), 0, $e); } diff --git a/src/Symfony/Component/Routing/Loader/YamlFileLoader.php b/src/Symfony/Component/Routing/Loader/YamlFileLoader.php index 5a08751321..f3072c927b 100644 --- a/src/Symfony/Component/Routing/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/YamlFileLoader.php @@ -58,7 +58,7 @@ class YamlFileLoader extends FileLoader } try { - $parsedConfig = $this->yamlParser->parse(file_get_contents($path)); + $parsedConfig = $this->yamlParser->parseFile($path); } catch (ParseException $e) { throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $path), 0, $e); } diff --git a/src/Symfony/Component/Routing/composer.json b/src/Symfony/Component/Routing/composer.json index 1226ffde88..2ad746ec53 100644 --- a/src/Symfony/Component/Routing/composer.json +++ b/src/Symfony/Component/Routing/composer.json @@ -21,7 +21,7 @@ "require-dev": { "symfony/config": "~2.8|~3.0|~4.0", "symfony/http-foundation": "~2.8|~3.0|~4.0", - "symfony/yaml": "~3.3|~4.0", + "symfony/yaml": "~3.4|~4.0", "symfony/expression-language": "~2.8|~3.0|~4.0", "symfony/dependency-injection": "~3.3|~4.0", "doctrine/annotations": "~1.0", @@ -31,7 +31,7 @@ "conflict": { "symfony/config": "<2.8", "symfony/dependency-injection": "<3.3", - "symfony/yaml": "<3.3" + "symfony/yaml": "<3.4" }, "suggest": { "symfony/http-foundation": "For using a Symfony Request object", diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php index e3afa47632..970241d347 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php @@ -113,7 +113,7 @@ class YamlFileLoader extends FileLoader $this->yamlParser = new Parser(); } - $classes = $this->yamlParser->parse(file_get_contents($this->file)); + $classes = $this->yamlParser->parseFile($this->file); if (empty($classes)) { return array(); diff --git a/src/Symfony/Component/Translation/Loader/YamlFileLoader.php b/src/Symfony/Component/Translation/Loader/YamlFileLoader.php index 41e390d0e9..874fa3a894 100644 --- a/src/Symfony/Component/Translation/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Translation/Loader/YamlFileLoader.php @@ -39,7 +39,7 @@ class YamlFileLoader extends FileLoader } try { - $messages = $this->yamlParser->parse(file_get_contents($resource)); + $messages = $this->yamlParser->parseFile($resource); } catch (ParseException $e) { throw new InvalidResourceException(sprintf('Error parsing YAML, invalid file "%s"', $resource), 0, $e); } diff --git a/src/Symfony/Component/Translation/composer.json b/src/Symfony/Component/Translation/composer.json index 48873b9826..3a1e1589f5 100644 --- a/src/Symfony/Component/Translation/composer.json +++ b/src/Symfony/Component/Translation/composer.json @@ -23,14 +23,14 @@ "symfony/config": "~2.8|~3.0|~4.0", "symfony/dependency-injection": "~3.4|~4.0", "symfony/intl": "^2.8.18|^3.2.5|~4.0", - "symfony/yaml": "~3.3|~4.0", + "symfony/yaml": "~3.4|~4.0", "symfony/finder": "~2.8|~3.0|~4.0", "psr/log": "~1.0" }, "conflict": { "symfony/config": "<2.8", "symfony/dependency-injection": "<3.4", - "symfony/yaml": "<3.3" + "symfony/yaml": "<3.4" }, "suggest": { "symfony/config": "", diff --git a/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php index ff6c65d05e..e5e84c38c3 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php @@ -116,7 +116,7 @@ class YamlFileLoader extends FileLoader private function parseFile($path) { try { - $classes = $this->yamlParser->parse(file_get_contents($path), Yaml::PARSE_CONSTANT); + $classes = $this->yamlParser->parseFile($path, Yaml::PARSE_CONSTANT); } catch (ParseException $e) { throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $path), 0, $e); }