Merge branch '4.3' into 4.4

* 4.3:
  [DI] Service locators can't be decorated
  [Console][SymfonyQuestionHelper] Handle multibytes question choices keys and custom prompt
  [DI] fix auto-binding service providers to their service subscribers
This commit is contained in:
Nicolas Grekas 2019-12-17 11:32:23 +01:00
commit 4b93c6210d
8 changed files with 172 additions and 19 deletions

View File

@ -166,15 +166,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[] = ' [<info>'.$key.str_repeat(' ', $width).'</info>] '.$value;
}
$output->writeln($messages);
$output->writeln(array_merge([
$question->getQuestion(),
], $this->formatChoiceQuestionChoices($question, 'info')));
$message = $question->getPrompt();
}
@ -182,6 +176,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</$tag>] %s", $key, $value);
}
return $messages;
}
/**
* Outputs an error message.
*/

View File

@ -68,15 +68,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(" [<comment>%-${width}s</comment>] %s", $key, $value));
}
if ($question instanceof ChoiceQuestion) {
$output->writeln($this->formatChoiceQuestionChoices($question, 'comment'));
$prompt = $question->getPrompt();
}
$output->write(' > ');
$output->write($prompt);
}
/**

View File

@ -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
qqq:
[foo ] foo
[żółw ] bar
[łabądź] baz
>
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(<<<EOT
qqq:
[0] foo
>ccc>
EOT
, $output);
}
protected function getInputStream($input)
{
$stream = fopen('php://memory', 'r+', false);

View File

@ -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(),

View File

@ -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);
}

View File

@ -25,6 +25,7 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\BarTagClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedForDefaultPriorityClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooTagClass;
use Symfony\Contracts\Service\ServiceProviderInterface;
/**
* This class tests the integration of the different compiler passes.
@ -143,6 +144,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
*/
@ -441,6 +465,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
{
}

View File

@ -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));
}
}

View File

@ -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(): array
{
return [