Merge branch '3.4'

* 3.4: (22 commits)
  use Precise on Travis to keep PHP LDAP support
  Fix case sensitive sameSite cookie
  [PropertyInfo] Use rawurlencode to escape PSR-6 keys
  fix(security): ensure the 'route' index is set before attempting to use it
  Fix registering lazy command services with autoconfigure enabled
  Fix the design of the profiler exceptions when there is no message
  [Config] Minor fix
  document the TwigRenderer class deprecation
  [Security] added more tests
  [Security] fixed default target path when referer contains a query string
  [Security] simplified tests
  [Security] refactored tests
  [WebProfilerBundle][TwigBundle] Fix infinite js loop on exception pages
  [FrameworkBundle] fix ValidatorCacheWarmer: use serializing ArrayAdapter
  Change "this" to "that" to avoid confusion
  [VarDumper] Move locale sniffing to dump() time
  [VarDumper] Use "C" locale when using "comma" flags
  [Config] Make ClassExistenceResource throw on invalid parents
  [DebugBundle] Added min_depth to Configuration
  [Console] Add a factory command loader for standalone application with lazy-loading needs
  ...
This commit is contained in:
Nicolas Grekas 2017-07-21 13:08:07 +02:00
commit d024d79559
39 changed files with 621 additions and 440 deletions

View File

@ -1,5 +1,6 @@
language: php language: php
dist: precise
sudo: false sudo: false
git: git:

View File

@ -77,6 +77,12 @@ SecurityBundle
* `FirewallContext::getListeners()` now returns `\Traversable|array` * `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 Validator
--------- ---------

View File

@ -520,6 +520,9 @@ TwigBundle
TwigBridge 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`. * 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 Upgrade Twig to `^1.30`, inject the `Twig_Environment` into the `TwigRendererEngine` and load
the `TwigRenderer` using the `Twig_FactoryRuntimeLoader` instead. the `TwigRenderer` using the `Twig_FactoryRuntimeLoader` instead.

View File

@ -36,6 +36,11 @@ class Configuration implements ConfigurationInterface
->min(-1) ->min(-1)
->defaultValue(2500) ->defaultValue(2500)
->end() ->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') ->integerNode('max_string_length')
->info('Max length of displayed strings, -1 means no limit') ->info('Max length of displayed strings, -1 means no limit')
->min(-1) ->min(-1)

View File

@ -37,6 +37,7 @@ class DebugExtension extends Extension
$container->getDefinition('var_dumper.cloner') $container->getDefinition('var_dumper.cloner')
->addMethodCall('setMaxItems', array($config['max_items'])) ->addMethodCall('setMaxItems', array($config['max_items']))
->addMethodCall('setMinDepth', array($config['min_depth']))
->addMethodCall('setMaxString', array($config['max_string_length'])); ->addMethodCall('setMaxString', array($config['max_string_length']));
if (null !== $config['dump_destination']) { if (null !== $config['dump_destination']) {

View File

@ -8,6 +8,7 @@
<xsd:complexType name="config"> <xsd:complexType name="config">
<xsd:attribute name="max-items" type="xsd:integer" /> <xsd:attribute name="max-items" type="xsd:integer" />
<xsd:attribute name="min-depth" type="xsd:integer" />
<xsd:attribute name="max-string-length" type="xsd:integer" /> <xsd:attribute name="max-string-length" type="xsd:integer" />
<xsd:attribute name="dump-destination" type="xsd:string" /> <xsd:attribute name="dump-destination" type="xsd:string" />
</xsd:complexType> </xsd:complexType>

View File

@ -0,0 +1,92 @@
<?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\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);
}

View File

@ -15,12 +15,8 @@ use Doctrine\Common\Annotations\AnnotationException;
use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Annotations\CachedReader;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter; 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\Cache\DoctrineProvider;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
/** /**
* Warms up annotation caches for classes found in composer's autoload class map * 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 <galopintitouan@gmail.com> * @author Titouan Galopin <galopintitouan@gmail.com>
*/ */
class AnnotationsCacheWarmer implements CacheWarmerInterface class AnnotationsCacheWarmer extends AbstractPhpFileCacheWarmer
{ {
private $annotationReader; private $annotationReader;
private $phpArrayFile;
private $fallbackPool;
/** /**
* @param Reader $annotationReader * @param Reader $annotationReader
@ -41,70 +35,41 @@ class AnnotationsCacheWarmer implements CacheWarmerInterface
*/ */
public function __construct(Reader $annotationReader, $phpArrayFile, CacheItemPoolInterface $fallbackPool) public function __construct(Reader $annotationReader, $phpArrayFile, CacheItemPoolInterface $fallbackPool)
{ {
parent::__construct($phpArrayFile, $fallbackPool);
$this->annotationReader = $annotationReader; $this->annotationReader = $annotationReader;
$this->phpArrayFile = $phpArrayFile;
if (!$fallbackPool instanceof AdapterInterface) {
$fallbackPool = new ProxyAdapter($fallbackPool);
}
$this->fallbackPool = $fallbackPool;
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function warmUp($cacheDir) protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter)
{ {
$adapter = new PhpArrayAdapter($this->phpArrayFile, $this->fallbackPool);
$annotatedClassPatterns = $cacheDir.'/annotations.map'; $annotatedClassPatterns = $cacheDir.'/annotations.map';
if (!is_file($annotatedClassPatterns)) { if (!is_file($annotatedClassPatterns)) {
$adapter->warmUp(array()); return true;
return;
} }
$annotatedClasses = include $annotatedClassPatterns; $annotatedClasses = include $annotatedClassPatterns;
$reader = new CachedReader($this->annotationReader, new DoctrineProvider($arrayAdapter));
$arrayPool = new ArrayAdapter(0, false); foreach ($annotatedClasses as $class) {
$reader = new CachedReader($this->annotationReader, new DoctrineProvider($arrayPool)); try {
$this->readAllComponents($reader, $class);
spl_autoload_register(array($adapter, 'throwOnRequiredClass')); } catch (\ReflectionException $e) {
try { // ignore failing reflection
foreach ($annotatedClasses as $class) { } catch (AnnotationException $e) {
try { /*
$this->readAllComponents($reader, $class); * Ignore any AnnotationException to not break the cache warming process if an Annotation is badly
} catch (\ReflectionException $e) { * configured or could not be found / read / etc.
// ignore failing reflection *
} catch (AnnotationException $e) { * 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,
* Ignore any AnnotationException to not break the cache warming process if an Annotation is badly * and you always end up with a not found Annotation.
* 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; return true;
} }

View File

@ -13,11 +13,7 @@ namespace Symfony\Bundle\FrameworkBundle\CacheWarmer;
use Doctrine\Common\Annotations\AnnotationException; use Doctrine\Common\Annotations\AnnotationException;
use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter; 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\CacheClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; use Symfony\Component\Serializer\Mapping\Loader\LoaderChain;
@ -30,11 +26,9 @@ use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader;
* *
* @author Titouan Galopin <galopintitouan@gmail.com> * @author Titouan Galopin <galopintitouan@gmail.com>
*/ */
class SerializerCacheWarmer implements CacheWarmerInterface class SerializerCacheWarmer extends AbstractPhpFileCacheWarmer
{ {
private $loaders; private $loaders;
private $phpArrayFile;
private $fallbackPool;
/** /**
* @param LoaderInterface[] $loaders The serializer metadata loaders * @param LoaderInterface[] $loaders The serializer metadata loaders
@ -43,60 +37,33 @@ class SerializerCacheWarmer implements CacheWarmerInterface
*/ */
public function __construct(array $loaders, $phpArrayFile, CacheItemPoolInterface $fallbackPool) public function __construct(array $loaders, $phpArrayFile, CacheItemPoolInterface $fallbackPool)
{ {
parent::__construct($phpArrayFile, $fallbackPool);
$this->loaders = $loaders; $this->loaders = $loaders;
$this->phpArrayFile = $phpArrayFile;
if (!$fallbackPool instanceof AdapterInterface) {
$fallbackPool = new ProxyAdapter($fallbackPool);
}
$this->fallbackPool = $fallbackPool;
} }
/** /**
* {@inheritdoc} * {@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')) { 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); $metadataFactory = new CacheClassMetadataFactory(new ClassMetadataFactory(new LoaderChain($this->loaders)), $arrayAdapter);
$arrayPool = new ArrayAdapter(0, false);
$metadataFactory = new CacheClassMetadataFactory(new ClassMetadataFactory(new LoaderChain($this->loaders)), $arrayPool); foreach ($this->extractSupportedLoaders($this->loaders) as $loader) {
foreach ($loader->getMappedClasses() as $mappedClass) {
spl_autoload_register(array($adapter, 'throwOnRequiredClass')); try {
try { $metadataFactory->getMetadataFor($mappedClass);
foreach ($this->extractSupportedLoaders($this->loaders) as $loader) { } catch (\ReflectionException $e) {
foreach ($loader->getMappedClasses() as $mappedClass) { // ignore failing reflection
try { } catch (AnnotationException $e) {
$metadataFactory->getMetadataFor($mappedClass); // ignore failing annotations
} 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; return true;
} }

View File

@ -13,11 +13,8 @@ namespace Symfony\Bundle\FrameworkBundle\CacheWarmer;
use Doctrine\Common\Annotations\AnnotationException; use Doctrine\Common\Annotations\AnnotationException;
use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\PhpArrayAdapter; 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\Cache\Psr6Cache;
use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory; use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory;
use Symfony\Component\Validator\Mapping\Loader\LoaderChain; use Symfony\Component\Validator\Mapping\Loader\LoaderChain;
@ -31,11 +28,9 @@ use Symfony\Component\Validator\ValidatorBuilderInterface;
* *
* @author Titouan Galopin <galopintitouan@gmail.com> * @author Titouan Galopin <galopintitouan@gmail.com>
*/ */
class ValidatorCacheWarmer implements CacheWarmerInterface class ValidatorCacheWarmer extends AbstractPhpFileCacheWarmer
{ {
private $validatorBuilder; private $validatorBuilder;
private $phpArrayFile;
private $fallbackPool;
/** /**
* @param ValidatorBuilderInterface $validatorBuilder * @param ValidatorBuilderInterface $validatorBuilder
@ -44,64 +39,43 @@ class ValidatorCacheWarmer implements CacheWarmerInterface
*/ */
public function __construct(ValidatorBuilderInterface $validatorBuilder, $phpArrayFile, CacheItemPoolInterface $fallbackPool) public function __construct(ValidatorBuilderInterface $validatorBuilder, $phpArrayFile, CacheItemPoolInterface $fallbackPool)
{ {
parent::__construct($phpArrayFile, $fallbackPool);
$this->validatorBuilder = $validatorBuilder; $this->validatorBuilder = $validatorBuilder;
$this->phpArrayFile = $phpArrayFile;
if (!$fallbackPool instanceof AdapterInterface) {
$fallbackPool = new ProxyAdapter($fallbackPool);
}
$this->fallbackPool = $fallbackPool;
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function warmUp($cacheDir) protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter)
{ {
if (!method_exists($this->validatorBuilder, 'getLoaders')) { 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(); $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')); foreach ($this->extractSupportedLoaders($loaders) as $loader) {
try { foreach ($loader->getMappedClasses() as $mappedClass) {
foreach ($this->extractSupportedLoaders($loaders) as $loader) { try {
foreach ($loader->getMappedClasses() as $mappedClass) { if ($metadataFactory->hasMetadataFor($mappedClass)) {
try { $metadataFactory->getMetadataFor($mappedClass);
if ($metadataFactory->hasMetadataFor($mappedClass)) {
$metadataFactory->getMetadataFor($mappedClass);
}
} catch (\ReflectionException $e) {
// ignore failing reflection
} catch (AnnotationException $e) {
// ignore failing annotations
} }
} catch (\ReflectionException $e) {
// ignore failing reflection
} catch (AnnotationException $e) {
// ignore failing annotations
} }
} }
} finally {
spl_autoload_unregister(array($adapter, 'throwOnRequiredClass'));
} }
$values = $arrayPool->getValues(); return true;
$adapter->warmUp(array_filter($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)
* {@inheritdoc}
*/
public function isOptional()
{ {
return true; // make sure we don't cache null values
parent::warmUpPhpArrayAdapter($phpArrayAdapter, array_filter($values));
} }
/** /**

View File

@ -1,4 +1,4 @@
<div class="exception-summary"> <div class="exception-summary {{ exception.message is empty ? 'exception-without-message' }}">
<div class="exception-metadata"> <div class="exception-metadata">
<div class="container"> <div class="container">
<h2 class="exception-hierarchy"> <h2 class="exception-hierarchy">
@ -13,9 +13,9 @@
</h2> </h2>
</div> </div>
</div> </div>
{% if exception.message is not empty %}
<div class="container"> <div class="exception-message-wrapper">
<div class="exception-message-wrapper"> <div class="container">
<h1 class="break-long-words exception-message {{ exception.message|length > 180 ? 'long' }}"> <h1 class="break-long-words exception-message {{ exception.message|length > 180 ? 'long' }}">
{{- exception.message|nl2br|format_file_from_text -}} {{- exception.message|nl2br|format_file_from_text -}}
</h1> </h1>
@ -25,7 +25,6 @@
</div> </div>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
<div class="container"> <div class="container">

View File

@ -512,14 +512,14 @@
var altContent = toggle.getAttribute('data-toggle-alt-content'); var altContent = toggle.getAttribute('data-toggle-alt-content');
toggle.innerHTML = currentContent !== altContent ? altContent : originalContent; toggle.innerHTML = currentContent !== altContent ? altContent : originalContent;
}); });
}
/* Prevents from disallowing clicks on links inside toggles */ /* Prevents from disallowing clicks on links inside toggles */
var toggleLinks = document.querySelectorAll('.sf-toggle a'); var toggleLinks = document.querySelectorAll('.sf-toggle a');
for (var i = 0; i < toggleLinks.length; i++) { for (var i = 0; i < toggleLinks.length; i++) {
addEventListener(toggleLinks[i], 'click', function(e) { addEventListener(toggleLinks[i], 'click', function(e) {
e.stopPropagation(); e.stopPropagation();
}); });
}
} }
} }
}; };

View File

@ -79,7 +79,8 @@ header .container { display: flex; justify-content: space-between; }
.exception-hierarchy .icon { margin: 0 3px; opacity: .7; } .exception-hierarchy .icon { margin: 0 3px; opacity: .7; }
.exception-hierarchy .icon svg { height: 13px; width: 13px; vertical-align: -2px; } .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 { flex-grow: 1; }
.exception-message, .exception-message a { color: #FFF; font-size: 21px; font-weight: 400; margin: 0; } .exception-message, .exception-message a { color: #FFF; font-size: 21px; font-weight: 400; margin: 0; }
.exception-message.long { font-size: 18px; } .exception-message.long { font-size: 18px; }

View File

@ -16,6 +16,9 @@
margin: 1em 0; margin: 1em 0;
padding: 10px; padding: 10px;
} }
.exception-summary.exception-without-message {
display: none;
}
.exception-message { .exception-message {
color: #B0413E; color: #B0413E;
@ -26,6 +29,6 @@
display: none; display: none;
} }
.exception-message-wrapper { .exception-message-wrapper .container {
min-height: auto; min-height: auto;
} }

View File

@ -380,13 +380,12 @@
100% { background: #222; } 100% { background: #222; }
} }
.sf-toolbar-block.sf-toolbar-block-dump {
position: static;
}
.sf-toolbar-block.sf-toolbar-block-dump .sf-toolbar-info { .sf-toolbar-block.sf-toolbar-block-dump .sf-toolbar-info {
max-width: none; max-width: none;
right: 0; width: 100%;
position: fixed;
box-sizing: border-box;
left: 0;
} }
.sf-toolbar-block-dump pre.sf-dump { .sf-toolbar-block-dump pre.sf-dump {

View File

@ -25,6 +25,7 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ
private $exists; private $exists;
private static $autoloadLevel = 0; private static $autoloadLevel = 0;
private static $autoloadedClass;
private static $existsCache = array(); private static $existsCache = array();
/** /**
@ -57,6 +58,8 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ
/** /**
* {@inheritdoc} * {@inheritdoc}
*
* @throws \ReflectionException when a parent class/interface/trait is not found
*/ */
public function isFresh($timestamp) public function isFresh($timestamp)
{ {
@ -68,12 +71,13 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ
if (!self::$autoloadLevel++) { if (!self::$autoloadLevel++) {
spl_autoload_register(__CLASS__.'::throwOnRequiredClass'); spl_autoload_register(__CLASS__.'::throwOnRequiredClass');
} }
$autoloadedClass = self::$autoloadedClass;
self::$autoloadedClass = $this->resource;
try { try {
$exists = class_exists($this->resource) || interface_exists($this->resource, false) || trait_exists($this->resource, false); $exists = class_exists($this->resource) || interface_exists($this->resource, false) || trait_exists($this->resource, false);
} catch (\ReflectionException $e) {
$exists = false;
} finally { } finally {
self::$autoloadedClass = $autoloadedClass;
if (!--self::$autoloadLevel) { if (!--self::$autoloadLevel) {
spl_autoload_unregister(__CLASS__.'::throwOnRequiredClass'); spl_autoload_unregister(__CLASS__.'::throwOnRequiredClass');
} }
@ -112,7 +116,10 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ
*/ */
private static function throwOnRequiredClass($class) 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(); $trace = $e->getTrace();
$autoloadFrame = array( $autoloadFrame = array(
'function' => 'spl_autoload_call', 'function' => 'spl_autoload_call',
@ -138,6 +145,18 @@ class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializ
case 'is_callable': case 'is_callable':
return; return;
} }
$props = array(
'file' => $trace[$i]['file'],
'line' => $trace[$i]['line'],
'trace' => array_slice($trace, 1 + $i),
);
foreach ($props as $p => $v) {
$r = new \ReflectionProperty('Exception', $p);
$r->setAccessible(true);
$r->setValue($e, $v);
}
} }
throw $e; throw $e;

View File

@ -14,7 +14,8 @@ CHANGELOG
3.4.0 3.4.0
----- -----
* added `CommandLoaderInterface` and PSR-11 `ContainerCommandLoader` * added `CommandLoaderInterface`, `FactoryCommandLoader` and PSR-11
`ContainerCommandLoader` for commands lazy-loading
3.3.0 3.3.0
----- -----

View File

@ -0,0 +1,62 @@
<?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\CommandLoader;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* A simple command loader using factories to instantiate commands lazily.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
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);
}
}

View File

@ -70,20 +70,15 @@ class AddConsoleCommandPass implements CompilerPassInterface
$serviceIds[$commandId] = false; $serviceIds[$commandId] = false;
$commandName = $tags[0]['command']; $commandName = $tags[0]['command'];
unset($tags[0]);
$lazyCommandMap[$commandName] = $id; $lazyCommandMap[$commandName] = $id;
$lazyCommandRefs[$id] = new TypedReference($id, $class); $lazyCommandRefs[$id] = new TypedReference($id, $class);
$aliases = array(); $aliases = array();
foreach ($tags as $tag) { foreach ($tags as $tag) {
if (!isset($tag['command'])) { if (isset($tag['command'])) {
throw new InvalidArgumentException(sprintf('Missing "command" attribute on tag "%s" for service "%s".', $this->commandTag, $id)); $aliases[] = $tag['command'];
} $lazyCommandMap[$tag['command']] = $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;
} }
} }

View File

@ -14,7 +14,7 @@ namespace Symfony\Component\Console\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command; 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\DependencyInjection\AddConsoleCommandPass;
use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Helper\FormatterHelper;
@ -34,7 +34,6 @@ use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcher;
class ApplicationTest extends TestCase class ApplicationTest extends TestCase
@ -128,10 +127,9 @@ class ApplicationTest extends TestCase
$commands = $application->all('foo'); $commands = $application->all('foo');
$this->assertCount(1, $commands, '->all() takes a namespace as its first argument'); $this->assertCount(1, $commands, '->all() takes a namespace as its first argument');
$application->setCommandLoader(new ContainerCommandLoader( $application->setCommandLoader(new FactoryCommandLoader(array(
new ServiceLocator(array('foo-bar' => function () { return new \Foo1Command(); })), 'foo:bar1' => function () { return new \Foo1Command(); },
array('foo:bar1' => 'foo-bar') )));
));
$commands = $application->all('foo'); $commands = $application->all('foo');
$this->assertCount(2, $commands, '->all() takes a namespace as its first argument'); $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(\FooCommand::class, $commands['foo:bar'], '->all() returns the registered commands');
@ -201,9 +199,9 @@ class ApplicationTest extends TestCase
$this->assertEquals($foo, $application->get('foo:bar'), '->get() returns a command by name'); $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'); $this->assertEquals($foo, $application->get('afoobar'), '->get() returns a command by alias');
$application->setCommandLoader(new ContainerCommandLoader(new ServiceLocator(array( $application->setCommandLoader(new FactoryCommandLoader(array(
'foo-bar' => function () { return new \Foo1Command(); }, 'foo:bar1' => 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->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('foo:bar'), '->get() returns an instance by name even with command loader');
@ -320,9 +318,9 @@ class ApplicationTest extends TestCase
public function testFindWithCommandLoader() public function testFindWithCommandLoader()
{ {
$application = new Application(); $application = new Application();
$application->setCommandLoader(new ContainerCommandLoader(new ServiceLocator(array( $application->setCommandLoader(new FactoryCommandLoader(array(
'foo-bar' => $f = function () { return new \FooCommand(); }, '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('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('Symfony\Component\Console\Command\HelpCommand', $application->find('h'), '->find() returns a command if its name exists');
@ -1382,8 +1380,9 @@ class ApplicationTest extends TestCase
$container->addCompilerPass(new AddConsoleCommandPass()); $container->addCompilerPass(new AddConsoleCommandPass());
$container $container
->register('lazy-command', LazyCommand::class) ->register('lazy-command', LazyCommand::class)
->addTag('console.command', array('command' => 'lazy:command', 'alias' => 'lazy:alias')) ->addTag('console.command', array('command' => 'lazy:command'))
->addTag('console.command', array('command' => 'lazy:command', 'alias' => 'lazy:alias2')); ->addTag('console.command', array('command' => 'lazy:alias'))
->addTag('console.command', array('command' => 'lazy:alias2'));
$container->compile(); $container->compile();
$application = new Application(); $application = new Application();

View File

@ -0,0 +1,60 @@
<?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\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());
}
}

View File

@ -56,13 +56,14 @@ class AddConsoleCommandPassTest extends TestCase
$this->assertSame(array($alias => $id), $container->getParameter('console.command.ids')); $this->assertSame(array($alias => $id), $container->getParameter('console.command.ids'));
} }
public function testProcessRegisterLazyCommands() public function testProcessRegistersLazyCommands()
{ {
$container = new ContainerBuilder(); $container = new ContainerBuilder();
$container $command = $container
->register('my-command', MyCommand::class) ->register('my-command', MyCommand::class)
->setPublic(false) ->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); (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->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->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('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() public function visibilityProvider()

View File

@ -97,8 +97,8 @@ class AutowirePass extends AbstractRecursivePass
if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) { if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
return $value; return $value;
} }
if (!$reflectionClass = $this->container->getReflectionClass($value->getClass())) { if (!$reflectionClass = $this->container->getReflectionClass($value->getClass(), false)) {
$this->container->log($this, sprintf('Skipping service "%s": Class or interface "%s" does not exist.', $this->currentId, $value->getClass())); $this->container->log($this, sprintf('Skipping service "%s": Class or interface "%s" cannot be loaded.', $this->currentId, $value->getClass()));
return $value; return $value;
} }
@ -342,7 +342,7 @@ class AutowirePass extends AbstractRecursivePass
return; return;
} }
if ($definition->isDeprecated() || !$reflectionClass = $this->container->getReflectionClass($definition->getClass())) { if ($definition->isDeprecated() || !$reflectionClass = $this->container->getReflectionClass($definition->getClass(), false)) {
return; return;
} }
@ -394,7 +394,7 @@ class AutowirePass extends AbstractRecursivePass
*/ */
private function createAutowiredDefinition($type) private function createAutowiredDefinition($type)
{ {
if (!($typeHint = $this->container->getReflectionClass($type)) || !$typeHint->isInstantiable()) { if (!($typeHint = $this->container->getReflectionClass($type, false)) || !$typeHint->isInstantiable()) {
return; return;
} }
@ -428,8 +428,8 @@ class AutowirePass extends AbstractRecursivePass
private function createTypeNotFoundMessage(TypedReference $reference, $label) private function createTypeNotFoundMessage(TypedReference $reference, $label)
{ {
if (!$r = $this->container->getReflectionClass($type = $reference->getType())) { if (!$r = $this->container->getReflectionClass($type = $reference->getType(), false)) {
$message = sprintf('has type "%s" but this class does not exist.', $type); $message = sprintf('has type "%s" but this class cannot be loaded.', $type);
} else { } else {
$message = $this->container->has($type) ? 'this service is abstract' : 'no such service exists'; $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)); $message = sprintf('references %s "%s" but %s.%s', $r->isInterface() ? 'interface' : 'class', $type, $message, $this->createTypeAlternatives($reference));

View File

@ -26,7 +26,7 @@ trait PriorityTaggedServiceTrait
* *
* The order of additions must be respected for services having the same priority, * 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, * 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=53710
* @see https://bugs.php.net/bug.php?id=60926 * @see https://bugs.php.net/bug.php?id=60926

View File

@ -66,7 +66,7 @@ class ResolveInstanceofConditionalsPass implements CompilerPassInterface
$instanceofTags = array(); $instanceofTags = array();
foreach ($conditionals as $interface => $instanceofDefs) { foreach ($conditionals as $interface => $instanceofDefs) {
if ($interface !== $class && (!$container->getReflectionClass($class))) { if ($interface !== $class && (!$container->getReflectionClass($class, false))) {
continue; continue;
} }

View File

@ -320,12 +320,15 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
* Retrieves the requested reflection class and registers it for resource tracking. * Retrieves the requested reflection class and registers it for resource tracking.
* *
* @param string $class * @param string $class
* @param bool $throw
* *
* @return \ReflectionClass|null * @return \ReflectionClass|null
* *
* @throws \ReflectionException when a parent class/interface/trait is not found and $throw is true
*
* @final * @final
*/ */
public function getReflectionClass($class) public function getReflectionClass($class, $throw = true)
{ {
if (!$class = $this->getParameterBag()->resolveValue($class)) { if (!$class = $this->getParameterBag()->resolveValue($class)) {
return; return;
@ -340,6 +343,9 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
$classReflector = $resource->isFresh(0) ? false : new \ReflectionClass($class); $classReflector = $resource->isFresh(0) ? false : new \ReflectionClass($class);
} }
} catch (\ReflectionException $e) { } catch (\ReflectionException $e) {
if ($throw) {
throw $e;
}
$classReflector = false; $classReflector = false;
} }

View File

@ -330,7 +330,7 @@ class AutowirePassTest extends TestCase
/** /**
* @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException * @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() public function testClassNotFoundThrowsException()
{ {
@ -345,7 +345,7 @@ class AutowirePassTest extends TestCase
/** /**
* @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException * @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() public function testParentClassNotFoundThrowsException()
{ {
@ -685,7 +685,7 @@ class AutowirePassTest extends TestCase
public function provideNotWireableCalls() public function provideNotWireableCalls()
{ {
return array( 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('setDifferentNamespace', 'Cannot autowire service "foo": argument "$n" of method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setDifferentNamespace()" references class "stdClass" but no such service exists. It cannot be auto-registered because it is from a different root namespace.'),
array(null, 'Cannot autowire service "foo": method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setProtectedMethod()" must be public.'), array(null, 'Cannot autowire service "foo": method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setProtectedMethod()" must be public.'),
); );

View File

@ -126,6 +126,10 @@ class Cookie
$this->httpOnly = (bool) $httpOnly; $this->httpOnly = (bool) $httpOnly;
$this->raw = (bool) $raw; $this->raw = (bool) $raw;
if (null !== $sameSite) {
$sameSite = strtolower($sameSite);
}
if (!in_array($sameSite, array(self::SAMESITE_LAX, self::SAMESITE_STRICT, null), true)) { if (!in_array($sameSite, array(self::SAMESITE_LAX, self::SAMESITE_STRICT, null), true)) {
throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.'); throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.');
} }

View File

@ -211,4 +211,10 @@ class CookieTest extends TestCase
$cookie = Cookie::fromString('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; path=/; domain=.myfoodomain.com; secure'); $cookie = Cookie::fromString('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; path=/; domain=.myfoodomain.com; secure');
$this->assertFalse($cookie->isHttpOnly()); $this->assertFalse($cookie->isHttpOnly());
} }
public function testSameSiteAttributeIsCaseInsensitive()
{
$cookie = new Cookie('foo', 'bar', 0, '/', null, false, true, false, 'Lax');
$this->assertEquals('lax', $cookie->getSameSite());
}
} }

View File

@ -108,7 +108,8 @@ class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface
return call_user_func_array(array($this->propertyInfoExtractor, $method), $arguments); 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)) { if (array_key_exists($key, $this->arrayCache)) {
return $this->arrayCache[$key]; return $this->arrayCache[$key];
@ -126,29 +127,4 @@ class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface
return $this->arrayCache[$key] = $value; 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',
));
}
} }

View File

@ -61,29 +61,4 @@ class PropertyInfoCacheExtractorTest extends AbstractPropertyInfoExtractorTest
parent::testGetProperties(); parent::testGetProperties();
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'),
);
}
} }

View File

@ -122,8 +122,14 @@ class DefaultAuthenticationSuccessHandler implements AuthenticationSuccessHandle
return $targetUrl; return $targetUrl;
} }
if ($this->options['use_referer'] && ($targetUrl = $request->headers->get('Referer')) && parse_url($targetUrl, PHP_URL_PATH) !== $this->httpUtils->generateUri($request, $this->options['login_path'])) { if ($this->options['use_referer']) {
return $targetUrl; $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']; return $this->options['default_target_path'];

View File

@ -108,7 +108,7 @@ class HttpUtils
$parameters = $this->urlMatcher->match($request->getPathInfo()); $parameters = $this->urlMatcher->match($request->getPathInfo());
} }
return $path === $parameters['_route']; return isset($parameters['_route']) && $path === $parameters['_route'];
} catch (MethodNotAllowedException $e) { } catch (MethodNotAllowedException $e) {
return false; return false;
} catch (ResourceNotFoundException $e) { } catch (ResourceNotFoundException $e) {

View File

@ -12,193 +12,92 @@
namespace Symfony\Component\Security\Http\Tests\Authentication; namespace Symfony\Component\Security\Http\Tests\Authentication;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler; use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler;
use Symfony\Component\Security\Http\HttpUtils;
class DefaultAuthenticationSuccessHandlerTest extends TestCase class DefaultAuthenticationSuccessHandlerTest extends TestCase
{ {
private $httpUtils = null; /**
* @dataProvider getRequestRedirections
private $request = null; */
public function testRequestRedirections(Request $request, $options, $redirectedUrl)
private $token = null;
protected function setUp()
{ {
$this->httpUtils = $this->getMockBuilder('Symfony\Component\Security\Http\HttpUtils')->getMock(); $urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock();
$this->request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->getMock(); $urlGenerator->expects($this->any())->method('generate')->will($this->returnValue('http://localhost/login'));
$this->request->headers = $this->getMockBuilder('Symfony\Component\HttpFoundation\HeaderBag')->getMock(); $httpUtils = new HttpUtils($urlGenerator);
$this->token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); $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() public function getRequestRedirections()
{
$response = $this->expectRedirectResponse('/');
$handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, array());
$result = $handler->onAuthenticationSuccess($this->request, $this->token);
$this->assertSame($response, $result);
}
public function testDefaultTargetPathCanBeForced()
{
$options = array(
'always_use_default_target_path' => true,
'default_target_path' => '/dashboard',
);
$response = $this->expectRedirectResponse('/dashboard');
$handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, $options);
$result = $handler->onAuthenticationSuccess($this->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');
$handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, array());
$result = $handler->onAuthenticationSuccess($this->request, $this->token);
$this->assertSame($response, $result);
}
public function testTargetPathIsPassedAsNestedParameterWithRequest()
{
$this->request->expects($this->once())
->method('get')->with('_target_path')
->will($this->returnValue(array('value' => '/dashboard')));
$response = $this->expectRedirectResponse('/dashboard');
$handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, array('target_path_parameter' => '_target_path[value]'));
$result = $handler->onAuthenticationSuccess($this->request, $this->token);
$this->assertSame($response, $result);
}
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');
$handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, $options);
$result = $handler->onAuthenticationSuccess($this->request, $this->token);
$this->assertSame($response, $result);
}
public function testTargetPathIsTakenFromTheSession()
{ {
$session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface')->getMock(); $session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface')->getMock();
$session->expects($this->once()) $session->expects($this->once())->method('get')->with('_security.admin.target_path')->will($this->returnValue('/admin/dashboard'));
->method('get')->with('_security.admin.target_path') $session->expects($this->once())->method('remove')->with('_security.admin.target_path');
->will($this->returnValue('/admin/dashboard')); $requestWithSession = Request::create('/');
$session->expects($this->once()) $requestWithSession->setSession($session);
->method('remove')->with('_security.admin.target_path');
$this->request->expects($this->any()) return array(
->method('getSession') 'default' => array(
->will($this->returnValue($session)); Request::create('/'),
array(),
$response = $this->expectRedirectResponse('/admin/dashboard'); '/',
),
$handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, array()); 'forced target path' => array(
$handler->setProviderKey('admin'); Request::create('/'),
array('always_use_default_target_path' => true, 'default_target_path' => '/dashboard'),
$result = $handler->onAuthenticationSuccess($this->request, $this->token); '/dashboard',
),
$this->assertSame($response, $result); 'target path as query string' => array(
} Request::create('/?_target_path=/dashboard'),
array(),
public function testTargetPathIsPassedAsReferer() '/dashboard',
{ ),
$options = array('use_referer' => true); 'target path name as query string is customized' => array(
Request::create('/?_my_target_path=/dashboard'),
$this->request->headers->expects($this->once()) array('target_path_parameter' => '_my_target_path'),
->method('get')->with('Referer') '/dashboard',
->will($this->returnValue('/dashboard')); ),
'target path name as query string is customized and nested' => array(
$response = $this->expectRedirectResponse('/dashboard'); Request::create('/?_target_path[value]=/dashboard'),
array('target_path_parameter' => '_target_path[value]'),
$handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, $options); '/dashboard',
$result = $handler->onAuthenticationSuccess($this->request, $this->token); ),
'target path in session' => array(
$this->assertSame($response, $result); $requestWithSession,
} array(),
'/admin/dashboard',
public function testRefererHasToBeDifferentThanLoginUrl() ),
{ 'target path as referer' => array(
$options = array('use_referer' => true); Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/dashboard')),
array('use_referer' => true),
$this->request->headers->expects($this->any()) '/dashboard',
->method('get')->with('Referer') ),
->will($this->returnValue('/login')); 'target path as referer is ignored if not configured' => array(
Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/dashboard')),
$this->httpUtils->expects($this->once()) array(),
->method('generateUri')->with($this->request, '/login') '/',
->will($this->returnValue('/login')); ),
'target path should be different than login URL' => array(
$response = $this->expectRedirectResponse('/'); Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/login')),
array('use_referer' => true, 'login_path' => '/login'),
$handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, $options); '/',
$result = $handler->onAuthenticationSuccess($this->request, $this->token); ),
'target path should be different than login URL (query string does not matter)' => array(
$this->assertSame($response, $result); Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/login?t=1&p=2')),
} array('use_referer' => true, 'login_path' => '/login'),
'/',
public function testRefererWithoutParametersHasToBeDifferentThanLoginUrl() ),
{ 'target path should be different than login URL (login_path as a route)' => array(
$options = array('use_referer' => true); Request::create('/', 'GET', array(), array(), array(), array('HTTP_REFERER' => 'http://localhost/login?t=1&p=2')),
array('use_referer' => true, 'login_path' => 'login_route'),
$this->request->headers->expects($this->any()) '/',
->method('get')->with('Referer') ),
->will($this->returnValue('/subfolder/login?t=1&p=2')); );
$this->httpUtils->expects($this->once())
->method('generateUri')->with($this->request, '/login')
->will($this->returnValue('/subfolder/login'));
$response = $this->expectRedirectResponse('/');
$handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, $options);
$result = $handler->onAuthenticationSuccess($this->request, $this->token);
$this->assertSame($response, $result);
}
public function testRefererTargetPathIsIgnoredByDefault()
{
$this->request->headers->expects($this->never())->method('get');
$response = $this->expectRedirectResponse('/');
$handler = new DefaultAuthenticationSuccessHandler($this->httpUtils, array());
$result = $handler->onAuthenticationSuccess($this->request, $this->token);
$this->assertSame($response, $result);
}
private function expectRedirectResponse($path)
{
$response = new Response();
$this->httpUtils->expects($this->once())
->method('createRedirectResponse')
->with($this->request, $path)
->will($this->returnValue($response));
return $response;
} }
} }

View File

@ -221,6 +221,19 @@ class HttpUtilsTest extends TestCase
$utils->checkRequestPath($this->getRequest(), 'foobar'); $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 * @expectedException \InvalidArgumentException
* @expectedExceptionMessage Matcher must either implement UrlMatcherInterface or RequestMatcherInterface * @expectedExceptionMessage Matcher must either implement UrlMatcherInterface or RequestMatcherInterface

View File

@ -27,7 +27,7 @@ class DateCaster
$fromNow = (new \DateTime())->diff($d); $fromNow = (new \DateTime())->diff($d);
$title = $d->format('l, F j, Y') $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") : '') .($location ? ($d->format('I') ? "\nDST On" : "\nDST Off") : '')
; ;
@ -38,4 +38,34 @@ class DateCaster
return $a; 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));
}
} }

View File

@ -110,6 +110,7 @@ abstract class AbstractCloner implements ClonerInterface
'RedisArray' => array('Symfony\Component\VarDumper\Caster\RedisCaster', 'castRedisArray'), 'RedisArray' => array('Symfony\Component\VarDumper\Caster\RedisCaster', 'castRedisArray'),
'DateTimeInterface' => array('Symfony\Component\VarDumper\Caster\DateCaster', 'castDateTime'), 'DateTimeInterface' => array('Symfony\Component\VarDumper\Caster\DateCaster', 'castDateTime'),
'DateInterval' => array('Symfony\Component\VarDumper\Caster\DateCaster', 'castInterval'),
':curl' => array('Symfony\Component\VarDumper\Caster\ResourceCaster', 'castCurl'), ':curl' => array('Symfony\Component\VarDumper\Caster\ResourceCaster', 'castCurl'),
':dba' => array('Symfony\Component\VarDumper\Caster\ResourceCaster', 'castDba'), ':dba' => array('Symfony\Component\VarDumper\Caster\ResourceCaster', 'castDba'),

View File

@ -46,8 +46,8 @@ abstract class AbstractDumper implements DataDumperInterface, DumperInterface
{ {
$this->flags = (int) $flags; $this->flags = (int) $flags;
$this->setCharset($charset ?: ini_get('php.output_encoding') ?: ini_get('default_charset') ?: 'UTF-8'); $this->setCharset($charset ?: ini_get('php.output_encoding') ?: ini_get('default_charset') ?: 'UTF-8');
$this->decimalPoint = (string) 0.5; $this->decimalPoint = localeconv();
$this->decimalPoint = $this->decimalPoint[1]; $this->decimalPoint = $this->decimalPoint['decimal_point'];
$this->setOutput($output ?: static::$defaultOutput); $this->setOutput($output ?: static::$defaultOutput);
if (!$output && is_string(static::$defaultOutput)) { if (!$output && is_string(static::$defaultOutput)) {
static::$defaultOutput = $this->outputStream; static::$defaultOutput = $this->outputStream;
@ -123,6 +123,13 @@ abstract class AbstractDumper implements DataDumperInterface, DumperInterface
*/ */
public function dump(Data $data, $output = null) public function dump(Data $data, $output = null)
{ {
$this->decimalPoint = localeconv();
$this->decimalPoint = $this->decimalPoint['decimal_point'];
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) { if ($returnDump = true === $output) {
$output = fopen('php://memory', 'r+b'); $output = fopen('php://memory', 'r+b');
} }
@ -143,6 +150,9 @@ abstract class AbstractDumper implements DataDumperInterface, DumperInterface
if ($output) { if ($output) {
$this->setOutput($prevOutput); $this->setOutput($prevOutput);
} }
if ($locale) {
setlocale(LC_NUMERIC, $locale);
}
} }
} }

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\VarDumper\Tests\Caster; namespace Symfony\Component\VarDumper\Tests\Caster;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\VarDumper\Caster\Caster;
use Symfony\Component\VarDumper\Caster\DateCaster; use Symfony\Component\VarDumper\Caster\DateCaster;
use Symfony\Component\VarDumper\Cloner\Stub; use Symfony\Component\VarDumper\Cloner\Stub;
use Symfony\Component\VarDumper\Test\VarDumperTestTrait; use Symfony\Component\VarDumper\Test\VarDumperTestTrait;
@ -80,4 +81,103 @@ EODUMP;
array('2017-04-30 00:00:00.000000', '+02:00', '2017-04-30 00:00:00.000000 +02:00'), 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 = <<<EODUMP
DateInterval {
interval: $expected
%A}
EODUMP;
$this->assertDumpMatchesFormat($xDump, $interval);
}
/**
* @dataProvider provideIntervals
*/
public function testDumpIntervalExcludingVerbosity($intervalSpec, $invert, $expected)
{
$interval = new \DateInterval($intervalSpec);
$interval->invert = $invert;
$xDump = <<<EODUMP
DateInterval {
interval: $expected
}
EODUMP;
$this->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 = <<<EODUMP
array:1 [
"\\x00~\\x00interval" => $xInterval
]
EODUMP;
$this->assertDumpMatchesFormat($xDump, $cast);
if (null === $xSeconds) {
return;
}
$xDump = <<<EODUMP
Symfony\Component\VarDumper\Caster\ConstStub {
+type: "ref"
+class: "$xInterval"
+value: "$xSeconds"
+cut: 0
+handle: 0
+refCount: 0
+position: 0
+attr: []
}
EODUMP;
$this->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),
);
}
} }