From 28930c59d3b985bc41a2125763b32e10c99bc3be Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Thu, 29 Jun 2017 21:02:30 +0200 Subject: [PATCH 01/22] [WebProfilerBundle] Fix full sized dump hovering in toolbar --- .../Resources/views/Profiler/toolbar.css.twig | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig index 7a2d92e134..6cbda5fb3b 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig @@ -350,13 +350,12 @@ 100% { background: #222; } } -.sf-toolbar-block.sf-toolbar-block-dump { - position: static; -} - .sf-toolbar-block.sf-toolbar-block-dump .sf-toolbar-info { max-width: none; - right: 0; + width: 100%; + position: fixed; + box-sizing: border-box; + left: 0; } .sf-toolbar-block-dump pre.sf-dump { From a73522c0de5cc9e58718c717817a85f14be4d48c Mon Sep 17 00:00:00 2001 From: Dany Maillard Date: Sun, 30 Apr 2017 17:09:09 +0200 Subject: [PATCH 02/22] Add interval caster --- .../Component/VarDumper/Caster/DateCaster.php | 32 +++++- .../VarDumper/Cloner/AbstractCloner.php | 1 + .../VarDumper/Tests/Caster/DateCasterTest.php | 100 ++++++++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/VarDumper/Caster/DateCaster.php b/src/Symfony/Component/VarDumper/Caster/DateCaster.php index 2fc57c190b..b96660605f 100644 --- a/src/Symfony/Component/VarDumper/Caster/DateCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/DateCaster.php @@ -27,7 +27,7 @@ class DateCaster $fromNow = (new \DateTime())->diff($d); $title = $d->format('l, F j, Y') - ."\n".$fromNow->format('%R').(ltrim($fromNow->format('%yy %mm %dd %H:%I:%Ss'), ' 0ymd:s') ?: '0s').' from now' + ."\n".$fromNow->format('%R').self::formatInterval($fromNow).' from now' .($location ? ($d->format('I') ? "\nDST On" : "\nDST Off") : '') ; @@ -38,4 +38,34 @@ class DateCaster return $a; } + + public static function castInterval(\DateInterval $interval, array $a, Stub $stub, $isNested, $filter) + { + $now = new \DateTimeImmutable(); + $numberOfSeconds = $now->add($interval)->getTimestamp() - $now->getTimestamp(); + $title = number_format($numberOfSeconds, 0, '.', ' ').'s'; + + $i = array(Caster::PREFIX_VIRTUAL.'interval' => new ConstStub(self::formatInterval($interval), $title)); + + return $filter & Caster::EXCLUDE_VERBOSE ? $i : $i + $a; + } + + private static function formatInterval(\DateInterval $i) + { + $format = '%R ' + .($i->y ? '%yy ' : '') + .($i->m ? '%mm ' : '') + .($i->d ? '%dd ' : '') + ; + + if (\PHP_VERSION_ID >= 70100 && isset($i->f)) { + $format .= $i->h || $i->i || $i->s || $i->f ? '%H:%I:%S.%F' : ''; + } else { + $format .= $i->h || $i->i || $i->s ? '%H:%I:%S' : ''; + } + + $format = '%R ' === $format ? '0s' : $format; + + return $i->format(rtrim($format)); + } } diff --git a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php index bb2a4b8507..745dcc0a8e 100644 --- a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php +++ b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php @@ -110,6 +110,7 @@ abstract class AbstractCloner implements ClonerInterface 'RedisArray' => array('Symfony\Component\VarDumper\Caster\RedisCaster', 'castRedisArray'), 'DateTimeInterface' => array('Symfony\Component\VarDumper\Caster\DateCaster', 'castDateTime'), + 'DateInterval' => array('Symfony\Component\VarDumper\Caster\DateCaster', 'castInterval'), ':curl' => array('Symfony\Component\VarDumper\Caster\ResourceCaster', 'castCurl'), ':dba' => array('Symfony\Component\VarDumper\Caster\ResourceCaster', 'castDba'), diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php index 10f7896aec..637604e24d 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\VarDumper\Tests\Caster; use PHPUnit\Framework\TestCase; +use Symfony\Component\VarDumper\Caster\Caster; use Symfony\Component\VarDumper\Caster\DateCaster; use Symfony\Component\VarDumper\Cloner\Stub; use Symfony\Component\VarDumper\Test\VarDumperTestTrait; @@ -84,4 +85,103 @@ EODUMP; array('2017-04-30 00:00:00.000000', '+02:00', '2017-04-30 00:00:00.000000 +02:00'), ); } + + /** + * @dataProvider provideIntervals + */ + public function testDumpInterval($intervalSpec, $invert, $expected) + { + $interval = new \DateInterval($intervalSpec); + $interval->invert = $invert; + + $xDump = <<assertDumpMatchesFormat($xDump, $interval); + } + + /** + * @dataProvider provideIntervals + */ + public function testDumpIntervalExcludingVerbosity($intervalSpec, $invert, $expected) + { + $interval = new \DateInterval($intervalSpec); + $interval->invert = $invert; + + $xDump = <<assertDumpMatchesFormat($xDump, $interval, Caster::EXCLUDE_VERBOSE); + } + + /** + * @dataProvider provideIntervals + */ + public function testCastInterval($intervalSpec, $invert, $xInterval, $xSeconds) + { + $interval = new \DateInterval($intervalSpec); + $interval->invert = $invert; + $stub = new Stub(); + + $cast = DateCaster::castInterval($interval, array('foo' => 'bar'), $stub, false, Caster::EXCLUDE_VERBOSE); + + $xDump = << $xInterval +] +EODUMP; + + $this->assertDumpMatchesFormat($xDump, $cast); + + if (null === $xSeconds) { + return; + } + + $xDump = <<assertDumpMatchesFormat($xDump, $cast["\0~\0interval"]); + } + + public function provideIntervals() + { + $i = new \DateInterval('PT0S'); + $ms = \PHP_VERSION_ID >= 70100 && isset($i->f) ? '.000000' : ''; + + return array( + array('PT0S', 0, '0s', '0s'), + array('PT1S', 0, '+ 00:00:01'.$ms, '1s'), + array('PT2M', 0, '+ 00:02:00'.$ms, '120s'), + array('PT3H', 0, '+ 03:00:00'.$ms, '10 800s'), + array('P4D', 0, '+ 4d', '345 600s'), + array('P5M', 0, '+ 5m', null), + array('P6Y', 0, '+ 6y', null), + array('P1Y2M3DT4H5M6S', 0, '+ 1y 2m 3d 04:05:06'.$ms, null), + + array('PT0S', 1, '0s', '0s'), + array('PT1S', 1, '- 00:00:01'.$ms, '-1s'), + array('PT2M', 1, '- 00:02:00'.$ms, '-120s'), + array('PT3H', 1, '- 03:00:00'.$ms, '-10 800s'), + array('P4D', 1, '- 4d', '-345 600s'), + array('P5M', 1, '- 5m', null), + array('P6Y', 1, '- 6y', null), + array('P1Y2M3DT4H5M6S', 1, '- 1y 2m 3d 04:05:06'.$ms, null), + ); + } } From 9b40b4af652bc7bbffd030cfe29ac370c0a9146e Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Fri, 14 Jul 2017 19:53:01 +0200 Subject: [PATCH 03/22] [Console] Add a factory command loader for standalone application with lazy-loading needs --- src/Symfony/Component/Console/CHANGELOG.md | 3 +- .../CommandLoader/FactoryCommandLoader.php | 62 +++++++++++++++++++ .../Console/Tests/ApplicationTest.php | 22 +++---- .../FactoryCommandLoaderTest.php | 60 ++++++++++++++++++ 4 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 src/Symfony/Component/Console/CommandLoader/FactoryCommandLoader.php create mode 100644 src/Symfony/Component/Console/Tests/CommandLoader/FactoryCommandLoaderTest.php diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 542d00623b..b337ecf992 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -4,7 +4,8 @@ CHANGELOG 3.4.0 ----- - * added `CommandLoaderInterface` and PSR-11 `ContainerCommandLoader` + * added `CommandLoaderInterface`, `FactoryCommandLoader` and PSR-11 + `ContainerCommandLoader` for commands lazy-loading 3.3.0 ----- diff --git a/src/Symfony/Component/Console/CommandLoader/FactoryCommandLoader.php b/src/Symfony/Component/Console/CommandLoader/FactoryCommandLoader.php new file mode 100644 index 0000000000..d9c2055710 --- /dev/null +++ b/src/Symfony/Component/Console/CommandLoader/FactoryCommandLoader.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\CommandLoader; + +use Symfony\Component\Console\Exception\CommandNotFoundException; + +/** + * A simple command loader using factories to instantiate commands lazily. + * + * @author Maxime Steinhausser + */ +class FactoryCommandLoader implements CommandLoaderInterface +{ + private $factories; + + /** + * @param callable[] $factories Indexed by command names + */ + public function __construct(array $factories) + { + $this->factories = $factories; + } + + /** + * {@inheritdoc} + */ + public function has($name) + { + return isset($this->factories[$name]); + } + + /** + * {@inheritdoc} + */ + public function get($name) + { + if (!isset($this->factories[$name])) { + throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); + } + + $factory = $this->factories[$name]; + + return $factory(); + } + + /** + * {@inheritdoc} + */ + public function getNames() + { + return array_keys($this->factories); + } +} diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 7bf76ddc8e..d475f12bc9 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -14,7 +14,7 @@ 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\CommandLoader\FactoryCommandLoader; use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\FormatterHelper; @@ -35,7 +35,6 @@ use Symfony\Component\Console\Event\ConsoleExceptionEvent; 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 @@ -129,10 +128,9 @@ class ApplicationTest extends TestCase $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') - )); + $application->setCommandLoader(new FactoryCommandLoader(array( + 'foo:bar1' => function () { return new \Foo1Command(); }, + ))); $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'); @@ -202,9 +200,9 @@ class ApplicationTest extends TestCase $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'))); + $application->setCommandLoader(new FactoryCommandLoader(array( + 'foo:bar1' => function () { return new \Foo1Command(); }, + ))); $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'); @@ -321,9 +319,9 @@ class ApplicationTest extends TestCase 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'))); + $application->setCommandLoader(new FactoryCommandLoader(array( + 'foo:bar' => $f = function () { return new \FooCommand(); }, + ))); $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'); diff --git a/src/Symfony/Component/Console/Tests/CommandLoader/FactoryCommandLoaderTest.php b/src/Symfony/Component/Console/Tests/CommandLoader/FactoryCommandLoaderTest.php new file mode 100644 index 0000000000..5ee6cd1ec3 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/CommandLoader/FactoryCommandLoaderTest.php @@ -0,0 +1,60 @@ + + * + * 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\FactoryCommandLoader; + +class FactoryCommandLoaderTest extends TestCase +{ + public function testHas() + { + $loader = new FactoryCommandLoader(array( + 'foo' => function () { return new Command('foo'); }, + 'bar' => function () { return new Command('bar'); }, + )); + + $this->assertTrue($loader->has('foo')); + $this->assertTrue($loader->has('bar')); + $this->assertFalse($loader->has('baz')); + } + + public function testGet() + { + $loader = new FactoryCommandLoader(array( + 'foo' => function () { return new Command('foo'); }, + 'bar' => function () { return new Command('bar'); }, + )); + + $this->assertInstanceOf(Command::class, $loader->get('foo')); + $this->assertInstanceOf(Command::class, $loader->get('bar')); + } + + /** + * @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException + */ + public function testGetUnknownCommandThrows() + { + (new FactoryCommandLoader(array()))->get('unknown'); + } + + public function testGetCommandNames() + { + $loader = new FactoryCommandLoader(array( + 'foo' => function () { return new Command('foo'); }, + 'bar' => function () { return new Command('bar'); }, + )); + + $this->assertSame(array('foo', 'bar'), $loader->getNames()); + } +} From 30cd70dcad190e3ad13a8b301d95cbcdcccb0400 Mon Sep 17 00:00:00 2001 From: James Johnston Date: Mon, 17 Jul 2017 09:04:54 -0700 Subject: [PATCH 04/22] [DebugBundle] Added min_depth to Configuration This enables calling the recently-added setMinDepth function on VarCloner. --- .../Bundle/DebugBundle/DependencyInjection/Configuration.php | 5 +++++ .../DebugBundle/DependencyInjection/DebugExtension.php | 1 + .../Bundle/DebugBundle/Resources/config/schema/debug-1.0.xsd | 1 + 3 files changed, 7 insertions(+) diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php index 5761e62a72..4af24cd488 100644 --- a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php @@ -36,6 +36,11 @@ class Configuration implements ConfigurationInterface ->min(-1) ->defaultValue(2500) ->end() + ->integerNode('min_depth') + ->info('Minimum tree depth to clone all the items, 1 is default') + ->min(0) + ->defaultValue(1) + ->end() ->integerNode('max_string_length') ->info('Max length of displayed strings, -1 means no limit') ->min(-1) diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php index ce6d1b7c67..835d823664 100644 --- a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php @@ -37,6 +37,7 @@ class DebugExtension extends Extension $container->getDefinition('var_dumper.cloner') ->addMethodCall('setMaxItems', array($config['max_items'])) + ->addMethodCall('setMinDepth', array($config['min_depth'])) ->addMethodCall('setMaxString', array($config['max_string_length'])); if (null !== $config['dump_destination']) { diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/schema/debug-1.0.xsd b/src/Symfony/Bundle/DebugBundle/Resources/config/schema/debug-1.0.xsd index a582ff8b2b..3230688602 100644 --- a/src/Symfony/Bundle/DebugBundle/Resources/config/schema/debug-1.0.xsd +++ b/src/Symfony/Bundle/DebugBundle/Resources/config/schema/debug-1.0.xsd @@ -8,6 +8,7 @@ + From 53b01903ceb91e1b0fb17c0f1a66faae3d68c51d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 18 Jul 2017 13:08:22 +0200 Subject: [PATCH 05/22] [Config] Make ClassExistenceResource throw on invalid parents --- .../Resource/ClassExistenceResource.php | 25 ++++++++++++++++--- .../Compiler/AutowirePass.php | 12 ++++----- .../Compiler/FactoryReturnTypePass.php | 2 +- .../ResolveInstanceofConditionalsPass.php | 2 +- .../DependencyInjection/ContainerBuilder.php | 8 +++++- .../Tests/Compiler/AutowirePassTest.php | 6 ++--- 6 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/Symfony/Component/Config/Resource/ClassExistenceResource.php b/src/Symfony/Component/Config/Resource/ClassExistenceResource.php index 8a86a42099..ada9a89b94 100644 --- a/src/Symfony/Component/Config/Resource/ClassExistenceResource.php +++ b/src/Symfony/Component/Config/Resource/ClassExistenceResource.php @@ -25,6 +25,7 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ private $exists; private static $autoloadLevel = 0; + private static $autoloadedClass; private static $existsCache = array(); /** @@ -57,6 +58,8 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ /** * {@inheritdoc} + * + * @throws \ReflectionException when a parent class/interface/trait is not found */ public function isFresh($timestamp) { @@ -68,12 +71,13 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ if (!self::$autoloadLevel++) { spl_autoload_register(__CLASS__.'::throwOnRequiredClass'); } + $autoloadedClass = self::$autoloadedClass; + self::$autoloadedClass = $this->resource; try { $exists = class_exists($this->resource) || interface_exists($this->resource, false) || trait_exists($this->resource, false); - } catch (\ReflectionException $e) { - $exists = false; } finally { + self::$autoloadedClass = $autoloadedClass; if (!--self::$autoloadLevel) { spl_autoload_unregister(__CLASS__.'::throwOnRequiredClass'); } @@ -112,7 +116,10 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ */ private static function throwOnRequiredClass($class) { - $e = new \ReflectionException("Class $class does not exist"); + if (self::$autoloadedClass === $class) { + return; + } + $e = new \ReflectionException("Class $class not found"); $trace = $e->getTrace(); $autoloadFrame = array( 'function' => 'spl_autoload_call', @@ -138,6 +145,18 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ case 'is_callable': return; } + + $props = array( + 'file' => $trace[$i]['file'], + 'line' => $trace[$i]['line'], + 'trace' => array_slice($trace, 0, 1 + $i), + ); + + foreach ($props as $p => $v) { + $r = new \ReflectionProperty('Exception', $p); + $r->setAccessible(true); + $r->setValue($e, $v); + } } throw $e; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index 8382d30385..055347b6f8 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -124,8 +124,8 @@ class AutowirePass extends AbstractRecursivePass if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) { return $value; } - if (!$reflectionClass = $this->container->getReflectionClass($value->getClass())) { - $this->container->log($this, sprintf('Skipping service "%s": Class or interface "%s" does not exist.', $this->currentId, $value->getClass())); + if (!$reflectionClass = $this->container->getReflectionClass($value->getClass(), false)) { + $this->container->log($this, sprintf('Skipping service "%s": Class or interface "%s" cannot be loaded.', $this->currentId, $value->getClass())); return $value; } @@ -388,7 +388,7 @@ class AutowirePass extends AbstractRecursivePass unset($this->ambiguousServiceTypes[$type]); } - if ($definition->isDeprecated() || !$reflectionClass = $this->container->getReflectionClass($definition->getClass())) { + if ($definition->isDeprecated() || !$reflectionClass = $this->container->getReflectionClass($definition->getClass(), false)) { return; } @@ -444,7 +444,7 @@ class AutowirePass extends AbstractRecursivePass */ private function createAutowiredDefinition($type) { - if (!($typeHint = $this->container->getReflectionClass($type)) || !$typeHint->isInstantiable()) { + if (!($typeHint = $this->container->getReflectionClass($type, false)) || !$typeHint->isInstantiable()) { return; } @@ -478,8 +478,8 @@ class AutowirePass extends AbstractRecursivePass private function createTypeNotFoundMessage(TypedReference $reference, $label) { - if (!$r = $this->container->getReflectionClass($type = $reference->getType())) { - $message = sprintf('has type "%s" but this class does not exist.', $type); + if (!$r = $this->container->getReflectionClass($type = $reference->getType(), false)) { + $message = sprintf('has type "%s" but this class cannot be loaded.', $type); } else { $message = $this->container->has($type) ? 'this service is abstract' : 'no such service exists'; $message = sprintf('references %s "%s" but %s.%s', $r->isInterface() ? 'interface' : 'class', $type, $message, $this->createTypeAlternatives($reference)); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/FactoryReturnTypePass.php b/src/Symfony/Component/DependencyInjection/Compiler/FactoryReturnTypePass.php index 3ba4a8caa0..85b5872b0e 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/FactoryReturnTypePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/FactoryReturnTypePass.php @@ -81,7 +81,7 @@ class FactoryReturnTypePass implements CompilerPassInterface $class = $factory[0]; } - if (!$m = $container->getReflectionClass($class)) { + if (!$m = $container->getReflectionClass($class, false)) { return; } try { diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php index 82f13ed2d0..32d7ad2911 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php @@ -66,7 +66,7 @@ class ResolveInstanceofConditionalsPass implements CompilerPassInterface $instanceofTags = array(); foreach ($conditionals as $interface => $instanceofDefs) { - if ($interface !== $class && (!$container->getReflectionClass($class))) { + if ($interface !== $class && (!$container->getReflectionClass($class, false))) { continue; } diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 14dd957e9e..c824cc959d 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -337,12 +337,15 @@ class ContainerBuilder extends Container implements TaggedContainerInterface * Retrieves the requested reflection class and registers it for resource tracking. * * @param string $class + * @param bool $throw * * @return \ReflectionClass|null * + * @throws \ReflectionException when a parent class/interface/trait is not found and $throw is true + * * @final */ - public function getReflectionClass($class) + public function getReflectionClass($class, $throw = true) { if (!$class = $this->getParameterBag()->resolveValue($class)) { return; @@ -357,6 +360,9 @@ class ContainerBuilder extends Container implements TaggedContainerInterface $classReflector = $resource->isFresh(0) ? false : new \ReflectionClass($class); } } catch (\ReflectionException $e) { + if ($throw) { + throw $e; + } $classReflector = false; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index 41ad563413..76cf290283 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -359,7 +359,7 @@ class AutowirePassTest extends TestCase /** * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException - * @expectedExceptionMessage Cannot autowire service "a": argument "$r" of method "Symfony\Component\DependencyInjection\Tests\Compiler\BadTypeHintedArgument::__construct()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\NotARealClass" but this class does not exist. + * @expectedExceptionMessage Cannot autowire service "a": argument "$r" of method "Symfony\Component\DependencyInjection\Tests\Compiler\BadTypeHintedArgument::__construct()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\NotARealClass" but this class cannot be loaded. */ public function testClassNotFoundThrowsException() { @@ -374,7 +374,7 @@ class AutowirePassTest extends TestCase /** * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException - * @expectedExceptionMessage Cannot autowire service "a": argument "$r" of method "Symfony\Component\DependencyInjection\Tests\Compiler\BadParentTypeHintedArgument::__construct()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\OptionalServiceClass" but this class does not exist. + * @expectedExceptionMessage Cannot autowire service "a": argument "$r" of method "Symfony\Component\DependencyInjection\Tests\Compiler\BadParentTypeHintedArgument::__construct()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\OptionalServiceClass" but this class cannot be loaded. */ public function testParentClassNotFoundThrowsException() { @@ -744,7 +744,7 @@ class AutowirePassTest extends TestCase public function provideNotWireableCalls() { 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 does not exist.'), + 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.'), ); From 305ae5e5d47de6746aa93af964980fee44bbc916 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 18 Jul 2017 18:05:00 +0200 Subject: [PATCH 06/22] [VarDumper] Use "C" locale when using "comma" flags --- src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php b/src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php index b266258623..0f42d8a6c4 100644 --- a/src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php +++ b/src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php @@ -123,6 +123,10 @@ abstract class AbstractDumper implements DataDumperInterface, DumperInterface */ public function dump(Data $data, $output = null) { + if ($locale = $this->flags & (self::DUMP_COMMA_SEPARATOR | self::DUMP_TRAILING_COMMA) ? setlocale(LC_NUMERIC, 0) : null) { + setlocale(LC_NUMERIC, 'C'); + } + if ($returnDump = true === $output) { $output = fopen('php://memory', 'r+b'); } @@ -143,6 +147,9 @@ abstract class AbstractDumper implements DataDumperInterface, DumperInterface if ($output) { $this->setOutput($prevOutput); } + if ($locale) { + setlocale(LC_NUMERIC, $locale); + } } } From eed8a5143d324d5de1e5c50d2215c9a39d365d1d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 18 Jul 2017 17:30:43 +0200 Subject: [PATCH 07/22] [VarDumper] Move locale sniffing to dump() time --- src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php b/src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php index bed08bf47e..abbe962954 100644 --- a/src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php +++ b/src/Symfony/Component/VarDumper/Dumper/AbstractDumper.php @@ -39,8 +39,8 @@ abstract class AbstractDumper implements DataDumperInterface, DumperInterface public function __construct($output = null, $charset = null) { $this->setCharset($charset ?: ini_get('php.output_encoding') ?: ini_get('default_charset') ?: 'UTF-8'); - $this->decimalPoint = (string) 0.5; - $this->decimalPoint = $this->decimalPoint[1]; + $this->decimalPoint = localeconv(); + $this->decimalPoint = $this->decimalPoint['decimal_point']; $this->setOutput($output ?: static::$defaultOutput); if (!$output && is_string(static::$defaultOutput)) { static::$defaultOutput = $this->outputStream; @@ -134,6 +134,9 @@ abstract class AbstractDumper implements DataDumperInterface, DumperInterface */ public function dump(Data $data, $output = null) { + $this->decimalPoint = localeconv(); + $this->decimalPoint = $this->decimalPoint['decimal_point']; + $exception = null; if ($output) { $prevOutput = $this->setOutput($output); From 04b7b04b65b7df233b56061b8dd3337317ecb544 Mon Sep 17 00:00:00 2001 From: Shaun Simmons Date: Tue, 18 Jul 2017 17:34:47 -0400 Subject: [PATCH 08/22] Change "this" to "that" to avoid confusion --- .../DependencyInjection/Compiler/PriorityTaggedServiceTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php index 64c041e93f..04e6cc67a5 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php @@ -26,7 +26,7 @@ trait PriorityTaggedServiceTrait * * The order of additions must be respected for services having the same priority, * and knowing that the \SplPriorityQueue class does not respect the FIFO method, - * we should not use this class. + * we should not use that class. * * @see https://bugs.php.net/bug.php?id=53710 * @see https://bugs.php.net/bug.php?id=60926 From c0556cb20447208e277e26a851680802983e0ab9 Mon Sep 17 00:00:00 2001 From: David Maicher Date: Mon, 17 Jul 2017 22:35:32 +0200 Subject: [PATCH 09/22] [FrameworkBundle] fix ValidatorCacheWarmer: use serializing ArrayAdapter --- .../AbstractPhpFileCacheWarmer.php | 92 +++++++++++++++++++ .../CacheWarmer/AnnotationsCacheWarmer.php | 73 ++++----------- .../CacheWarmer/SerializerCacheWarmer.php | 59 +++--------- .../CacheWarmer/ValidatorCacheWarmer.php | 62 ++++--------- 4 files changed, 142 insertions(+), 144 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php new file mode 100644 index 0000000000..25801a7829 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\Cache\Adapter\ProxyAdapter; +use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; + +/** + * @internal + */ +abstract class AbstractPhpFileCacheWarmer implements CacheWarmerInterface +{ + private $phpArrayFile; + private $fallbackPool; + + /** + * @param string $phpArrayFile The PHP file where metadata are cached + * @param CacheItemPoolInterface $fallbackPool The pool where runtime-discovered metadata are cached + */ + public function __construct($phpArrayFile, CacheItemPoolInterface $fallbackPool) + { + $this->phpArrayFile = $phpArrayFile; + if (!$fallbackPool instanceof AdapterInterface) { + $fallbackPool = new ProxyAdapter($fallbackPool); + } + $this->fallbackPool = $fallbackPool; + } + + /** + * {@inheritdoc} + */ + public function isOptional() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function warmUp($cacheDir) + { + $arrayAdapter = new ArrayAdapter(); + + spl_autoload_register(array(PhpArrayAdapter::class, 'throwOnRequiredClass')); + try { + if (!$this->doWarmUp($cacheDir, $arrayAdapter)) { + return; + } + } finally { + spl_autoload_unregister(array(PhpArrayAdapter::class, 'throwOnRequiredClass')); + } + + // the ArrayAdapter stores the values serialized + // to avoid mutation of the data after it was written to the cache + // so here we un-serialize the values first + $values = array_map(function ($val) { return null !== $val ? unserialize($val) : null; }, $arrayAdapter->getValues()); + + $this->warmUpPhpArrayAdapter(new PhpArrayAdapter($this->phpArrayFile, $this->fallbackPool), $values); + + foreach ($values as $k => $v) { + $item = $this->fallbackPool->getItem($k); + $this->fallbackPool->saveDeferred($item->set($v)); + } + $this->fallbackPool->commit(); + } + + protected function warmUpPhpArrayAdapter(PhpArrayAdapter $phpArrayAdapter, array $values) + { + $phpArrayAdapter->warmUp($values); + } + + /** + * @param string $cacheDir + * @param ArrayAdapter $arrayAdapter + * + * @return bool false if there is nothing to warm-up + */ + abstract protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter); +} diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php index a6fb4ed095..4026b53bd7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php @@ -15,12 +15,8 @@ use Doctrine\Common\Annotations\AnnotationException; use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Annotations\Reader; use Psr\Cache\CacheItemPoolInterface; -use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; -use Symfony\Component\Cache\Adapter\PhpArrayAdapter; -use Symfony\Component\Cache\Adapter\ProxyAdapter; use Symfony\Component\Cache\DoctrineProvider; -use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; /** * Warms up annotation caches for classes found in composer's autoload class map @@ -28,11 +24,9 @@ use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; * * @author Titouan Galopin */ -class AnnotationsCacheWarmer implements CacheWarmerInterface +class AnnotationsCacheWarmer extends AbstractPhpFileCacheWarmer { private $annotationReader; - private $phpArrayFile; - private $fallbackPool; /** * @param Reader $annotationReader @@ -41,70 +35,41 @@ class AnnotationsCacheWarmer implements CacheWarmerInterface */ public function __construct(Reader $annotationReader, $phpArrayFile, CacheItemPoolInterface $fallbackPool) { + parent::__construct($phpArrayFile, $fallbackPool); $this->annotationReader = $annotationReader; - $this->phpArrayFile = $phpArrayFile; - if (!$fallbackPool instanceof AdapterInterface) { - $fallbackPool = new ProxyAdapter($fallbackPool); - } - $this->fallbackPool = $fallbackPool; } /** * {@inheritdoc} */ - public function warmUp($cacheDir) + protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) { - $adapter = new PhpArrayAdapter($this->phpArrayFile, $this->fallbackPool); $annotatedClassPatterns = $cacheDir.'/annotations.map'; if (!is_file($annotatedClassPatterns)) { - $adapter->warmUp(array()); - - return; + return true; } $annotatedClasses = include $annotatedClassPatterns; + $reader = new CachedReader($this->annotationReader, new DoctrineProvider($arrayAdapter)); - $arrayPool = new ArrayAdapter(0, false); - $reader = new CachedReader($this->annotationReader, new DoctrineProvider($arrayPool)); - - spl_autoload_register(array($adapter, 'throwOnRequiredClass')); - try { - foreach ($annotatedClasses as $class) { - try { - $this->readAllComponents($reader, $class); - } catch (\ReflectionException $e) { - // ignore failing reflection - } catch (AnnotationException $e) { - /* - * Ignore any AnnotationException to not break the cache warming process if an Annotation is badly - * configured or could not be found / read / etc. - * - * In particular cases, an Annotation in your code can be used and defined only for a specific - * environment but is always added to the annotations.map file by some Symfony default behaviors, - * and you always end up with a not found Annotation. - */ - } + foreach ($annotatedClasses as $class) { + try { + $this->readAllComponents($reader, $class); + } catch (\ReflectionException $e) { + // ignore failing reflection + } catch (AnnotationException $e) { + /* + * Ignore any AnnotationException to not break the cache warming process if an Annotation is badly + * configured or could not be found / read / etc. + * + * In particular cases, an Annotation in your code can be used and defined only for a specific + * environment but is always added to the annotations.map file by some Symfony default behaviors, + * and you always end up with a not found Annotation. + */ } - } finally { - spl_autoload_unregister(array($adapter, 'throwOnRequiredClass')); } - $values = $arrayPool->getValues(); - $adapter->warmUp($values); - - foreach ($values as $k => $v) { - $item = $this->fallbackPool->getItem($k); - $this->fallbackPool->saveDeferred($item->set($v)); - } - $this->fallbackPool->commit(); - } - - /** - * {@inheritdoc} - */ - public function isOptional() - { return true; } diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php index c017f51268..75cc2bcf9b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php @@ -13,11 +13,7 @@ namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; use Doctrine\Common\Annotations\AnnotationException; use Psr\Cache\CacheItemPoolInterface; -use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; -use Symfony\Component\Cache\Adapter\PhpArrayAdapter; -use Symfony\Component\Cache\Adapter\ProxyAdapter; -use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; @@ -30,11 +26,9 @@ use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; * * @author Titouan Galopin */ -class SerializerCacheWarmer implements CacheWarmerInterface +class SerializerCacheWarmer extends AbstractPhpFileCacheWarmer { private $loaders; - private $phpArrayFile; - private $fallbackPool; /** * @param LoaderInterface[] $loaders The serializer metadata loaders @@ -43,60 +37,33 @@ class SerializerCacheWarmer implements CacheWarmerInterface */ public function __construct(array $loaders, $phpArrayFile, CacheItemPoolInterface $fallbackPool) { + parent::__construct($phpArrayFile, $fallbackPool); $this->loaders = $loaders; - $this->phpArrayFile = $phpArrayFile; - if (!$fallbackPool instanceof AdapterInterface) { - $fallbackPool = new ProxyAdapter($fallbackPool); - } - $this->fallbackPool = $fallbackPool; } /** * {@inheritdoc} */ - public function warmUp($cacheDir) + protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) { if (!class_exists(CacheClassMetadataFactory::class) || !method_exists(XmlFileLoader::class, 'getMappedClasses') || !method_exists(YamlFileLoader::class, 'getMappedClasses')) { - return; + return false; } - $adapter = new PhpArrayAdapter($this->phpArrayFile, $this->fallbackPool); - $arrayPool = new ArrayAdapter(0, false); + $metadataFactory = new CacheClassMetadataFactory(new ClassMetadataFactory(new LoaderChain($this->loaders)), $arrayAdapter); - $metadataFactory = new CacheClassMetadataFactory(new ClassMetadataFactory(new LoaderChain($this->loaders)), $arrayPool); - - spl_autoload_register(array($adapter, 'throwOnRequiredClass')); - try { - foreach ($this->extractSupportedLoaders($this->loaders) as $loader) { - foreach ($loader->getMappedClasses() as $mappedClass) { - try { - $metadataFactory->getMetadataFor($mappedClass); - } catch (\ReflectionException $e) { - // ignore failing reflection - } catch (AnnotationException $e) { - // ignore failing annotations - } + foreach ($this->extractSupportedLoaders($this->loaders) as $loader) { + foreach ($loader->getMappedClasses() as $mappedClass) { + try { + $metadataFactory->getMetadataFor($mappedClass); + } catch (\ReflectionException $e) { + // ignore failing reflection + } catch (AnnotationException $e) { + // ignore failing annotations } } - } finally { - spl_autoload_unregister(array($adapter, 'throwOnRequiredClass')); } - $values = $arrayPool->getValues(); - $adapter->warmUp($values); - - foreach ($values as $k => $v) { - $item = $this->fallbackPool->getItem($k); - $this->fallbackPool->saveDeferred($item->set($v)); - } - $this->fallbackPool->commit(); - } - - /** - * {@inheritdoc} - */ - public function isOptional() - { return true; } diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php index 81291d772f..20f76cb4fd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php @@ -13,11 +13,8 @@ namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; use Doctrine\Common\Annotations\AnnotationException; use Psr\Cache\CacheItemPoolInterface; -use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; -use Symfony\Component\Cache\Adapter\ProxyAdapter; -use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; use Symfony\Component\Validator\Mapping\Cache\Psr6Cache; use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory; use Symfony\Component\Validator\Mapping\Loader\LoaderChain; @@ -31,11 +28,9 @@ use Symfony\Component\Validator\ValidatorBuilderInterface; * * @author Titouan Galopin */ -class ValidatorCacheWarmer implements CacheWarmerInterface +class ValidatorCacheWarmer extends AbstractPhpFileCacheWarmer { private $validatorBuilder; - private $phpArrayFile; - private $fallbackPool; /** * @param ValidatorBuilderInterface $validatorBuilder @@ -44,64 +39,43 @@ class ValidatorCacheWarmer implements CacheWarmerInterface */ public function __construct(ValidatorBuilderInterface $validatorBuilder, $phpArrayFile, CacheItemPoolInterface $fallbackPool) { + parent::__construct($phpArrayFile, $fallbackPool); $this->validatorBuilder = $validatorBuilder; - $this->phpArrayFile = $phpArrayFile; - if (!$fallbackPool instanceof AdapterInterface) { - $fallbackPool = new ProxyAdapter($fallbackPool); - } - $this->fallbackPool = $fallbackPool; } /** * {@inheritdoc} */ - public function warmUp($cacheDir) + protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) { if (!method_exists($this->validatorBuilder, 'getLoaders')) { - return; + return false; } - $adapter = new PhpArrayAdapter($this->phpArrayFile, $this->fallbackPool); - $arrayPool = new ArrayAdapter(0, false); - $loaders = $this->validatorBuilder->getLoaders(); - $metadataFactory = new LazyLoadingMetadataFactory(new LoaderChain($loaders), new Psr6Cache($arrayPool)); + $metadataFactory = new LazyLoadingMetadataFactory(new LoaderChain($loaders), new Psr6Cache($arrayAdapter)); - spl_autoload_register(array($adapter, 'throwOnRequiredClass')); - try { - foreach ($this->extractSupportedLoaders($loaders) as $loader) { - foreach ($loader->getMappedClasses() as $mappedClass) { - try { - if ($metadataFactory->hasMetadataFor($mappedClass)) { - $metadataFactory->getMetadataFor($mappedClass); - } - } catch (\ReflectionException $e) { - // ignore failing reflection - } catch (AnnotationException $e) { - // ignore failing annotations + foreach ($this->extractSupportedLoaders($loaders) as $loader) { + foreach ($loader->getMappedClasses() as $mappedClass) { + try { + if ($metadataFactory->hasMetadataFor($mappedClass)) { + $metadataFactory->getMetadataFor($mappedClass); } + } catch (\ReflectionException $e) { + // ignore failing reflection + } catch (AnnotationException $e) { + // ignore failing annotations } } - } finally { - spl_autoload_unregister(array($adapter, 'throwOnRequiredClass')); } - $values = $arrayPool->getValues(); - $adapter->warmUp(array_filter($values)); - - foreach ($values as $k => $v) { - $item = $this->fallbackPool->getItem($k); - $this->fallbackPool->saveDeferred($item->set($v)); - } - $this->fallbackPool->commit(); + return true; } - /** - * {@inheritdoc} - */ - public function isOptional() + protected function warmUpPhpArrayAdapter(PhpArrayAdapter $phpArrayAdapter, array $values) { - return true; + // make sure we don't cache null values + parent::warmUpPhpArrayAdapter($phpArrayAdapter, array_filter($values)); } /** From 295fda9a31e66463570a9fd92c0076d772ab9503 Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Tue, 18 Jul 2017 19:41:39 +0200 Subject: [PATCH 10/22] [WebProfilerBundle][TwigBundle] Fix infinite js loop on exception pages --- .../TwigBundle/Resources/views/base_js.html.twig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/base_js.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/base_js.html.twig index 4ce8013c44..036af2b025 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/base_js.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/base_js.html.twig @@ -512,14 +512,14 @@ var altContent = toggle.getAttribute('data-toggle-alt-content'); toggle.innerHTML = currentContent !== altContent ? altContent : originalContent; }); + } - /* Prevents from disallowing clicks on links inside toggles */ - var toggleLinks = document.querySelectorAll('.sf-toggle a'); - for (var i = 0; i < toggleLinks.length; i++) { - addEventListener(toggleLinks[i], 'click', function(e) { - e.stopPropagation(); - }); - } + /* Prevents from disallowing clicks on links inside toggles */ + var toggleLinks = document.querySelectorAll('.sf-toggle a'); + for (var i = 0; i < toggleLinks.length; i++) { + addEventListener(toggleLinks[i], 'click', function(e) { + e.stopPropagation(); + }); } } }; From 338761245100e09dafdce47f7646ef7d4fc14f64 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 19 Jul 2017 07:07:01 +0200 Subject: [PATCH 11/22] [Security] refactored tests --- ...efaultAuthenticationSuccessHandlerTest.php | 80 +++++++------------ 1 file changed, 31 insertions(+), 49 deletions(-) diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php index 2c133014cc..70836170c7 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php @@ -12,31 +12,28 @@ namespace Symfony\Component\Security\Http\Tests\Authentication; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler; class DefaultAuthenticationSuccessHandlerTest extends TestCase { private $httpUtils = null; - - private $request = null; - private $token = null; protected function setUp() { $this->httpUtils = $this->getMockBuilder('Symfony\Component\Security\Http\HttpUtils')->getMock(); - $this->request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->getMock(); - $this->request->headers = $this->getMockBuilder('Symfony\Component\HttpFoundation\HeaderBag')->getMock(); $this->token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); } public function testRequestIsRedirected() { - $response = $this->expectRedirectResponse('/'); + $request = Request::create('/'); + $response = $this->expectRedirectResponse($request, '/'); $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, array()); - $result = $handler->onAuthenticationSuccess($this->request, $this->token); + $result = $handler->onAuthenticationSuccess($request, $this->token); $this->assertSame($response, $result); } @@ -48,24 +45,22 @@ class DefaultAuthenticationSuccessHandlerTest extends TestCase 'default_target_path' => '/dashboard', ); - $response = $this->expectRedirectResponse('/dashboard'); + $request = Request::create('/'); + $response = $this->expectRedirectResponse($request, '/dashboard'); $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, $options); - $result = $handler->onAuthenticationSuccess($this->request, $this->token); + $result = $handler->onAuthenticationSuccess($request, $this->token); $this->assertSame($response, $result); } public function testTargetPathIsPassedWithRequest() { - $this->request->expects($this->once()) - ->method('get')->with('_target_path') - ->will($this->returnValue('/dashboard')); - - $response = $this->expectRedirectResponse('/dashboard'); + $request = Request::create('/?_target_path=/dashboard'); + $response = $this->expectRedirectResponse($request, '/dashboard'); $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, array()); - $result = $handler->onAuthenticationSuccess($this->request, $this->token); + $result = $handler->onAuthenticationSuccess($request, $this->token); $this->assertSame($response, $result); } @@ -73,15 +68,11 @@ class DefaultAuthenticationSuccessHandlerTest extends TestCase public function testTargetPathParameterIsCustomised() { $options = array('target_path_parameter' => '_my_target_path'); - - $this->request->expects($this->once()) - ->method('get')->with('_my_target_path') - ->will($this->returnValue('/dashboard')); - - $response = $this->expectRedirectResponse('/dashboard'); + $request = Request::create('/?_my_target_path=/dashboard'); + $response = $this->expectRedirectResponse($request, '/dashboard'); $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, $options); - $result = $handler->onAuthenticationSuccess($this->request, $this->token); + $result = $handler->onAuthenticationSuccess($request, $this->token); $this->assertSame($response, $result); } @@ -95,16 +86,14 @@ class DefaultAuthenticationSuccessHandlerTest extends TestCase $session->expects($this->once()) ->method('remove')->with('_security.admin.target_path'); - $this->request->expects($this->any()) - ->method('getSession') - ->will($this->returnValue($session)); - - $response = $this->expectRedirectResponse('/admin/dashboard'); + $request = Request::create('/?_my_target_path=/dashboard'); + $request->setSession($session); + $response = $this->expectRedirectResponse($request, '/admin/dashboard'); $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, array()); $handler->setProviderKey('admin'); - $result = $handler->onAuthenticationSuccess($this->request, $this->token); + $result = $handler->onAuthenticationSuccess($request, $this->token); $this->assertSame($response, $result); } @@ -112,15 +101,12 @@ class DefaultAuthenticationSuccessHandlerTest extends TestCase public function testTargetPathIsPassedAsReferer() { $options = array('use_referer' => true); - - $this->request->headers->expects($this->once()) - ->method('get')->with('Referer') - ->will($this->returnValue('/dashboard')); - - $response = $this->expectRedirectResponse('/dashboard'); + $request = Request::create('/'); + $request->headers->set('Referer', '/dashboard'); + $response = $this->expectRedirectResponse($request, '/dashboard'); $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, $options); - $result = $handler->onAuthenticationSuccess($this->request, $this->token); + $result = $handler->onAuthenticationSuccess($request, $this->token); $this->assertSame($response, $result); } @@ -128,41 +114,37 @@ class DefaultAuthenticationSuccessHandlerTest extends TestCase public function testRefererHasToBeDifferentThatLoginUrl() { $options = array('use_referer' => true); - - $this->request->headers->expects($this->any()) - ->method('get')->with('Referer') - ->will($this->returnValue('/login')); - + $request = Request::create('/'); + $request->headers->set('Referer', '/login'); $this->httpUtils->expects($this->once()) - ->method('generateUri')->with($this->request, '/login') + ->method('generateUri')->with($request, '/login') ->will($this->returnValue('/login')); - $response = $this->expectRedirectResponse('/'); + $response = $this->expectRedirectResponse($request, '/'); $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, $options); - $result = $handler->onAuthenticationSuccess($this->request, $this->token); + $result = $handler->onAuthenticationSuccess($request, $this->token); $this->assertSame($response, $result); } public function testRefererTargetPathIsIgnoredByDefault() { - $this->request->headers->expects($this->never())->method('get'); - - $response = $this->expectRedirectResponse('/'); + $request = Request::create('/'); + $response = $this->expectRedirectResponse($request, '/'); $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, array()); - $result = $handler->onAuthenticationSuccess($this->request, $this->token); + $result = $handler->onAuthenticationSuccess($request, $this->token); $this->assertSame($response, $result); } - private function expectRedirectResponse($path) + private function expectRedirectResponse(Request $request, $path) { $response = new Response(); $this->httpUtils->expects($this->once()) ->method('createRedirectResponse') - ->with($this->request, $path) + ->with($request, $path) ->will($this->returnValue($response)); return $response; From b1f1ae26b47e6aaf5282584d880724fed66a86df Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 19 Jul 2017 07:42:44 +0200 Subject: [PATCH 12/22] [Security] simplified tests --- ...efaultAuthenticationSuccessHandlerTest.php | 193 +++++++----------- 1 file changed, 69 insertions(+), 124 deletions(-) diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php index 70836170c7..8d39e63cd5 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php @@ -15,138 +15,83 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler; +use Symfony\Component\Security\Http\HttpUtils; class DefaultAuthenticationSuccessHandlerTest extends TestCase { - private $httpUtils = null; - private $token = null; - - protected function setUp() + /** + * @dataProvider getRequestRedirections + */ + public function testRequestRedirections(Request $request, $options, $redirectedUrl) { - $this->httpUtils = $this->getMockBuilder('Symfony\Component\Security\Http\HttpUtils')->getMock(); - $this->token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); + $httpUtils = new HttpUtils(); + $token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); + $handler = new DefaultAuthenticationSuccessHandler($httpUtils, $options); + if ($request->hasSession()) { + $handler->setProviderKey('admin'); + } + $this->assertSame('http://localhost'.$redirectedUrl, $handler->onAuthenticationSuccess($request, $token)->getTargetUrl()); } - public function testRequestIsRedirected() - { - $request = Request::create('/'); - $response = $this->expectRedirectResponse($request, '/'); - - $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, array()); - $result = $handler->onAuthenticationSuccess($request, $this->token); - - $this->assertSame($response, $result); - } - - public function testDefaultTargetPathCanBeForced() - { - $options = array( - 'always_use_default_target_path' => true, - 'default_target_path' => '/dashboard', - ); - - $request = Request::create('/'); - $response = $this->expectRedirectResponse($request, '/dashboard'); - - $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, $options); - $result = $handler->onAuthenticationSuccess($request, $this->token); - - $this->assertSame($response, $result); - } - - public function testTargetPathIsPassedWithRequest() - { - $request = Request::create('/?_target_path=/dashboard'); - $response = $this->expectRedirectResponse($request, '/dashboard'); - - $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, array()); - $result = $handler->onAuthenticationSuccess($request, $this->token); - - $this->assertSame($response, $result); - } - - public function testTargetPathParameterIsCustomised() - { - $options = array('target_path_parameter' => '_my_target_path'); - $request = Request::create('/?_my_target_path=/dashboard'); - $response = $this->expectRedirectResponse($request, '/dashboard'); - - $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, $options); - $result = $handler->onAuthenticationSuccess($request, $this->token); - - $this->assertSame($response, $result); - } - - public function testTargetPathIsTakenFromTheSession() + public function getRequestRedirections() { $session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface')->getMock(); - $session->expects($this->once()) - ->method('get')->with('_security.admin.target_path') - ->will($this->returnValue('/admin/dashboard')); - $session->expects($this->once()) - ->method('remove')->with('_security.admin.target_path'); + $session->expects($this->once())->method('get')->with('_security.admin.target_path')->will($this->returnValue('/admin/dashboard')); + $session->expects($this->once())->method('remove')->with('_security.admin.target_path'); + $requestWithSession = Request::create('/'); + $requestWithSession->setSession($session); - $request = Request::create('/?_my_target_path=/dashboard'); - $request->setSession($session); - $response = $this->expectRedirectResponse($request, '/admin/dashboard'); - - $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, array()); - $handler->setProviderKey('admin'); - - $result = $handler->onAuthenticationSuccess($request, $this->token); - - $this->assertSame($response, $result); - } - - public function testTargetPathIsPassedAsReferer() - { - $options = array('use_referer' => true); - $request = Request::create('/'); - $request->headers->set('Referer', '/dashboard'); - $response = $this->expectRedirectResponse($request, '/dashboard'); - - $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, $options); - $result = $handler->onAuthenticationSuccess($request, $this->token); - - $this->assertSame($response, $result); - } - - public function testRefererHasToBeDifferentThatLoginUrl() - { - $options = array('use_referer' => true); - $request = Request::create('/'); - $request->headers->set('Referer', '/login'); - $this->httpUtils->expects($this->once()) - ->method('generateUri')->with($request, '/login') - ->will($this->returnValue('/login')); - - $response = $this->expectRedirectResponse($request, '/'); - - $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, $options); - $result = $handler->onAuthenticationSuccess($request, $this->token); - - $this->assertSame($response, $result); - } - - public function testRefererTargetPathIsIgnoredByDefault() - { - $request = Request::create('/'); - $response = $this->expectRedirectResponse($request, '/'); - - $handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, array()); - $result = $handler->onAuthenticationSuccess($request, $this->token); - - $this->assertSame($response, $result); - } - - private function expectRedirectResponse(Request $request, $path) - { - $response = new Response(); - $this->httpUtils->expects($this->once()) - ->method('createRedirectResponse') - ->with($request, $path) - ->will($this->returnValue($response)); - - return $response; + return array( + 'default' => array( + Request::create('/'), + array(), + '/', + ), + 'forced target path' => array( + Request::create('/'), + array('always_use_default_target_path' => true, 'default_target_path' => '/dashboard'), + '/dashboard', + ), + 'target path as query string' => array( + Request::create('/?_target_path=/dashboard'), + array(), + '/dashboard', + ), + 'target path name as query string is customized' => array( + Request::create('/?_my_target_path=/dashboard'), + array('target_path_parameter' => '_my_target_path'), + '/dashboard', + ), + 'target path name as query string is customized and nested' => array( + Request::create('/?_target_path[value]=/dashboard'), + array('target_path_parameter' => '_target_path[value]'), + '/dashboard', + ), + 'target path in session' => array( + $requestWithSession, + array(), + '/admin/dashboard', + ), + 'target path as referer' => array( + Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/dashboard')), + array('use_referer' => true), + '/dashboard', + ), + 'target path as referer is ignored if not configured' => array( + Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/dashboard')), + array(), + '/', + ), + 'target path should be different than login URL' => array( + Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/login')), + array('use_referer' => true, 'login_path' => '/login'), + '/', + ), + 'target path should be different than login URL (query string does not matter)' => array( + Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/login?t=1&p=2')), + array('use_referer' => true, 'login_path' => '/login'), + '/', + ), + ); } } From 9c7a1406cb81f4d1ccd7e3521347e2d847b7467c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 19 Jul 2017 07:57:00 +0200 Subject: [PATCH 13/22] [Security] fixed default target path when referer contains a query string --- .../DefaultAuthenticationSuccessHandler.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php b/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php index b6a7df5062..7da6e35572 100644 --- a/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php +++ b/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php @@ -118,8 +118,14 @@ class DefaultAuthenticationSuccessHandler implements AuthenticationSuccessHandle return $targetUrl; } - if ($this->options['use_referer'] && ($targetUrl = $request->headers->get('Referer')) && $targetUrl !== $this->httpUtils->generateUri($request, $this->options['login_path'])) { - return $targetUrl; + if ($this->options['use_referer']) { + $targetUrl = $request->headers->get('Referer'); + if (false !== $pos = strpos($targetUrl, '?')) { + $targetUrl = substr($targetUrl, 0, $pos); + } + if ($targetUrl !== $this->httpUtils->generateUri($request, $this->options['login_path'])) { + return $targetUrl; + } } return $this->options['default_target_path']; From 022ac0be096a303aa4de7be64b84be9ffee3ecd9 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 19 Jul 2017 08:04:16 +0200 Subject: [PATCH 14/22] [Security] added more tests --- ...DefaultAuthenticationSuccessHandlerTest.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php index 8d39e63cd5..b42f840358 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php @@ -13,7 +13,6 @@ namespace Symfony\Component\Security\Http\Tests\Authentication; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler; use Symfony\Component\Security\Http\HttpUtils; @@ -24,7 +23,9 @@ class DefaultAuthenticationSuccessHandlerTest extends TestCase */ public function testRequestRedirections(Request $request, $options, $redirectedUrl) { - $httpUtils = new HttpUtils(); + $urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock(); + $urlGenerator->expects($this->any())->method('generate')->will($this->returnValue('http://localhost/login')); + $httpUtils = new HttpUtils($urlGenerator); $token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); $handler = new DefaultAuthenticationSuccessHandler($httpUtils, $options); if ($request->hasSession()) { @@ -73,25 +74,30 @@ class DefaultAuthenticationSuccessHandlerTest extends TestCase '/admin/dashboard', ), 'target path as referer' => array( - Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/dashboard')), + Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/dashboard')), array('use_referer' => true), '/dashboard', ), 'target path as referer is ignored if not configured' => array( - Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/dashboard')), + Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/dashboard')), array(), '/', ), 'target path should be different than login URL' => array( - Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/login')), + Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/login')), array('use_referer' => true, 'login_path' => '/login'), '/', ), 'target path should be different than login URL (query string does not matter)' => array( - Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/login?t=1&p=2')), + Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/login?t=1&p=2')), array('use_referer' => true, 'login_path' => '/login'), '/', ), + 'target path should be different than login URL (login_path as a route)' => array( + Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/login?t=1&p=2')), + array('use_referer' => true, 'login_path' => 'login_route'), + '/', + ), ); } } From 535208042a6afa33c2043ab766bfcd1a2ced17ea Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 19 Jul 2017 08:45:45 +0200 Subject: [PATCH 15/22] document the TwigRenderer class deprecation --- UPGRADE-3.4.md | 6 ++++++ UPGRADE-4.0.md | 3 +++ 2 files changed, 9 insertions(+) diff --git a/UPGRADE-3.4.md b/UPGRADE-3.4.md index 1c3993685d..0e1e966d66 100644 --- a/UPGRADE-3.4.md +++ b/UPGRADE-3.4.md @@ -77,6 +77,12 @@ SecurityBundle * `FirewallContext::getListeners()` now returns `\Traversable|array` +TwigBridge +---------- + + * deprecated the `Symfony\Bridge\Twig\Form\TwigRenderer` class, use the `FormRenderer` + class from the Form component instead + Validator --------- diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index 3b11ad422f..1a7a254237 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -514,6 +514,9 @@ TwigBundle TwigBridge ---------- + * removed the `Symfony\Bridge\Twig\Form\TwigRenderer` class, use the `FormRenderer` + class from the Form component instead + * Removed the possibility to inject the Form `TwigRenderer` into the `FormExtension`. Upgrade Twig to `^1.30`, inject the `Twig_Environment` into the `TwigRendererEngine` and load the `TwigRenderer` using the `Twig_FactoryRuntimeLoader` instead. From fa0b9428c5c895f14f60d7f5544a29aeba725f62 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 19 Jul 2017 09:37:29 +0200 Subject: [PATCH 16/22] [Config] Minor fix --- .../Component/Config/Resource/ClassExistenceResource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Config/Resource/ClassExistenceResource.php b/src/Symfony/Component/Config/Resource/ClassExistenceResource.php index ada9a89b94..623124ab8d 100644 --- a/src/Symfony/Component/Config/Resource/ClassExistenceResource.php +++ b/src/Symfony/Component/Config/Resource/ClassExistenceResource.php @@ -149,7 +149,7 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ $props = array( 'file' => $trace[$i]['file'], 'line' => $trace[$i]['line'], - 'trace' => array_slice($trace, 0, 1 + $i), + 'trace' => array_slice($trace, 1 + $i), ); foreach ($props as $p => $v) { From 25a47d8796e584348bec48822fa351f864b5a690 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Wed, 19 Jul 2017 10:54:05 +0200 Subject: [PATCH 17/22] Fix the design of the profiler exceptions when there is no message --- .../Resources/views/Exception/exception.html.twig | 9 ++++----- .../Bundle/TwigBundle/Resources/views/exception.css.twig | 3 ++- .../Resources/views/Collector/exception.css.twig | 5 ++++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig index 8ed5226ad5..d42a9dec47 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig @@ -1,4 +1,4 @@ -
+
- {% if exception.message is not empty %} -
-
+ +
+

{{- exception.message|nl2br|format_file_from_text -}}

@@ -25,7 +25,6 @@
- {% endif %}
diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig index ba3dd9412e..9d03015f2f 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig @@ -79,7 +79,8 @@ header .container { display: flex; justify-content: space-between; } .exception-hierarchy .icon { margin: 0 3px; opacity: .7; } .exception-hierarchy .icon svg { height: 13px; width: 13px; vertical-align: -2px; } -.exception-message-wrapper { display: flex; align-items: flex-start; min-height: 70px; padding: 10px 0 8px; } +.exception-without-message .exception-message-wrapper { display: none; } +.exception-message-wrapper .container { display: flex; align-items: flex-start; min-height: 70px; padding: 10px 0 8px; } .exception-message { flex-grow: 1; } .exception-message, .exception-message a { color: #FFF; font-size: 21px; font-weight: 400; margin: 0; } .exception-message.long { font-size: 18px; } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.css.twig index 8a64ed5936..c849cb2966 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.css.twig @@ -16,6 +16,9 @@ margin: 1em 0; padding: 10px; } +.exception-summary.exception-without-message { + display: none; +} .exception-message { color: #B0413E; @@ -26,6 +29,6 @@ display: none; } -.exception-message-wrapper { +.exception-message-wrapper .container { min-height: auto; } From 8a71aa31bbb1f6be8f7e507ff5273e9d775333b2 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Mon, 17 Jul 2017 18:13:39 +0200 Subject: [PATCH 18/22] Fix registering lazy command services with autoconfigure enabled --- .../DependencyInjection/AddConsoleCommandPass.php | 13 ++++--------- .../Component/Console/Tests/ApplicationTest.php | 5 +++-- .../AddConsoleCommandPassTest.php | 8 +++++--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php b/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php index 1ceed91573..67034098c2 100644 --- a/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php +++ b/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php @@ -70,20 +70,15 @@ class AddConsoleCommandPass implements CompilerPassInterface $serviceIds[$commandId] = false; $commandName = $tags[0]['command']; + unset($tags[0]); $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 (isset($tag['command'])) { + $aliases[] = $tag['command']; + $lazyCommandMap[$tag['command']] = $id; } } diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 7bf76ddc8e..4409d8dedf 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -1431,8 +1431,9 @@ class ApplicationTest extends TestCase $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')); + ->addTag('console.command', array('command' => 'lazy:command')) + ->addTag('console.command', array('command' => 'lazy:alias')) + ->addTag('console.command', array('command' => 'lazy:alias2')); $container->compile(); $application = new Application(); diff --git a/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php b/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php index 6454dd63df..596d9e0d2d 100644 --- a/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php +++ b/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php @@ -56,13 +56,14 @@ class AddConsoleCommandPassTest extends TestCase $this->assertSame(array($alias => $id), $container->getParameter('console.command.ids')); } - public function testProcessRegisterLazyCommands() + public function testProcessRegistersLazyCommands() { $container = new ContainerBuilder(); - $container + $command = $container ->register('my-command', MyCommand::class) ->setPublic(false) - ->addTag('console.command', array('command' => 'my:command', 'alias' => 'my:alias')) + ->addTag('console.command', array('command' => 'my:command')) + ->addTag('console.command', array('command' => 'my:alias')) ; (new AddConsoleCommandPass())->process($container); @@ -74,6 +75,7 @@ class AddConsoleCommandPassTest extends TestCase $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')); + $this->assertSame(array(array('setName', array('my:command')), array('setAliases', array(array('my:alias')))), $command->getMethodCalls()); } public function visibilityProvider() From 7ae578cc1acab2c44a6e2634cbbe59dc2518b3b5 Mon Sep 17 00:00:00 2001 From: Gavin Staniforth Date: Tue, 20 Jun 2017 16:21:01 +0100 Subject: [PATCH 19/22] fix(security): ensure the 'route' index is set before attempting to use it --- src/Symfony/Component/Security/Http/HttpUtils.php | 2 +- .../Component/Security/Http/Tests/HttpUtilsTest.php | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Security/Http/HttpUtils.php b/src/Symfony/Component/Security/Http/HttpUtils.php index ed737a2f61..56add79926 100644 --- a/src/Symfony/Component/Security/Http/HttpUtils.php +++ b/src/Symfony/Component/Security/Http/HttpUtils.php @@ -108,7 +108,7 @@ class HttpUtils $parameters = $this->urlMatcher->match($request->getPathInfo()); } - return $path === $parameters['_route']; + return isset($parameters['_route']) && $path === $parameters['_route']; } catch (MethodNotAllowedException $e) { return false; } catch (ResourceNotFoundException $e) { diff --git a/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php b/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php index 3d0e63b6fe..b508012665 100644 --- a/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php +++ b/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php @@ -221,6 +221,19 @@ class HttpUtilsTest extends TestCase $utils->checkRequestPath($this->getRequest(), 'foobar'); } + public function testCheckPathWithoutRouteParam() + { + $urlMatcher = $this->getMockBuilder('Symfony\Component\Routing\Matcher\UrlMatcherInterface')->getMock(); + $urlMatcher + ->expects($this->any()) + ->method('match') + ->willReturn(array('_controller' => 'PathController')) + ; + + $utils = new HttpUtils(null, $urlMatcher); + $this->assertFalse($utils->checkRequestPath($this->getRequest(), 'path/index.html')); + } + /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage Matcher must either implement UrlMatcherInterface or RequestMatcherInterface From ab91659ad6205157caf24ed5598886d64d128d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 19 Jul 2017 21:33:00 +0200 Subject: [PATCH 20/22] [PropertyInfo] Use rawurlencode to escape PSR-6 keys --- .../PropertyInfoCacheExtractor.php | 28 ++----------------- .../Tests/PropertyInfoCacheExtractorTest.php | 25 ----------------- 2 files changed, 2 insertions(+), 51 deletions(-) diff --git a/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php b/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php index 69ade35dfe..269b90810e 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php +++ b/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php @@ -106,7 +106,8 @@ class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface return call_user_func_array(array($this->propertyInfoExtractor, $method), $arguments); } - $key = $this->escape($method.'.'.$serializedArguments); + // Calling rawurlencode escapes special characters not allowed in PSR-6's keys + $key = rawurlencode($method.'.'.$serializedArguments); if (array_key_exists($key, $this->arrayCache)) { return $this->arrayCache[$key]; @@ -124,29 +125,4 @@ class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface return $this->arrayCache[$key] = $value; } - - /** - * Escapes a key according to PSR-6. - * - * Replaces characters forbidden by PSR-6 and the _ char by the _ char followed by the ASCII - * code of the escaped char. - * - * @param string $key - * - * @return string - */ - private function escape($key) - { - return strtr($key, array( - '{' => '_123', - '}' => '_125', - '(' => '_40', - ')' => '_41', - '/' => '_47', - '\\' => '_92', - '@' => '_64', - ':' => '_58', - '_' => '_95', - )); - } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php index 455d39fa96..d31a7bd51d 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php @@ -61,29 +61,4 @@ class PropertyInfoCacheExtractorTest extends AbstractPropertyInfoExtractorTest parent::testGetProperties(); parent::testGetProperties(); } - - /** - * @dataProvider escapeDataProvider - */ - public function testEscape($toEscape, $expected) - { - $reflectionMethod = new \ReflectionMethod($this->propertyInfo, 'escape'); - $reflectionMethod->setAccessible(true); - - $this->assertSame($expected, $reflectionMethod->invoke($this->propertyInfo, $toEscape)); - } - - public function escapeDataProvider() - { - return array( - array('foo_bar', 'foo_95bar'), - array('foo_95bar', 'foo_9595bar'), - array('foo{bar}', 'foo_123bar_125'), - array('foo(bar)', 'foo_40bar_41'), - array('foo/bar', 'foo_47bar'), - array('foo\bar', 'foo_92bar'), - array('foo@bar', 'foo_64bar'), - array('foo:bar', 'foo_58bar'), - ); - } } From 14c310f5fb8ee6cb0f7ae5c8c8f8aa5a894af844 Mon Sep 17 00:00:00 2001 From: Mike Francis Date: Wed, 19 Jul 2017 12:17:30 +0100 Subject: [PATCH 21/22] Fix case sensitive sameSite cookie --- src/Symfony/Component/HttpFoundation/Cookie.php | 4 ++++ src/Symfony/Component/HttpFoundation/Tests/CookieTest.php | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/Symfony/Component/HttpFoundation/Cookie.php b/src/Symfony/Component/HttpFoundation/Cookie.php index 492c6fbe75..5ea881c6e3 100644 --- a/src/Symfony/Component/HttpFoundation/Cookie.php +++ b/src/Symfony/Component/HttpFoundation/Cookie.php @@ -77,6 +77,10 @@ class Cookie $this->httpOnly = (bool) $httpOnly; $this->raw = (bool) $raw; + if (null !== $sameSite) { + $sameSite = strtolower($sameSite); + } + if (!in_array($sameSite, array(self::SAMESITE_LAX, self::SAMESITE_STRICT, null), true)) { throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.'); } diff --git a/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php b/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php index 3986a7e8f8..ad6d3f508c 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php @@ -180,4 +180,10 @@ class CookieTest extends TestCase $this->assertTrue($cookie->isRaw()); $this->assertEquals('foo=b+a+r; path=/', (string) $cookie); } + + public function testSameSiteAttributeIsCaseInsensitive() + { + $cookie = new Cookie('foo', 'bar', 0, '/', null, false, true, false, 'Lax'); + $this->assertEquals('lax', $cookie->getSameSite()); + } } From 5441b1ad38d4a42e799cdb26d665dd8c85ac53c8 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 21 Jul 2017 12:50:19 +0200 Subject: [PATCH 22/22] use Precise on Travis to keep PHP LDAP support --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 42cdc02bae..af674057da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: php +dist: precise sudo: false git: