Merge branch '3.4'

* 3.4:
  Add TokenProcessor
  [DI] Handle root namespace in service definitions
  Add support for command lazy-loading
  Use rawurlencode() to transform the Cookie into a string
  [TwigBundle] Added a RuntimeExtensionInterface to take advantage of autoconfigure
  [Process] Fix parsing args on Windows
  Add exculde verbosity test
  [HttpKernel][VarDumper] Truncate profiler data & optim perf
  [DI] Allow imports in string format for YAML
  [Validator] Allow to use a property path to get value to compare in comparison constraints
  [Security] Fix authentication.failure event not dispatched on AccountStatusException
  add option to define the access decision manager
  Add support for doctrin/dbal 2.6 types
This commit is contained in:
Nicolas Grekas 2017-07-12 16:12:10 +02:00
commit bdaa7b118e
70 changed files with 1166 additions and 149 deletions

View File

@ -11,6 +11,11 @@ CHANGELOG
method throws an exception
* removed the `DoctrineParserCache` class
3.4.0
-----
* added support for doctrine/dbal v2.6 types
3.1.0
-----

View File

@ -60,6 +60,8 @@ class DoctrineOrmTypeGuesser implements FormTypeGuesserInterface
case Type::DATETIMETZ:
case 'vardatetime':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', array(), Guess::HIGH_CONFIDENCE);
case 'dateinterval':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateIntervalType', array(), Guess::HIGH_CONFIDENCE);
case Type::DATE:
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', array(), Guess::HIGH_CONFIDENCE);
case Type::TIME:

View File

@ -117,6 +117,15 @@ class DoctrineExtractor implements PropertyListExtractorInterface, PropertyTypeE
case DBALType::TIME:
return array(new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTime'));
case 'date_immutable':
case 'datetime_immutable':
case 'datetimetz_immutable':
case 'time_immutable':
return array(new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTimeImmutable'));
case 'dateinterval':
return array(new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateInterval'));
case DBALType::TARRAY:
return array(new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true));

View File

@ -48,6 +48,8 @@ class DoctrineExtractorTest extends TestCase
'id',
'guid',
'time',
'timeImmutable',
'dateInterval',
'json',
'simpleArray',
'float',
@ -78,6 +80,9 @@ class DoctrineExtractorTest extends TestCase
array('id', array(new Type(Type::BUILTIN_TYPE_INT))),
array('guid', array(new Type(Type::BUILTIN_TYPE_STRING))),
array('bigint', array(new Type(Type::BUILTIN_TYPE_STRING))),
array('time', array(new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTime'))),
array('timeImmutable', array(new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))),
array('dateInterval', array(new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateInterval'))),
array('float', array(new Type(Type::BUILTIN_TYPE_FLOAT))),
array('decimal', array(new Type(Type::BUILTIN_TYPE_STRING))),
array('bool', array(new Type(Type::BUILTIN_TYPE_BOOL))),

View File

@ -55,6 +55,16 @@ class DoctrineDummy
*/
private $time;
/**
* @Column(type="time_immutable")
*/
private $timeImmutable;
/**
* @Column(type="dateinterval")
*/
private $dateInterval;
/**
* @Column(type="json_array")
*/

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Monolog\Processor;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
/**
* Adds the current security token to the log entry.
*
* @author Dany Maillard <danymaillard93b@gmail.com>
*/
class TokenProcessor
{
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function __invoke(array $records)
{
$records['extra']['token'] = null;
if (null !== $token = $this->tokenStorage->getToken()) {
$records['extra']['token'] = array(
'username' => $token->getUsername(),
'authenticated' => $token->isAuthenticated(),
'roles' => array_map(function ($role) { return $role->getRole(); }, $token->getRoles()),
);
}
return $records;
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Monolog\Tests\Processor;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Monolog\Processor\TokenProcessor;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
/**
* Tests the TokenProcessor.
*
* @author Dany Maillard <danymaillard93b@gmail.com>
*/
class TokenProcessorTest extends TestCase
{
public function testProcessor()
{
$token = new UsernamePasswordToken('user', 'password', 'provider', array('ROLE_USER'));
$tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock();
$tokenStorage->method('getToken')->willReturn($token);
$processor = new TokenProcessor($tokenStorage);
$record = array('extra' => array());
$record = $processor($record);
$this->assertArrayHasKey('token', $record['extra']);
$this->assertEquals($token->getUsername(), $record['extra']['token']['username']);
$this->assertEquals($token->isAuthenticated(), $record['extra']['token']['authenticated']);
$roles = array_map(function ($role) { return $role->getRole(); }, $token->getRoles());
$this->assertEquals($roles, $record['extra']['token']['roles']);
}
}

View File

@ -23,6 +23,7 @@
"require-dev": {
"symfony/console": "~3.4|~4.0",
"symfony/event-dispatcher": "~3.4|~4.0",
"symfony/security-core": "~3.4|~4.0",
"symfony/var-dumper": "~3.4|~4.0"
},
"conflict": {

View File

@ -37,6 +37,8 @@ CHANGELOG
`Symfony\Component\Translation\DependencyInjection\TranslationExtractorPass` instead
* Deprecated `TranslatorPass`, use
`Symfony\Component\Translation\DependencyInjection\TranslatorPass` instead
* Added `command` attribute to the `console.command` tag which takes the command
name as value, using it makes the command lazy
3.3.0
-----

View File

@ -68,15 +68,7 @@ class Application extends BaseApplication
{
$this->kernel->boot();
$container = $this->kernel->getContainer();
foreach ($this->all() as $command) {
if ($command instanceof ContainerAwareInterface) {
$command->setContainer($container);
}
}
$this->setDispatcher($container->get('event_dispatcher'));
$this->setDispatcher($this->kernel->getContainer()->get('event_dispatcher'));
return parent::doRun($input, $output);
}
@ -98,7 +90,13 @@ class Application extends BaseApplication
{
$this->registerCommands();
return parent::get($name);
$command = parent::get($name);
if ($command instanceof ContainerAwareInterface) {
$command->setContainer($this->kernel->getContainer());
}
return $command;
}
/**
@ -144,9 +142,15 @@ class Application extends BaseApplication
}
}
if ($container->has('console.command_loader')) {
$this->setCommandLoader($container->get('console.command_loader'));
}
if ($container->hasParameter('console.command.ids')) {
foreach ($container->getParameter('console.command.ids') as $id) {
$this->add($container->get($id));
if (false !== $id) {
$this->add($container->get($id));
}
}
}
}

View File

@ -77,10 +77,15 @@ class RouterDebugCommandTest extends TestCase
$container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock();
$container
->expects($this->once())
->expects($this->atLeastOnce())
->method('has')
->with('router')
->will($this->returnValue(true))
->will($this->returnCallback(function ($id) {
if ('console.command_loader' === $id) {
return false;
}
return true;
}))
;
$container
->expects($this->any())

View File

@ -78,8 +78,14 @@ class RouterMatchCommandTest extends TestCase
$container
->expects($this->atLeastOnce())
->method('has')
->with('router')
->will($this->returnValue(true));
->will($this->returnCallback(function ($id) {
if ('console.command_loader' === $id) {
return false;
}
return true;
}))
;
$container
->expects($this->any())
->method('get')

View File

@ -59,6 +59,26 @@ class MainConfiguration implements ConfigurationInterface
$rootNode = $tb->root('security');
$rootNode
->beforeNormalization()
->ifTrue(function ($v) {
if (!isset($v['access_decision_manager'])) {
return true;
}
if (!isset($v['access_decision_manager']['strategy']) && !isset($v['access_decision_manager']['service'])) {
return true;
}
return false;
})
->then(function ($v) {
$v['access_decision_manager'] = array(
'strategy' => AccessDecisionManager::STRATEGY_AFFIRMATIVE,
);
return $v;
})
->end()
->children()
->scalarNode('access_denied_url')->defaultNull()->example('/foo/error403')->end()
->enumNode('session_fixation_strategy')
@ -73,11 +93,15 @@ class MainConfiguration implements ConfigurationInterface
->children()
->enumNode('strategy')
->values(array(AccessDecisionManager::STRATEGY_AFFIRMATIVE, AccessDecisionManager::STRATEGY_CONSENSUS, AccessDecisionManager::STRATEGY_UNANIMOUS))
->defaultValue(AccessDecisionManager::STRATEGY_AFFIRMATIVE)
->end()
->scalarNode('service')->end()
->booleanNode('allow_if_all_abstain')->defaultFalse()->end()
->booleanNode('allow_if_equal_granted_denied')->defaultTrue()->end()
->end()
->validate()
->ifTrue(function ($v) { return isset($v['strategy']) && isset($v['service']); })
->thenInvalid('"strategy" and "service" cannot be used together.')
->end()
->end()
->end()
;

View File

@ -83,12 +83,17 @@ class SecurityExtension extends Extension
$container->setParameter('security.access.denied_url', $config['access_denied_url']);
$container->setParameter('security.authentication.manager.erase_credentials', $config['erase_credentials']);
$container->setParameter('security.authentication.session_strategy.strategy', $config['session_fixation_strategy']);
$container
->getDefinition('security.access.decision_manager')
->addArgument($config['access_decision_manager']['strategy'])
->addArgument($config['access_decision_manager']['allow_if_all_abstain'])
->addArgument($config['access_decision_manager']['allow_if_equal_granted_denied'])
;
if (isset($config['access_decision_manager']['service'])) {
$container->setAlias('security.access.decision_manager', $config['access_decision_manager']['service']);
} else {
$container
->getDefinition('security.access.decision_manager')
->addArgument($config['access_decision_manager']['strategy'])
->addArgument($config['access_decision_manager']['allow_if_all_abstain'])
->addArgument($config['access_decision_manager']['allow_if_equal_granted_denied']);
}
$container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']);
$container->setParameter('security.authentication.hide_user_not_found', $config['hide_user_not_found']);

View File

@ -10,7 +10,7 @@
<service id="security.console.user_password_encoder_command" class="Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand">
<argument type="service" id="security.encoder_factory"/>
<argument type="collection" /> <!-- encoders' user classes -->
<tag name="console.command" />
<tag name="console.command" command="security:encode-password" />
</service>
</services>
</container>

View File

@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\Reference;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
abstract class CompleteConfigurationTest extends TestCase
{
@ -357,6 +358,29 @@ abstract class CompleteConfigurationTest extends TestCase
$this->assertTrue($this->getContainer('remember_me_options')->has('security.console.user_password_encoder_command'));
}
public function testDefaultAccessDecisionManagerStrategyIsAffirmative()
{
$container = $this->getContainer('access_decision_manager_default_strategy');
$this->assertSame(AccessDecisionManager::STRATEGY_AFFIRMATIVE, $container->getDefinition('security.access.decision_manager')->getArgument(1), 'Default vote strategy is affirmative');
}
public function testCustomAccessDecisionManagerService()
{
$container = $this->getContainer('access_decision_manager_service');
$this->assertSame('app.access_decision_manager', (string) $container->getAlias('security.access.decision_manager'), 'The custom access decision manager service is aliased');
}
/**
* @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
* @expectedExceptionMessage "strategy" and "service" cannot be used together.
*/
public function testAccessDecisionManagerServiceAndStrategyCannotBeUsedAtTheSameTime()
{
$container = $this->getContainer('access_decision_manager_service_and_strategy');
}
protected function getContainer($file)
{
$file = $file.'.'.$this->getFileExtension();

View File

@ -0,0 +1,16 @@
<?php
$container->loadFromExtension('security', array(
'providers' => array(
'default' => array(
'memory' => array(
'users' => array(
'foo' => array('password' => 'foo', 'roles' => 'ROLE_USER'),
),
),
),
),
'firewalls' => array(
'simple' => array('pattern' => '/login', 'security' => false),
),
));

View File

@ -0,0 +1,19 @@
<?php
$container->loadFromExtension('security', array(
'access_decision_manager' => array(
'service' => 'app.access_decision_manager',
),
'providers' => array(
'default' => array(
'memory' => array(
'users' => array(
'foo' => array('password' => 'foo', 'roles' => 'ROLE_USER'),
),
),
),
),
'firewalls' => array(
'simple' => array('pattern' => '/login', 'security' => false),
),
));

View File

@ -0,0 +1,20 @@
<?php
$container->loadFromExtension('security', array(
'access_decision_manager' => array(
'service' => 'app.access_decision_manager',
'strategy' => 'affirmative',
),
'providers' => array(
'default' => array(
'memory' => array(
'users' => array(
'foo' => array('password' => 'foo', 'roles' => 'ROLE_USER'),
),
),
),
),
'firewalls' => array(
'simple' => array('pattern' => '/login', 'security' => false),
),
));

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<config>
<provider name="default">
<memory>
<user name="foo" password="foo" roles="ROLE_USER" />
</memory>
</provider>
<firewall name="simple" pattern="/login" security="false" />
</config>
</srv:container>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<config>
<access-decision-manager service="app.access_decision_manager" />
<provider name="default">
<memory>
<user name="foo" password="foo" roles="ROLE_USER" />
</memory>
</provider>
<firewall name="simple" pattern="/login" security="false" />
</config>
</srv:container>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<config>
<access-decision-manager service="app.access_decision_manager" strategy="affirmative" />
<provider name="default">
<memory>
<user name="foo" password="foo" roles="ROLE_USER" />
</memory>
</provider>
<firewall name="simple" pattern="/login" security="false" />
</config>
</srv:container>

View File

@ -0,0 +1,8 @@
security:
providers:
default:
memory:
users:
foo: { password: foo, roles: ROLE_USER }
firewalls:
simple: { pattern: /login, security: false }

View File

@ -0,0 +1,10 @@
security:
access_decision_manager:
service: app.access_decision_manager
providers:
default:
memory:
users:
foo: { password: foo, roles: ROLE_USER }
firewalls:
simple: { pattern: /login, security: false }

View File

@ -0,0 +1,11 @@
security:
access_decision_manager:
service: app.access_decision_manager
strategy: affirmative
providers:
default:
memory:
users:
foo: { password: foo, roles: ROLE_USER }
firewalls:
simple: { pattern: /login, security: false }

View File

@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\WebLink\HttpHeaderSerializer;
use Twig\Extension\ExtensionInterface;
use Twig\Extension\RuntimeExtensionInterface;
use Twig\Loader\LoaderInterface;
/**
@ -150,6 +151,7 @@ class TwigExtension extends Extension
$container->registerForAutoconfiguration(\Twig_LoaderInterface::class)->addTag('twig.loader');
$container->registerForAutoconfiguration(ExtensionInterface::class)->addTag('twig.extension');
$container->registerForAutoconfiguration(LoaderInterface::class)->addTag('twig.loader');
$container->registerForAutoconfiguration(RuntimeExtensionInterface::class)->addTag('twig.runtime');
}
private function getBundleHierarchy(ContainerBuilder $container)

View File

@ -10,21 +10,21 @@
<service id="web_server.command.server_run" class="Symfony\Bundle\WebServerBundle\Command\ServerRunCommand">
<argument>%kernel.project_dir%/web</argument>
<argument>%kernel.environment%</argument>
<tag name="console.command" />
<tag name="console.command" command="server:run" />
</service>
<service id="web_server.command.server_start" class="Symfony\Bundle\WebServerBundle\Command\ServerStartCommand">
<argument>%kernel.project_dir%/web</argument>
<argument>%kernel.environment%</argument>
<tag name="console.command" />
<tag name="console.command" command="server:start" />
</service>
<service id="web_server.command.server_stop" class="Symfony\Bundle\WebServerBundle\Command\ServerStopCommand">
<tag name="console.command" />
<tag name="console.command" command="server:stop" />
</service>
<service id="web_server.command.server_status" class="Symfony\Bundle\WebServerBundle\Command\ServerStatusCommand">
<tag name="console.command" />
<tag name="console.command" command="server:status" />
</service>
</services>
</container>

View File

@ -62,7 +62,7 @@ class Cookie
$this->rawValue = $value;
} else {
$this->value = $value;
$this->rawValue = urlencode($value);
$this->rawValue = rawurlencode($value);
}
$this->name = $name;
$this->path = empty($path) ? '/' : $path;

View File

@ -16,6 +16,21 @@ use Symfony\Component\BrowserKit\Cookie;
class CookieTest extends TestCase
{
public function testToString()
{
$cookie = new Cookie('foo', 'bar', strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true);
$this->assertEquals('foo=bar; expires=Fri, 20 May 2011 15:25:52 GMT; domain=.myfoodomain.com; path=/; secure; httponly', (string) $cookie, '->__toString() returns string representation of the cookie');
$cookie = new Cookie('foo', 'bar with white spaces', strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true);
$this->assertEquals('foo=bar%20with%20white%20spaces; expires=Fri, 20 May 2011 15:25:52 GMT; domain=.myfoodomain.com; path=/; secure; httponly', (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)');
$cookie = new Cookie('foo', null, 1, '/admin/', '.myfoodomain.com');
$this->assertEquals('foo=; expires=Thu, 01 Jan 1970 00:00:01 GMT; domain=.myfoodomain.com; path=/admin/; httponly', (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');
$cookie = new Cookie('foo', 'bar', 0, '/', '');
$this->assertEquals('foo=bar; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; httponly', (string) $cookie);
}
/**
* @dataProvider getTestsForToFromString
*/

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Console;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\DebugFormatterHelper;
@ -63,6 +64,7 @@ class Application
private $runningCommand;
private $name;
private $version;
private $commandLoader;
private $catchExceptions = true;
private $autoExit = true;
private $definition;
@ -95,6 +97,11 @@ class Application
$this->dispatcher = $dispatcher;
}
public function setCommandLoader(CommandLoaderInterface $commandLoader)
{
$this->commandLoader = $commandLoader;
}
/**
* Runs the current application.
*
@ -423,6 +430,10 @@ class Application
throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command)));
}
if (!$command->getName()) {
throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($command)));
}
$this->commands[$command->getName()] = $command;
foreach ($command->getAliases() as $alias) {
@ -443,12 +454,16 @@ class Application
*/
public function get($name)
{
if (!isset($this->commands[$name])) {
if (isset($this->commands[$name])) {
$command = $this->commands[$name];
} elseif ($this->commandLoader && $this->commandLoader->has($name)) {
$command = $this->commandLoader->get($name);
$command->setName($name);
$this->add($command);
} else {
throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
}
$command = $this->commands[$name];
if ($this->wantHelps) {
$this->wantHelps = false;
@ -470,7 +485,7 @@ class Application
*/
public function has($name)
{
return isset($this->commands[$name]);
return isset($this->commands[$name]) || ($this->commandLoader && $this->commandLoader->has($name));
}
/**
@ -547,7 +562,7 @@ class Application
*/
public function find($name)
{
$allCommands = array_keys($this->commands);
$allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands);
$expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $name);
$commands = preg_grep('{^'.$expr.'}', $allCommands);
@ -573,12 +588,12 @@ class Application
// filter out aliases for commands which are already on the list
if (count($commands) > 1) {
$commandList = $this->commands;
$commands = array_filter($commands, function ($nameOrAlias) use ($commandList, $commands) {
$commandName = $commandList[$nameOrAlias]->getName();
$commandList = $this->commandLoader ? array_merge(array_flip($this->commandLoader->getNames()), $this->commands) : $this->commands;
$commands = array_unique(array_filter($commands, function ($nameOrAlias) use ($commandList, $commands) {
$commandName = $commandList[$nameOrAlias] instanceof Command ? $commandList[$nameOrAlias]->getName() : $nameOrAlias;
return $commandName === $nameOrAlias || !in_array($commandName, $commands);
});
}));
}
$exact = in_array($name, $commands, true);
@ -590,6 +605,9 @@ class Application
$maxLen = max(Helper::strlen($abbrev), $maxLen);
}
$abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen) {
if (!$commandList[$cmd] instanceof Command) {
return $cmd;
}
$abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription();
return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
@ -614,7 +632,18 @@ class Application
public function all($namespace = null)
{
if (null === $namespace) {
return $this->commands;
if (!$this->commandLoader) {
return $this->commands;
}
$commands = $this->commands;
foreach ($this->commandLoader->getNames() as $name) {
if (!isset($commands[$name])) {
$commands[$name] = $this->commandLoader->get($name);
}
}
return $commands;
}
$commands = array();
@ -624,6 +653,14 @@ class Application
}
}
if ($this->commandLoader) {
foreach ($this->commandLoader->getNames() as $name) {
if (!isset($commands[$name]) && $namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) {
$commands[$name] = $this->commandLoader->get($name);
}
}
}
return $commands;
}

View File

@ -11,6 +11,11 @@ CHANGELOG
* removed `ConsoleExceptionEvent`
* removed `ConsoleEvents::EXCEPTION`
3.4.0
-----
* added `CommandLoaderInterface` and PSR-11 `ContainerCommandLoader`
3.3.0
-----

View File

@ -61,10 +61,6 @@ class Command
}
$this->configure();
if (!$this->name) {
throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($this)));
}
}
/**

View File

@ -0,0 +1,37 @@
<?php
namespace Symfony\Component\Console\CommandLoader;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface CommandLoaderInterface
{
/**
* Loads a command.
*
* @param string $name
*
* @return Command
*
* @throws CommandNotFoundException
*/
public function get($name);
/**
* Checks if a command exists.
*
* @param string $name
*
* @return bool
*/
public function has($name);
/**
* @return string[] All registered command names
*/
public function getNames();
}

View File

@ -0,0 +1,55 @@
<?php
namespace Symfony\Component\Console\CommandLoader;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* Loads commands from a PSR-11 container.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class ContainerCommandLoader implements CommandLoaderInterface
{
private $container;
private $commandMap;
/**
* @param ContainerInterface $container A container from which to load command services
* @param array $commandMap An array with command names as keys and service ids as values
*/
public function __construct(ContainerInterface $container, array $commandMap)
{
$this->container = $container;
$this->commandMap = $commandMap;
}
/**
* {@inheritdoc}
*/
public function get($name)
{
if (!$this->has($name)) {
throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
}
return $this->container->get($this->commandMap[$name]);
}
/**
* {@inheritdoc}
*/
public function has($name)
{
return isset($this->commandMap[$name]) && $this->container->has($this->commandMap[$name]);
}
/**
* {@inheritdoc}
*/
public function getNames()
{
return array_keys($this->commandMap);
}
}

View File

@ -12,9 +12,12 @@
namespace Symfony\Component\Console\DependencyInjection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\TypedReference;
/**
* Registers console commands.
@ -23,9 +26,20 @@ use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
*/
class AddConsoleCommandPass implements CompilerPassInterface
{
private $commandLoaderServiceId;
private $commandTag;
public function __construct($commandLoaderServiceId = 'console.command_loader', $commandTag = 'console.command')
{
$this->commandLoaderServiceId = $commandLoaderServiceId;
$this->commandTag = $commandTag;
}
public function process(ContainerBuilder $container)
{
$commandServices = $container->findTaggedServiceIds('console.command', true);
$commandServices = $container->findTaggedServiceIds($this->commandTag, true);
$lazyCommandMap = array();
$lazyCommandRefs = array();
$serviceIds = array();
foreach ($commandServices as $id => $tags) {
@ -36,21 +50,52 @@ class AddConsoleCommandPass implements CompilerPassInterface
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}
if (!$r->isSubclassOf(Command::class)) {
throw new InvalidArgumentException(sprintf('The service "%s" tagged "console.command" must be a subclass of "%s".', $id, Command::class));
throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class));
}
$commandId = 'console.command.'.strtolower(str_replace('\\', '_', $class));
if ($container->hasAlias($commandId) || isset($serviceIds[$commandId])) {
$commandId = $commandId.'_'.$id;
}
if (!$definition->isPublic()) {
$container->setAlias($commandId, $id);
$id = $commandId;
if (!isset($tags[0]['command'])) {
if (isset($serviceIds[$commandId]) || $container->hasAlias($commandId)) {
$commandId = $commandId.'_'.$id;
}
if (!$definition->isPublic()) {
$container->setAlias($commandId, $id);
$id = $commandId;
}
$serviceIds[$commandId] = $id;
continue;
}
$serviceIds[$commandId] = $id;
$serviceIds[$commandId] = false;
$commandName = $tags[0]['command'];
$lazyCommandMap[$commandName] = $id;
$lazyCommandRefs[$id] = new TypedReference($id, $class);
$aliases = array();
foreach ($tags as $tag) {
if (!isset($tag['command'])) {
throw new InvalidArgumentException(sprintf('Missing "command" attribute on tag "%s" for service "%s".', $this->commandTag, $id));
}
if ($commandName !== $tag['command']) {
throw new InvalidArgumentException(sprintf('The "command" attribute must be the same on each "%s" tag for service "%s".', $this->commandTag, $id));
}
if (isset($tag['alias'])) {
$aliases[] = $tag['alias'];
$lazyCommandMap[$tag['alias']] = $id;
}
}
if ($aliases) {
$definition->addMethodCall('setAliases', array($aliases));
}
}
$container
->register($this->commandLoaderServiceId, ContainerCommandLoader::class)
->setArguments(array(ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap));
$container->setParameter('console.command.ids', $serviceIds);
}
}

View File

@ -13,6 +13,9 @@ namespace Symfony\Component\Console\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Input\ArgvInput;
@ -30,6 +33,8 @@ use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\EventDispatcher\EventDispatcher;
class ApplicationTest extends TestCase
@ -113,6 +118,26 @@ class ApplicationTest extends TestCase
$this->assertCount(1, $commands, '->all() takes a namespace as its first argument');
}
public function testAllWithCommandLoader()
{
$application = new Application();
$commands = $application->all();
$this->assertInstanceOf('Symfony\\Component\\Console\\Command\\HelpCommand', $commands['help'], '->all() returns the registered commands');
$application->add(new \FooCommand());
$commands = $application->all('foo');
$this->assertCount(1, $commands, '->all() takes a namespace as its first argument');
$application->setCommandLoader(new ContainerCommandLoader(
new ServiceLocator(array('foo-bar' => function () { return new \Foo1Command(); })),
array('foo:bar1' => 'foo-bar')
));
$commands = $application->all('foo');
$this->assertCount(2, $commands, '->all() takes a namespace as its first argument');
$this->assertInstanceOf(\FooCommand::class, $commands['foo:bar'], '->all() returns the registered commands');
$this->assertInstanceOf(\Foo1Command::class, $commands['foo:bar1'], '->all() returns the registered commands');
}
public function testRegister()
{
$application = new Application();
@ -165,6 +190,30 @@ class ApplicationTest extends TestCase
$this->assertInstanceOf('Symfony\Component\Console\Command\HelpCommand', $command, '->get() returns the help command if --help is provided as the input');
}
public function testHasGetWithCommandLoader()
{
$application = new Application();
$this->assertTrue($application->has('list'), '->has() returns true if a named command is registered');
$this->assertFalse($application->has('afoobar'), '->has() returns false if a named command is not registered');
$application->add($foo = new \FooCommand());
$this->assertTrue($application->has('afoobar'), '->has() returns true if an alias is registered');
$this->assertEquals($foo, $application->get('foo:bar'), '->get() returns a command by name');
$this->assertEquals($foo, $application->get('afoobar'), '->get() returns a command by alias');
$application->setCommandLoader(new ContainerCommandLoader(new ServiceLocator(array(
'foo-bar' => function () { return new \Foo1Command(); },
)), array('foo:bar1' => 'foo-bar', 'afoobar1' => 'foo-bar')));
$this->assertTrue($application->has('afoobar'), '->has() returns true if an instance is registered for an alias even with command loader');
$this->assertEquals($foo, $application->get('foo:bar'), '->get() returns an instance by name even with command loader');
$this->assertEquals($foo, $application->get('afoobar'), '->get() returns an instance by alias even with command loader');
$this->assertTrue($application->has('foo:bar1'), '->has() returns true for commands registered in the loader');
$this->assertInstanceOf(\Foo1Command::class, $foo1 = $application->get('foo:bar1'), '->get() returns a command by name from the command loader');
$this->assertTrue($application->has('afoobar1'), '->has() returns true for commands registered in the loader');
$this->assertEquals($foo1, $application->get('afoobar1'), '->get() returns a command by name from the command loader');
}
public function testSilentHelp()
{
$application = new Application();
@ -268,6 +317,20 @@ class ApplicationTest extends TestCase
$this->assertInstanceOf('FooCommand', $application->find('a'), '->find() returns a command if the abbreviation exists for an alias');
}
public function testFindWithCommandLoader()
{
$application = new Application();
$application->setCommandLoader(new ContainerCommandLoader(new ServiceLocator(array(
'foo-bar' => $f = function () { return new \FooCommand(); },
)), array('foo:bar' => 'foo-bar')));
$this->assertInstanceOf('FooCommand', $application->find('foo:bar'), '->find() returns a command if its name exists');
$this->assertInstanceOf('Symfony\Component\Console\Command\HelpCommand', $application->find('h'), '->find() returns a command if its name exists');
$this->assertInstanceOf('FooCommand', $application->find('f:bar'), '->find() returns a command if the abbreviation for the namespace exists');
$this->assertInstanceOf('FooCommand', $application->find('f:b'), '->find() returns a command if the abbreviation for the namespace and the command name exist');
$this->assertInstanceOf('FooCommand', $application->find('a'), '->find() returns a command if the abbreviation exists for an alias');
}
/**
* @dataProvider provideAmbiguousAbbreviations
*/
@ -1313,6 +1376,35 @@ class ApplicationTest extends TestCase
$this->assertEquals($tester->getInput()->isInteractive(), @posix_isatty($inputStream));
}
public function testRunLazyCommandService()
{
$container = new ContainerBuilder();
$container->addCompilerPass(new AddConsoleCommandPass());
$container
->register('lazy-command', LazyCommand::class)
->addTag('console.command', array('command' => 'lazy:command', 'alias' => 'lazy:alias'))
->addTag('console.command', array('command' => 'lazy:command', 'alias' => 'lazy:alias2'));
$container->compile();
$application = new Application();
$application->setCommandLoader($container->get('console.command_loader'));
$application->setAutoExit(false);
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'lazy:command'));
$this->assertSame("lazy-command called\n", $tester->getDisplay(true));
$tester->run(array('command' => 'lazy:alias'));
$this->assertSame("lazy-command called\n", $tester->getDisplay(true));
$tester->run(array('command' => 'lazy:alias2'));
$this->assertSame("lazy-command called\n", $tester->getDisplay(true));
$command = $application->get('lazy:command');
$this->assertSame(array('lazy:alias', 'lazy:alias2'), $command->getAliases());
}
protected function getDispatcher($skipCommand = false)
{
$dispatcher = new EventDispatcher();
@ -1397,3 +1489,11 @@ class CustomDefaultCommandApplication extends Application
$this->setDefaultCommand($command->getName());
}
}
class LazyCommand extends Command
{
public function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln('lazy-command called');
}
}

View File

@ -46,7 +46,7 @@ class CommandTest extends TestCase
*/
public function testCommandNameCannotBeEmpty()
{
new Command();
(new Application())->add(new Command());
}
public function testSetApplication()

View File

@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Tests\CommandLoader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
use Symfony\Component\DependencyInjection\ServiceLocator;
class ContainerCommandLoaderTest extends TestCase
{
public function testHas()
{
$loader = new ContainerCommandLoader(new ServiceLocator(array(
'foo-service' => function () { return new Command('foo'); },
'bar-service' => function () { return new Command('bar'); },
)), array('foo' => 'foo-service', 'bar' => 'bar-service'));
$this->assertTrue($loader->has('foo'));
$this->assertTrue($loader->has('bar'));
$this->assertFalse($loader->has('baz'));
}
public function testGet()
{
$loader = new ContainerCommandLoader(new ServiceLocator(array(
'foo-service' => function () { return new Command('foo'); },
'bar-service' => function () { return new Command('bar'); },
)), array('foo' => 'foo-service', 'bar' => 'bar-service'));
$this->assertInstanceOf(Command::class, $loader->get('foo'));
$this->assertInstanceOf(Command::class, $loader->get('bar'));
}
/**
* @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException
*/
public function testGetUnknownCommandThrows()
{
(new ContainerCommandLoader(new ServiceLocator(array()), array()))->get('unknown');
}
public function testGetCommandNames()
{
$loader = new ContainerCommandLoader(new ServiceLocator(array(
'foo-service' => function () { return new Command('foo'); },
'bar-service' => function () { return new Command('bar'); },
)), array('foo' => 'foo-service', 'bar' => 'bar-service'));
$this->assertSame(array('foo', 'bar'), $loader->getNames());
}
}

View File

@ -12,10 +12,13 @@
namespace Symfony\Component\Console\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AddConsoleCommandPassTest extends TestCase
@ -53,6 +56,26 @@ class AddConsoleCommandPassTest extends TestCase
$this->assertSame(array($alias => $id), $container->getParameter('console.command.ids'));
}
public function testProcessRegisterLazyCommands()
{
$container = new ContainerBuilder();
$container
->register('my-command', MyCommand::class)
->setPublic(false)
->addTag('console.command', array('command' => 'my:command', 'alias' => 'my:alias'))
;
(new AddConsoleCommandPass())->process($container);
$commandLoader = $container->getDefinition('console.command_loader');
$commandLocator = $container->getDefinition((string) $commandLoader->getArgument(0));
$this->assertSame(ContainerCommandLoader::class, $commandLoader->getClass());
$this->assertSame(array('my:command' => 'my-command', 'my:alias' => 'my-command'), $commandLoader->getArgument(1));
$this->assertEquals(array(array('my-command' => new ServiceClosureArgument(new TypedReference('my-command', MyCommand::class)))), $commandLocator->getArguments());
$this->assertSame(array('console.command.symfony_component_console_tests_dependencyinjection_mycommand' => false), $container->getParameter('console.command.ids'));
}
public function visibilityProvider()
{
return array(
@ -72,7 +95,7 @@ class AddConsoleCommandPassTest extends TestCase
$container->addCompilerPass(new AddConsoleCommandPass());
$definition = new Definition('Symfony\Component\Console\Tests\DependencyInjection\MyCommand');
$definition->addTag('console.command');
$definition->addTag('console.command', array('command' => 'my:command'));
$definition->setAbstract(true);
$container->setDefinition('my-command', $definition);
@ -90,7 +113,7 @@ class AddConsoleCommandPassTest extends TestCase
$container->addCompilerPass(new AddConsoleCommandPass());
$definition = new Definition('SplObjectStorage');
$definition->addTag('console.command');
$definition->addTag('console.command', array('command' => 'my:command'));
$container->setDefinition('my-command', $definition);
$container->compile();

View File

@ -392,15 +392,9 @@ class PhpDumper extends Dumper
*/
private function addServiceInstance($id, Definition $definition, $isSimpleInstance)
{
$class = $definition->getClass();
$class = $this->dumpValue($definition->getClass());
if ('\\' === substr($class, 0, 1)) {
$class = substr($class, 1);
}
$class = $this->dumpValue($class);
if (0 === strpos($class, "'") && false === strpos($class, '$') && !preg_match('/^\'[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class)) {
if (0 === strpos($class, "'") && false === strpos($class, '$') && !preg_match('/^\'(?:\\\{2})?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class)) {
throw new InvalidArgumentException(sprintf('"%s" is not a valid class name for the "%s" service.', $class, $id));
}
@ -1488,11 +1482,13 @@ EOF;
if (false !== strpos($class, '$')) {
return sprintf('${($_ = %s) && false ?: "_"}', $class);
}
if (0 !== strpos($class, "'") || !preg_match('/^\'[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class)) {
if (0 !== strpos($class, "'") || !preg_match('/^\'(?:\\\{2})?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class)) {
throw new RuntimeException(sprintf('Cannot dump definition because of invalid class name (%s)', $class ?: 'n/a'));
}
return '\\'.substr(str_replace('\\\\', '\\', $class), 1, -1);
$class = substr(str_replace('\\\\', '\\', $class), 1, -1);
return 0 === strpos($class, '\\') ? $class : '\\'.$class;
}
/**

View File

@ -176,7 +176,10 @@ class YamlFileLoader extends FileLoader
$defaultDirectory = dirname($file);
foreach ($content['imports'] as $import) {
if (!is_array($import)) {
throw new InvalidArgumentException(sprintf('The values in the "imports" key should be arrays in %s. Check your YAML syntax.', $file));
$import = array('resource' => $import);
}
if (!isset($import['resource'])) {
throw new InvalidArgumentException(sprintf('An import should provide a resource in %s. Check your YAML syntax.', $file));
}
$this->setCurrentDir($defaultDirectory);

View File

@ -597,4 +597,18 @@ class PhpDumperTest extends TestCase
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_array_params.php', str_replace('\\\\Dumper', '/Dumper', $dumper->dump(array('file' => self::$fixturesPath.'/php/services_array_params.php'))));
}
public function testDumpHandlesLiteralClassWithRootNamespace()
{
$container = new ContainerBuilder();
$container->register('foo', '\\stdClass');
$container->compile();
$dumper = new PhpDumper($container);
eval('?>'.$dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_Literal_Class_With_Root_Namespace')));
$container = new \Symfony_DI_PhpDumper_Test_Literal_Class_With_Root_Namespace();
$this->assertInstanceOf('stdClass', $container->get('foo'));
}
}

View File

@ -1,2 +1,2 @@
imports:
- foo.yml
- { resource: ~ }

View File

@ -1,5 +1,5 @@
imports:
- { resource: services2.yml }
- services2.yml
- { resource: services3.yml }
- { resource: "../php/simple.php" }
- { resource: "../ini/parameters.ini", class: Symfony\Component\DependencyInjection\Loader\IniFileLoader }

View File

@ -19,11 +19,7 @@ use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\VarDumper\Caster\Caster;
use Symfony\Component\VarDumper\Caster\ClassStub;
use Symfony\Component\VarDumper\Caster\CutStub;
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Cloner\Stub;
use Symfony\Component\VarDumper\Cloner\VarCloner;
/**
* Data collector for {@link FormInterface} instances.
@ -71,11 +67,6 @@ class FormDataCollector extends DataCollector implements FormDataCollectorInterf
*/
private $formsByView;
/**
* @var ClonerInterface
*/
private $cloner;
public function __construct(FormDataExtractorInterface $dataExtractor)
{
if (!class_exists(ClassStub::class)) {
@ -248,49 +239,33 @@ class FormDataCollector extends DataCollector implements FormDataCollectorInterf
/**
* {@inheritdoc}
*/
protected function cloneVar($var, $isClass = false)
protected function getCasters()
{
if ($var instanceof Data) {
return $var;
}
if (null === $this->cloner) {
$this->cloner = new VarCloner();
$this->cloner->setMaxItems(-1);
$this->cloner->addCasters(array(
'*' => function ($v, array $a, Stub $s, $isNested) {
foreach ($a as &$v) {
if (is_object($v) && !$v instanceof \DateTimeInterface) {
$v = new CutStub($v);
}
}
return $a;
},
\Exception::class => function (\Exception $e, array $a, Stub $s) {
if (isset($a[$k = "\0Exception\0previous"])) {
return parent::getCasters() + array(
\Exception::class => function (\Exception $e, array $a, Stub $s) {
foreach (array("\0Exception\0previous", "\0Exception\0trace") as $k) {
if (isset($a[$k])) {
unset($a[$k]);
++$s->cut;
}
}
return $a;
},
FormInterface::class => function (FormInterface $f, array $a) {
return array(
Caster::PREFIX_VIRTUAL.'name' => $f->getName(),
Caster::PREFIX_VIRTUAL.'type_class' => new ClassStub(get_class($f->getConfig()->getType()->getInnerType())),
);
},
ConstraintViolationInterface::class => function (ConstraintViolationInterface $v, array $a) {
return array(
Caster::PREFIX_VIRTUAL.'root' => $v->getRoot(),
Caster::PREFIX_VIRTUAL.'path' => $v->getPropertyPath(),
Caster::PREFIX_VIRTUAL.'value' => $v->getInvalidValue(),
);
},
));
}
return $this->cloner->cloneVar($var, Caster::EXCLUDE_VERBOSE);
return $a;
},
FormInterface::class => function (FormInterface $f, array $a) {
return array(
Caster::PREFIX_VIRTUAL.'name' => $f->getName(),
Caster::PREFIX_VIRTUAL.'type_class' => new ClassStub(get_class($f->getConfig()->getType()->getInnerType())),
);
},
ConstraintViolationInterface::class => function (ConstraintViolationInterface $v, array $a) {
return array(
Caster::PREFIX_VIRTUAL.'root' => $v->getRoot(),
Caster::PREFIX_VIRTUAL.'path' => $v->getPropertyPath(),
Caster::PREFIX_VIRTUAL.'value' => $v->getInvalidValue(),
);
},
);
}
private function &recursiveBuildPreliminaryFormTree(FormInterface $form, array &$outputByHash)

View File

@ -39,8 +39,8 @@
"symfony/dependency-injection": "<3.4",
"symfony/doctrine-bridge": "<3.4",
"symfony/framework-bundle": "<3.4",
"symfony/twig-bridge": "<3.4",
"symfony/var-dumper": "<3.4"
"symfony/http-kernel": "<3.4",
"symfony/twig-bridge": "<3.4"
},
"suggest": {
"symfony/validator": "For form validation.",

View File

@ -145,7 +145,7 @@ class Cookie
if ('' === (string) $this->getValue()) {
$str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; max-age=-31536001';
} else {
$str .= $this->isRaw() ? $this->getValue() : urlencode($this->getValue());
$str .= $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue());
if (0 !== $this->getExpiresTime()) {
$str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; max-age='.$this->getMaxAge();

View File

@ -161,6 +161,9 @@ class CookieTest extends TestCase
$cookie = new Cookie('foo', 'bar', $expire = strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true);
$this->assertEquals('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; max-age='.($expire - time()).'; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() returns string representation of the cookie');
$cookie = new Cookie('foo', 'bar with white spaces', strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true);
$this->assertEquals('foo=bar%20with%20white%20spaces; expires=Fri, 20-May-2011 15:25:52 GMT; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)');
$cookie = new Cookie('foo', null, 1, '/admin/', '.myfoodomain.com');
$this->assertEquals('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', $expire = time() - 31536001).'; max-age='.($expire - time()).'; path=/admin/; domain=.myfoodomain.com; httponly', (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');

View File

@ -11,9 +11,10 @@
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\VarDumper\Caster\ClassStub;
use Symfony\Component\VarDumper\Caster\CutStub;
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Cloner\Stub;
use Symfony\Component\VarDumper\Cloner\VarCloner;
/**
@ -31,7 +32,7 @@ abstract class DataCollector implements DataCollectorInterface, \Serializable
/**
* @var ClonerInterface
*/
private static $cloner;
private $cloner;
public function serialize()
{
@ -55,15 +56,38 @@ abstract class DataCollector implements DataCollectorInterface, \Serializable
*/
protected function cloneVar($var)
{
if (null === self::$cloner) {
if (!class_exists(ClassStub::class)) {
if ($var instanceof Data) {
return $var;
}
if (null === $this->cloner) {
if (!class_exists(CutStub::class)) {
throw new \LogicException(sprintf('The VarDumper component is needed for the %s() method. Install symfony/var-dumper version 3.4 or above.', __METHOD__));
}
self::$cloner = new VarCloner();
self::$cloner->setMaxItems(-1);
$this->cloner = new VarCloner();
$this->cloner->setMaxItems(-1);
$this->cloner->addCasters($this->getCasters());
}
return self::$cloner->cloneVar($var);
return $this->cloner->cloneVar($var);
}
/**
* @return callable[] The casters to add to the cloner
*/
protected function getCasters()
{
return array(
'*' => function ($v, array $a, Stub $s, $isNested) {
if (!$v instanceof Stub) {
foreach ($a as $k => $v) {
if (is_object($v) && !$v instanceof \DateTimeInterface && !$v instanceof Stub) {
$a[$k] = new CutStub($v);
}
}
}
return $a;
},
);
}
}

View File

@ -1472,14 +1472,17 @@ class Process implements \IteratorAggregate
$varCount = 0;
$varCache = array();
$cmd = preg_replace_callback(
'/"(
'/"(?:(
[^"%!^]*+
(?:
(?: !LF! | "(?:\^[%!^])?+" )
[^"%!^]*+
)++
)"/x',
) | [^"]*+ )"/x',
function ($m) use (&$envBackup, &$env, &$varCache, &$varCount, $uid) {
if (!isset($m[1])) {
return $m[0];
}
if (isset($varCache[$m[0]])) {
return $varCache[$m[0]];
}

View File

@ -1423,6 +1423,24 @@ class ProcessTest extends TestCase
$this->assertSame($arg, $p->getOutput());
}
public function testRawCommandLine()
{
$p = new Process(sprintf('"%s" -r %s "a" "" "b"', self::$phpBin, escapeshellarg('print_r($argv);')));
$p->run();
$expected = <<<EOTXT
Array
(
[0] => -
[1] => a
[2] =>
[3] => b
)
EOTXT;
$this->assertSame($expected, $p->getOutput());
}
public function provideEscapeArgument()
{
yield array('a"b%c%');

View File

@ -81,9 +81,9 @@ class AuthenticationProviderManager implements AuthenticationManagerInterface
break;
}
} catch (AccountStatusException $e) {
$e->setToken($token);
$lastException = $e;
throw $e;
break;
} catch (AuthenticationException $e) {
$lastException = $e;
}

View File

@ -13,6 +13,9 @@ namespace Symfony\Component\Security\Core\Tests\Authentication;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationEvent;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\Security\Core\Exception\ProviderNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
@ -125,6 +128,50 @@ class AuthenticationProviderManagerTest extends TestCase
$this->assertEquals('bar', $token->getCredentials());
}
public function testAuthenticateDispatchesAuthenticationFailureEvent()
{
$token = new UsernamePasswordToken('foo', 'bar', 'key');
$provider = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface')->getMock();
$provider->expects($this->once())->method('supports')->willReturn(true);
$provider->expects($this->once())->method('authenticate')->willThrowException($exception = new AuthenticationException());
$dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock();
$dispatcher
->expects($this->once())
->method('dispatch')
->with(AuthenticationEvents::AUTHENTICATION_FAILURE, $this->equalTo(new AuthenticationFailureEvent($token, $exception)));
$manager = new AuthenticationProviderManager(array($provider));
$manager->setEventDispatcher($dispatcher);
try {
$manager->authenticate($token);
$this->fail('->authenticate() should rethrow exceptions');
} catch (AuthenticationException $e) {
$this->assertSame($token, $exception->getToken());
}
}
public function testAuthenticateDispatchesAuthenticationSuccessEvent()
{
$token = new UsernamePasswordToken('foo', 'bar', 'key');
$provider = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface')->getMock();
$provider->expects($this->once())->method('supports')->willReturn(true);
$provider->expects($this->once())->method('authenticate')->willReturn($token);
$dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock();
$dispatcher
->expects($this->once())
->method('dispatch')
->with(AuthenticationEvents::AUTHENTICATION_SUCCESS, $this->equalTo(new AuthenticationEvent($token)));
$manager = new AuthenticationProviderManager(array($provider));
$manager->setEventDispatcher($dispatcher);
$this->assertSame($token, $manager->authenticate($token));
}
protected function getAuthenticationProvider($supports, $token = null, $exception = null)
{
$provider = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface')->getMock();

View File

@ -18,6 +18,7 @@ CHANGELOG
* setting the `checkDNS` option of the `Url` constraint to `true` is deprecated in favor of
the `Url::CHECK_DNS_TYPE_*` constants values and will throw an exception in Symfony 4.0
* added min/max amount of pixels check to `Image` constraint via `minPixels` and `maxPixels`
* added a new "propertyPath" option to comparison constraints in order to get the value to compare from an array or object
3.3.0
-----

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Constraints;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
@ -24,6 +25,7 @@ abstract class AbstractComparison extends Constraint
{
public $message;
public $value;
public $propertyPath;
/**
* {@inheritdoc}
@ -34,11 +36,18 @@ abstract class AbstractComparison extends Constraint
$options = array();
}
if (is_array($options) && !isset($options['value'])) {
throw new ConstraintDefinitionException(sprintf(
'The %s constraint requires the "value" option to be set.',
get_class($this)
));
if (is_array($options)) {
if (!isset($options['value']) && !isset($options['propertyPath'])) {
throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires either the "value" or "propertyPath" option to be set.', get_class($this)));
}
if (isset($options['value']) && isset($options['propertyPath'])) {
throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires only one of the "value" or "propertyPath" options to be set, not both.', get_class($this)));
}
if (isset($options['propertyPath']) && !class_exists(PropertyAccess::class)) {
throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires the Symfony PropertyAccess component to use the "propertyPath" option.', get_class($this)));
}
}
parent::__construct($options);

View File

@ -11,8 +11,12 @@
namespace Symfony\Component\Validator\Constraints;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
@ -23,6 +27,13 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
*/
abstract class AbstractComparisonValidator extends ConstraintValidator
{
private $propertyAccessor;
public function __construct(PropertyAccessor $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor;
}
/**
* {@inheritdoc}
*/
@ -36,7 +47,19 @@ abstract class AbstractComparisonValidator extends ConstraintValidator
return;
}
$comparedValue = $constraint->value;
if ($path = $constraint->propertyPath) {
if (null === $object = $this->context->getObject()) {
return;
}
try {
$comparedValue = $this->getPropertyAccessor()->getValue($object, $path);
} catch (NoSuchPropertyException $e) {
throw new ConstraintDefinitionException(sprintf('Invalid property path "%s" provided to "%s" constraint: %s', $path, get_class($constraint), $e->getMessage()), 0, $e);
}
} else {
$comparedValue = $constraint->value;
}
// Convert strings to DateTimes if comparing another DateTime
// This allows to compare with any date/time value supported by
@ -63,6 +86,15 @@ abstract class AbstractComparisonValidator extends ConstraintValidator
}
}
private function getPropertyAccessor()
{
if (null === $this->propertyAccessor) {
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
}
return $this->propertyAccessor;
}
/**
* Compares the two given values to find if their relationship is valid.
*

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Validator\Tests\Constraints;
use Symfony\Component\Intl\Util\IntlTestHelper;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
class ComparisonTest_Class
@ -28,6 +29,11 @@ class ComparisonTest_Class
{
return (string) $this->value;
}
public function getValue()
{
return $this->value;
}
}
/**
@ -76,12 +82,25 @@ abstract class AbstractComparisonValidatorTestCase extends ConstraintValidatorTe
/**
* @dataProvider provideInvalidConstraintOptions
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
* @expectedExceptionMessage requires either the "value" or "propertyPath" option to be set.
*/
public function testThrowsConstraintExceptionIfNoValueOrProperty($options)
public function testThrowsConstraintExceptionIfNoValueOrPropertyPath($options)
{
$this->createConstraint($options);
}
/**
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
* @expectedExceptionMessage requires only one of the "value" or "propertyPath" options to be set, not both.
*/
public function testThrowsConstraintExceptionIfBothValueAndPropertyPath()
{
$this->createConstraint((array(
'value' => 'value',
'propertyPath' => 'propertyPath',
)));
}
/**
* @dataProvider provideAllValidComparisons
*
@ -113,11 +132,75 @@ abstract class AbstractComparisonValidatorTestCase extends ConstraintValidatorTe
return $comparisons;
}
/**
* @dataProvider provideValidComparisonsToPropertyPath
*/
public function testValidComparisonToPropertyPath($comparedValue)
{
$constraint = $this->createConstraint(array('propertyPath' => 'value'));
$object = new ComparisonTest_Class(5);
$this->setObject($object);
$this->validator->validate($comparedValue, $constraint);
$this->assertNoViolation();
}
/**
* @dataProvider provideValidComparisonsToPropertyPath
*/
public function testValidComparisonToPropertyPathOnArray($comparedValue)
{
$constraint = $this->createConstraint(array('propertyPath' => '[root][value]'));
$this->setObject(array('root' => array('value' => 5)));
$this->validator->validate($comparedValue, $constraint);
$this->assertNoViolation();
}
public function testNoViolationOnNullObjectWithPropertyPath()
{
$constraint = $this->createConstraint(array('propertyPath' => 'propertyPath'));
$this->setObject(null);
$this->validator->validate('some data', $constraint);
$this->assertNoViolation();
}
public function testInvalidValuePath()
{
$constraint = $this->createConstraint(array('propertyPath' => 'foo'));
if (method_exists($this, 'expectException')) {
$this->expectException(ConstraintDefinitionException::class);
$this->expectExceptionMessage(sprintf('Invalid property path "foo" provided to "%s" constraint', get_class($constraint)));
} else {
$this->setExpectedException(ConstraintDefinitionException::class, sprintf('Invalid property path "foo" provided to "%s" constraint', get_class($constraint)));
}
$object = new ComparisonTest_Class(5);
$this->setObject($object);
$this->validator->validate(5, $constraint);
}
/**
* @return array
*/
abstract public function provideValidComparisons();
/**
* @return array
*/
abstract public function provideValidComparisonsToPropertyPath();
/**
* @dataProvider provideAllInvalidComparisons
*

View File

@ -51,6 +51,16 @@ class EqualToValidatorTest extends AbstractComparisonValidatorTestCase
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(5),
);
}
/**
* {@inheritdoc}
*/

View File

@ -54,6 +54,17 @@ class GreaterThanOrEqualValidatorTest extends AbstractComparisonValidatorTestCas
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(5),
array(6),
);
}
/**
* {@inheritdoc}
*/

View File

@ -50,6 +50,16 @@ class GreaterThanValidatorTest extends AbstractComparisonValidatorTestCase
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(6),
);
}
/**
* {@inheritdoc}
*/

View File

@ -69,6 +69,16 @@ class IdenticalToValidatorTest extends AbstractComparisonValidatorTestCase
return $comparisons;
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(5),
);
}
/**
* {@inheritdoc}
*/

View File

@ -56,6 +56,17 @@ class LessThanOrEqualValidatorTest extends AbstractComparisonValidatorTestCase
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(4),
array(5),
);
}
/**
* {@inheritdoc}
*/

View File

@ -50,6 +50,16 @@ class LessThanValidatorTest extends AbstractComparisonValidatorTestCase
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(4),
);
}
/**
* {@inheritdoc}
*/

View File

@ -50,6 +50,16 @@ class NotEqualToValidatorTest extends AbstractComparisonValidatorTestCase
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(0),
);
}
/**
* {@inheritdoc}
*/

View File

@ -53,6 +53,16 @@ class NotIdenticalToValidatorTest extends AbstractComparisonValidatorTestCase
);
}
/**
* {@inheritdoc}
*/
public function provideValidComparisonsToPropertyPath()
{
return array(
array(0),
);
}
public function provideAllInvalidComparisons()
{
$this->setDefaultTimezone('UTC');

View File

@ -30,6 +30,7 @@
"symfony/dependency-injection": "~3.4|~4.0",
"symfony/expression-language": "~3.4|~4.0",
"symfony/cache": "~3.4|~4.0",
"symfony/property-access": "~3.4|~4.0",
"doctrine/annotations": "~1.0",
"doctrine/cache": "~1.0",
"egulias/email-validator": "^1.2.8|~2.0"
@ -48,6 +49,7 @@
"symfony/yaml": "",
"symfony/config": "",
"egulias/email-validator": "Strict (RFC compliant) email validation",
"symfony/property-access": "For accessing properties within comparison constraints",
"symfony/expression-language": "For using the Expression validator"
},
"autoload": {

View File

@ -60,11 +60,20 @@ class Caster
}
if ($a) {
static $publicProperties = array();
$i = 0;
$prefixedKeys = array();
foreach ($a as $k => $v) {
if (isset($k[0]) && "\0" !== $k[0] && !property_exists($class, $k)) {
$prefixedKeys[$i] = self::PREFIX_DYNAMIC.$k;
if (isset($k[0]) && "\0" !== $k[0]) {
if (!isset($publicProperties[$class])) {
foreach (get_class_vars($class) as $prop => $v) {
$publicProperties[$class][$prop] = true;
}
}
if (!isset($publicProperties[$class][$k])) {
$prefixedKeys[$i] = self::PREFIX_DYNAMIC.$k;
}
} elseif (isset($k[16]) && "\0" === $k[16] && 0 === strpos($k, "\0class@anonymous\0")) {
$prefixedKeys[$i] = "\0".get_parent_class($class).'@anonymous'.strrchr($k, "\0");
}

View File

@ -102,7 +102,7 @@ class ExceptionCaster
}
unset($a[$sPrefix.'file'], $a[$sPrefix.'line'], $a[$sPrefix.'trace']);
$a[Caster::PREFIX_VIRTUAL.'trace'] = new TraceStub($trace);
$a[Caster::PREFIX_VIRTUAL.'trace'] = new TraceStub($trace, self::$traceArgs);
return $a;
}
@ -256,7 +256,7 @@ class ExceptionCaster
$trace = array();
}
if (!($filter & Caster::EXCLUDE_VERBOSE)) {
if (!($filter & Caster::EXCLUDE_VERBOSE) && $trace) {
if (isset($a[Caster::PREFIX_PROTECTED.'file'], $a[Caster::PREFIX_PROTECTED.'line'])) {
self::traceUnshift($trace, $xClass, $a[Caster::PREFIX_PROTECTED.'file'], $a[Caster::PREFIX_PROTECTED.'line']);
}

View File

@ -209,15 +209,17 @@ abstract class AbstractCloner implements ClonerInterface
});
$this->filter = $filter;
if ($gc = gc_enabled()) {
gc_disable();
}
try {
$data = $this->doClone($var);
} catch (\Exception $e) {
}
restore_error_handler();
$this->prevErrorHandler = null;
if (isset($e)) {
throw $e;
} finally {
if ($gc) {
gc_enable();
}
restore_error_handler();
$this->prevErrorHandler = null;
}
return new Data($data);

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\VarDumper\Tests\Caster;
use PHPUnit\Framework\TestCase;
use Symfony\Component\VarDumper\Caster\Caster;
use Symfony\Component\VarDumper\Test\VarDumperTestTrait;
use Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo;
use Symfony\Component\VarDumper\Tests\Fixtures\NotLoadableClass;
@ -77,13 +78,27 @@ Closure {
\$b: & 123
}
file: "%sReflectionCasterTest.php"
line: "67 to 67"
line: "68 to 68"
}
EOTXT
, $var
);
}
public function testClosureCasterExcludingVerbosity()
{
$var = function () {};
$expectedDump = <<<EOTXT
Closure {
class: "Symfony\Component\VarDumper\Tests\Caster\ReflectionCasterTest"
this: Symfony\Component\VarDumper\Tests\Caster\ReflectionCasterTest { }
}
EOTXT;
$this->assertDumpEquals($expectedDump, $var, Caster::EXCLUDE_VERBOSE);
}
public function testReflectionParameter()
{
$var = new \ReflectionParameter(__NAMESPACE__.'\reflectionParameterFixture', 0);