From 6c2ceb0c44ab4a6de36de31c4192e2aa30de4b30 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 16 Dec 2019 19:31:23 +0100 Subject: [PATCH 1/3] [DI] fix auto-binding service providers to their service subscribers --- .../RegisterServiceSubscribersPass.php | 12 +++++++- .../RegisterServiceSubscribersPassTest.php | 29 +++++++++++++++++++ .../Tests/Fixtures/TestServiceSubscriber.php | 5 ++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php b/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php index aeb9641811..14bf000863 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php @@ -11,11 +11,14 @@ namespace Symfony\Component\DependencyInjection\Compiler; +use Psr\Container\ContainerInterface as PsrContainerInterface; +use Symfony\Component\DependencyInjection\Argument\BoundArgument; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\TypedReference; +use Symfony\Contracts\Service\ServiceProviderInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface; /** @@ -105,7 +108,14 @@ class RegisterServiceSubscribersPass extends AbstractRecursivePass throw new InvalidArgumentException(sprintf('Service %s not exist in the map returned by "%s::getSubscribedServices()" for service "%s".', $message, $class, $this->currentId)); } - $value->addTag('container.service_subscriber.locator', ['id' => (string) ServiceLocatorTagPass::register($this->container, $subscriberMap, $this->currentId)]); + $locatorRef = ServiceLocatorTagPass::register($this->container, $subscriberMap, $this->currentId); + + $value->addTag('container.service_subscriber.locator', ['id' => (string) $locatorRef]); + + $value->setBindings([ + PsrContainerInterface::class => new BoundArgument($locatorRef, false), + ServiceProviderInterface::class => new BoundArgument($locatorRef, false), + ] + $value->getBindings()); return parent::processValue($value); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php index e78d468d66..212bd11ec0 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php @@ -16,6 +16,7 @@ use Psr\Container\ContainerInterface as PsrContainerInterface; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Compiler\AutowirePass; use Symfony\Component\DependencyInjection\Compiler\RegisterServiceSubscribersPass; +use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass; use Symfony\Component\DependencyInjection\Compiler\ResolveServiceSubscribersPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -235,4 +236,32 @@ class RegisterServiceSubscribersPassTest extends TestCase ]; $this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0)); } + + public function testBinding() + { + $container = new ContainerBuilder(); + + $container->register('foo', TestServiceSubscriber::class) + ->addMethodCall('setServiceProvider') + ->addTag('container.service_subscriber') + ; + + (new RegisterServiceSubscribersPass())->process($container); + (new ResolveBindingsPass())->process($container); + + $foo = $container->getDefinition('foo'); + $locator = $container->getDefinition((string) $foo->getMethodCalls()[0][1][0]); + + $this->assertFalse($locator->isPublic()); + $this->assertSame(ServiceLocator::class, $locator->getClass()); + + $expected = [ + TestServiceSubscriber::class => new ServiceClosureArgument(new TypedReference(TestServiceSubscriber::class, TestServiceSubscriber::class)), + CustomDefinition::class => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), + 'bar' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'bar')), + 'baz' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'baz')), + ]; + + $this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0)); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriber.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriber.php index 7fc3842af3..e040dec0a9 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriber.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriber.php @@ -2,6 +2,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Fixtures; +use Symfony\Contracts\Service\ServiceProviderInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface; class TestServiceSubscriber implements ServiceSubscriberInterface @@ -10,6 +11,10 @@ class TestServiceSubscriber implements ServiceSubscriberInterface { } + public function setServiceProvider(ServiceProviderInterface $container) + { + } + public static function getSubscribedServices() { return [ From f175032ed88a2393c17a8c971ca080ff4ab77d61 Mon Sep 17 00:00:00 2001 From: fancyweb Date: Mon, 16 Dec 2019 21:06:42 +0100 Subject: [PATCH 2/3] [Console][SymfonyQuestionHelper] Handle multibytes question choices keys and custom prompt Co-authored-by: Mikhail Fesenko --- .../Console/Helper/QuestionHelper.php | 32 ++++++++++---- .../Console/Helper/SymfonyQuestionHelper.php | 12 +++--- .../Helper/SymfonyQuestionHelperTest.php | 43 +++++++++++++++++++ 3 files changed, 72 insertions(+), 15 deletions(-) diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index 1f93c06451..93e221d36a 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -198,15 +198,9 @@ class QuestionHelper extends Helper $message = $question->getQuestion(); if ($question instanceof ChoiceQuestion) { - $maxWidth = max(array_map([$this, 'strlen'], array_keys($question->getChoices()))); - - $messages = (array) $question->getQuestion(); - foreach ($question->getChoices() as $key => $value) { - $width = $maxWidth - $this->strlen($key); - $messages[] = ' ['.$key.str_repeat(' ', $width).'] '.$value; - } - - $output->writeln($messages); + $output->writeln(array_merge([ + $question->getQuestion(), + ], $this->formatChoiceQuestionChoices($question, 'info'))); $message = $question->getPrompt(); } @@ -214,6 +208,26 @@ class QuestionHelper extends Helper $output->write($message); } + /** + * @param string $tag + * + * @return string[] + */ + protected function formatChoiceQuestionChoices(ChoiceQuestion $question, $tag) + { + $messages = []; + + $maxWidth = max(array_map('self::strlen', array_keys($choices = $question->getChoices()))); + + foreach ($choices as $key => $value) { + $padding = str_repeat(' ', $maxWidth - self::strlen($key)); + + $messages[] = sprintf(" [<$tag>%s$padding] %s", $key, $value); + } + + return $messages; + } + /** * Outputs an error message. */ diff --git a/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php b/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php index 5937741a2c..7cd050fee1 100644 --- a/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php @@ -96,15 +96,15 @@ class SymfonyQuestionHelper extends QuestionHelper $output->writeln($text); - if ($question instanceof ChoiceQuestion) { - $width = max(array_map('strlen', array_keys($question->getChoices()))); + $prompt = ' > '; - foreach ($question->getChoices() as $key => $value) { - $output->writeln(sprintf(" [%-${width}s] %s", $key, $value)); - } + if ($question instanceof ChoiceQuestion) { + $output->writeln($this->formatChoiceQuestionChoices($question, 'comment')); + + $prompt = $question->getPrompt(); } - $output->write(' > '); + $output->write($prompt); } /** diff --git a/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php index cbf3b957b3..b7a26f9508 100644 --- a/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php @@ -130,6 +130,49 @@ class SymfonyQuestionHelperTest extends AbstractQuestionHelperTest $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new Question('What\'s your name?')); } + public function testChoiceQuestionPadding() + { + $choiceQuestion = new ChoiceQuestion('qqq', [ + 'foo' => 'foo', + 'żółw' => 'bar', + 'łabądź' => 'baz', + ]); + + (new SymfonyQuestionHelper())->ask( + $this->createStreamableInputInterfaceMock($this->getInputStream("foo\n")), + $output = $this->createOutputInterface(), + $choiceQuestion + ); + + $this->assertOutputContains(<< +EOT + , $output); + } + + public function testChoiceQuestionCustomPrompt() + { + $choiceQuestion = new ChoiceQuestion('qqq', ['foo']); + $choiceQuestion->setPrompt(' >ccc> '); + + (new SymfonyQuestionHelper())->ask( + $this->createStreamableInputInterfaceMock($this->getInputStream("foo\n")), + $output = $this->createOutputInterface(), + $choiceQuestion + ); + + $this->assertOutputContains(<<ccc> +EOT + , $output); + } + protected function getInputStream($input) { $stream = fopen('php://memory', 'r+', false); From 343282b9d41757a010dbb948d770f8719b8ba1b9 Mon Sep 17 00:00:00 2001 From: Maciej Malarz Date: Mon, 23 Sep 2019 12:01:45 +0200 Subject: [PATCH 3/3] [DI] Service locators can't be decorated --- .../Compiler/PassConfig.php | 6 +-- .../Tests/Compiler/IntegrationTest.php | 52 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index 9db617520d..744645dd70 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -52,14 +52,14 @@ class PassConfig new ValidateEnvPlaceholdersPass(), new ResolveChildDefinitionsPass(), new RegisterServiceSubscribersPass(), - new DecoratorServicePass(), new ResolveParameterPlaceHoldersPass(false), - new ResolveFactoryClassPass(), new ResolveNamedArgumentsPass(), - new AutowireRequiredMethodsPass(), new ResolveBindingsPass(), new ServiceLocatorTagPass(), new CheckDefinitionValidityPass(), + new DecoratorServicePass(), + new ResolveFactoryClassPass(), + new AutowireRequiredMethodsPass(), new AutowirePass(false), new ResolveTaggedIteratorArgumentPass(), new ResolveServiceSubscribersPass(), diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php index 83d3e23d3b..a418d48897 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php @@ -24,6 +24,7 @@ use Symfony\Component\DependencyInjection\ServiceSubscriberInterface; use Symfony\Component\DependencyInjection\Tests\Fixtures\BarTagClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooTagClass; +use Symfony\Contracts\Service\ServiceProviderInterface; /** * This class tests the integration of the different compiler passes. @@ -142,6 +143,29 @@ class IntegrationTest extends TestCase $this->assertInstanceOf(DecoratedServiceSubscriber::class, $container->get(ServiceSubscriberStub::class)); } + public function testCanDecorateServiceLocator() + { + $container = new ContainerBuilder(); + + $container->register('foo', 'stdClass')->setPublic(true); + + $container->register(ServiceLocator::class) + ->addTag('container.service_locator') + ->setArguments([[new Reference('foo')]]) + ; + + $container->register(DecoratedServiceLocator::class) + ->setDecoratedService(ServiceLocator::class) + ->setPublic(true) + ->setArguments([new Reference(DecoratedServiceLocator::class.'.inner')]) + ; + + $container->compile(); + + $this->assertInstanceOf(DecoratedServiceLocator::class, $container->get(DecoratedServiceLocator::class)); + $this->assertSame($container->get('foo'), $container->get(DecoratedServiceLocator::class)->get('foo')); + } + /** * @dataProvider getYamlCompileTests */ @@ -416,6 +440,34 @@ class DecoratedServiceSubscriber { } +class DecoratedServiceLocator implements ServiceProviderInterface +{ + /** + * @var ServiceLocator + */ + private $locator; + + public function __construct(ServiceLocator $locator) + { + $this->locator = $locator; + } + + public function get($id) + { + return $this->locator->get($id); + } + + public function has($id): bool + { + return $this->locator->has($id); + } + + public function getProvidedServices(): array + { + return $this->locator->getProvidedServices(); + } +} + class IntegrationTestStub extends IntegrationTestStubParent { }