Merge branch '3.4' into 4.0

* 3.4:
  fixed deprecations in tests
  fixed Twig URL
  [Cache] Add missing `@internal` tag on ProxyTrait
  fix formatting arguments in plaintext format
  Fix PSR exception context key
  Don't assume that file binary exists on *nix OS
  Fix that ESI/SSI processing can turn a \"private\" response \"public\"
  [Form] Fixed trimming choice values
  fix rendering exception stack traces
  [Routing] Fix loading multiple class annotations for invokable classes
This commit is contained in:
Fabien Potencier 2018-04-20 08:20:23 +02:00
commit bb45c75d7b
16 changed files with 300 additions and 26 deletions

View File

@ -1,7 +1,7 @@
Twig Bridge
===========
Provides integration for [Twig](http://twig.sensiolabs.org/) with various
Provides integration for [Twig](https://twig.symfony.com/) with various
Symfony components.
Resources

View File

@ -1,5 +1,5 @@
{% if trace.function %}
at {{ trace.class ~ trace.type ~ trace.function }}({{ trace.args|format_args }})
at {{ trace.class ~ trace.type ~ trace.function }}({{ trace.args|format_args_as_text }})
{%- endif -%}
{% if trace.file|default('') is not empty and trace.line|default('') is not empty %}
{{- trace.function ? '\n (' : 'at '}}{{ trace.file|format_file(trace.line)|striptags|replace({ (' at line ' ~ trace.line): '' }) }}:{{ trace.line }}{{ trace.function ? ')' }}

View File

@ -1,5 +1,4 @@
{% if exception.trace|length %}
<pre class="stacktrace">
{{ exception.class }}:
{% if exception.message is not empty %}
{{- exception.message }}
@ -8,5 +7,4 @@
{% for trace in exception.trace %}
{{ include('@Twig/Exception/trace.txt.twig', { trace: trace }, with_context = false) }}
{% endfor %}
</pre>
{% endif %}

View File

@ -17,7 +17,13 @@
<tbody id="trace-text-{{ index }}">
<tr>
<td>
{{ include('@Twig/Exception/traces.txt.twig', { exception: exception }, with_context = false) }}
{% if exception.trace|length %}
<pre class="stacktrace">
{%- filter escape('html') -%}
{{- include('@Twig/Exception/traces.txt.twig', { exception: exception, format: 'html' }, with_context = false) }}
{% endfilter %}
</pre>
{% endif %}
</td>
</tr>
</tbody>

View File

@ -16,6 +16,8 @@ use Symfony\Component\Cache\ResettableInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
trait ProxyTrait
{

View File

@ -40,10 +40,10 @@ class ErrorListener implements EventSubscriberInterface
$error = $event->getError();
if (!$inputString = $this->getInputString($event)) {
return $this->logger->error('An error occurred while using the console. Message: "{message}"', array('error' => $error, 'message' => $error->getMessage()));
return $this->logger->error('An error occurred while using the console. Message: "{message}"', array('exception' => $error, 'message' => $error->getMessage()));
}
$this->logger->error('Error thrown while running command "{command}". Message: "{message}"', array('error' => $error, 'command' => $inputString, 'message' => $error->getMessage()));
$this->logger->error('Error thrown while running command "{command}". Message: "{message}"', array('exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()));
}
public function onConsoleTerminate(ConsoleTerminateEvent $event)

View File

@ -34,7 +34,7 @@ class ErrorListenerTest extends TestCase
$logger
->expects($this->once())
->method('error')
->with('Error thrown while running command "{command}". Message: "{message}"', array('error' => $error, 'command' => 'test:run --foo=baz buzz', 'message' => 'An error occurred'))
->with('Error thrown while running command "{command}". Message: "{message}"', array('exception' => $error, 'command' => 'test:run --foo=baz buzz', 'message' => 'An error occurred'))
;
$listener = new ErrorListener($logger);
@ -49,7 +49,7 @@ class ErrorListenerTest extends TestCase
$logger
->expects($this->once())
->method('error')
->with('An error occurred while using the console. Message: "{message}"', array('error' => $error, 'message' => 'An error occurred'))
->with('An error occurred while using the console. Message: "{message}"', array('exception' => $error, 'message' => 'An error occurred'))
;
$listener = new ErrorListener($logger);

View File

@ -319,6 +319,7 @@ class ChoiceType extends AbstractType
// See https://github.com/symfony/symfony/pull/5582
'data_class' => null,
'choice_translation_domain' => true,
'trim' => false,
));
$resolver->setNormalizer('placeholder', $placeholderNormalizer);

View File

@ -1880,4 +1880,176 @@ class ChoiceTypeTest extends BaseTypeTest
'multiple, expanded' => array(true, true, array(array())),
);
}
public function testInheritTranslationDomainFromParent()
{
$view = $this->factory
->createNamedBuilder('parent', FormTypeTest::TESTED_TYPE, null, array(
'translation_domain' => 'domain',
))
->add('child', static::TESTED_TYPE)
->getForm()
->createView();
$this->assertEquals('domain', $view['child']->vars['translation_domain']);
}
public function testPassTranslationDomainToView()
{
$view = $this->factory->create(static::TESTED_TYPE, null, array(
'translation_domain' => 'domain',
))
->createView();
$this->assertSame('domain', $view->vars['translation_domain']);
}
public function testPreferOwnTranslationDomain()
{
$view = $this->factory
->createNamedBuilder('parent', FormTypeTest::TESTED_TYPE, null, array(
'translation_domain' => 'parent_domain',
))
->add('child', static::TESTED_TYPE, array(
'translation_domain' => 'domain',
))
->getForm()
->createView();
$this->assertEquals('domain', $view['child']->vars['translation_domain']);
}
public function testDefaultTranslationDomain()
{
$view = $this->factory->createNamedBuilder('parent', FormTypeTest::TESTED_TYPE)
->add('child', static::TESTED_TYPE)
->getForm()
->createView();
$this->assertNull($view['child']->vars['translation_domain']);
}
public function testPassMultipartFalseToView()
{
$view = $this->factory->create(static::TESTED_TYPE, null)
->createView();
$this->assertFalse($view->vars['multipart']);
}
public function testPassLabelToView()
{
$view = $this->factory->createNamed('__test___field', static::TESTED_TYPE, null, array(
'label' => 'My label',
))
->createView();
$this->assertSame('My label', $view->vars['label']);
}
public function testPassIdAndNameToViewWithGrandParent()
{
$builder = $this->factory->createNamedBuilder('parent', FormTypeTest::TESTED_TYPE)
->add('child', FormTypeTest::TESTED_TYPE);
$builder->get('child')->add('grand_child', static::TESTED_TYPE);
$view = $builder->getForm()->createView();
$this->assertEquals('parent_child_grand_child', $view['child']['grand_child']->vars['id']);
$this->assertEquals('grand_child', $view['child']['grand_child']->vars['name']);
$this->assertEquals('parent[child][grand_child]', $view['child']['grand_child']->vars['full_name']);
}
public function testPassIdAndNameToViewWithParent()
{
$view = $this->factory->createNamedBuilder('parent', FormTypeTest::TESTED_TYPE)
->add('child', static::TESTED_TYPE)
->getForm()
->createView();
$this->assertEquals('parent_child', $view['child']->vars['id']);
$this->assertEquals('child', $view['child']->vars['name']);
$this->assertEquals('parent[child]', $view['child']->vars['full_name']);
}
public function testPassDisabledAsOption()
{
$form = $this->factory->create(static::TESTED_TYPE, null, array(
'disabled' => true,
));
$this->assertTrue($form->isDisabled());
}
public function testPassIdAndNameToView()
{
$view = $this->factory->createNamed('name', static::TESTED_TYPE, null)
->createView();
$this->assertEquals('name', $view->vars['id']);
$this->assertEquals('name', $view->vars['name']);
$this->assertEquals('name', $view->vars['full_name']);
}
public function testStripLeadingUnderscoresAndDigitsFromId()
{
$view = $this->factory->createNamed('_09name', static::TESTED_TYPE, null)
->createView();
$this->assertEquals('name', $view->vars['id']);
$this->assertEquals('_09name', $view->vars['name']);
$this->assertEquals('_09name', $view->vars['full_name']);
}
/**
* @dataProvider provideTrimCases
*/
public function testTrimIsDisabled($multiple, $expanded)
{
$form = $this->factory->create(static::TESTED_TYPE, null, array(
'multiple' => $multiple,
'expanded' => $expanded,
'choices' => array(
'a' => '1',
),
));
$submittedData = ' 1';
$form->submit($multiple ? (array) $submittedData : $submittedData);
// When the choice does not exist the transformation fails
$this->assertFalse($form->isSynchronized());
$this->assertNull($form->getData());
}
/**
* @dataProvider provideTrimCases
*/
public function testSubmitValueWithWhiteSpace($multiple, $expanded)
{
$valueWhitWhiteSpace = '1 ';
$form = $this->factory->create(static::TESTED_TYPE, null, array(
'multiple' => $multiple,
'expanded' => $expanded,
'choices' => array(
'a' => $valueWhitWhiteSpace,
),
));
$form->submit($multiple ? (array) $valueWhitWhiteSpace : $valueWhitWhiteSpace);
$this->assertTrue($form->isSynchronized());
$this->assertSame($multiple ? (array) $valueWhitWhiteSpace : $valueWhitWhiteSpace, $form->getData());
}
public function provideTrimCases()
{
return array(
'Simple' => array(false, false),
'Multiple' => array(true, false),
'Simple expanded' => array(false, true),
'Multiple expanded' => array(true, true),
);
}
}

View File

@ -43,7 +43,21 @@ class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface
*/
public static function isSupported()
{
return '\\' !== DIRECTORY_SEPARATOR && function_exists('passthru') && function_exists('escapeshellarg');
static $supported = null;
if (null !== $supported) {
return $supported;
}
if ('\\' === DIRECTORY_SEPARATOR || !function_exists('passthru') || !function_exists('escapeshellarg')) {
return $supported = false;
}
ob_start();
passthru('command -v file', $exitStatus);
$binPath = trim(ob_get_clean());
return $supported = 0 === $exitStatus && '' !== $binPath;
}
/**

View File

@ -502,13 +502,19 @@ class Response
}
/**
* Returns true if the response is worth caching under any circumstance.
* Returns true if the response may safely be kept in a shared (surrogate) cache.
*
* Responses marked "private" with an explicit Cache-Control directive are
* considered uncacheable.
*
* Responses with neither a freshness lifetime (Expires, max-age) nor cache
* validator (Last-Modified, ETag) are considered uncacheable.
* validator (Last-Modified, ETag) are considered uncacheable because there is
* no way to tell when or how to remove them from the cache.
*
* Note that RFC 7231 and RFC 7234 possibly allow for a more permissive implementation,
* for example "status codes that are defined as cacheable by default [...]
* can be reused by a cache with heuristic expiration unless otherwise indicated"
* (https://tools.ietf.org/html/rfc7231#section-6.1)
*
* @final
*/

View File

@ -72,7 +72,7 @@ class ResponseCacheStrategy implements ResponseCacheStrategyInterface
$response->setLastModified(null);
}
if (!$response->isFresh()) {
if (!$response->isFresh() || !$response->isCacheable()) {
$this->cacheable = false;
}

View File

@ -175,8 +175,26 @@ class ResponseCacheStrategyTest extends TestCase
$cacheStrategy->update($masterResponse);
$this->assertTrue($masterResponse->headers->hasCacheControlDirective('private'));
// Not sure if we should pass "max-age: 60" in this case, as long as the response is private and
// that's the more conservative of both the master and embedded response...?
$this->assertFalse($masterResponse->headers->hasCacheControlDirective('public'));
}
public function testEmbeddingPublicResponseDoesNotMakeMainResponsePublic()
{
$cacheStrategy = new ResponseCacheStrategy();
$masterResponse = new Response();
$masterResponse->setPrivate(); // this is the default, but let's be explicit
$masterResponse->setMaxAge(100);
$embeddedResponse = new Response();
$embeddedResponse->setPublic();
$embeddedResponse->setSharedMaxAge(100);
$cacheStrategy->add($embeddedResponse);
$cacheStrategy->update($masterResponse);
$this->assertTrue($masterResponse->headers->hasCacheControlDirective('private'));
$this->assertFalse($masterResponse->headers->hasCacheControlDirective('public'));
}
public function testResponseIsExiprableWhenEmbeddedResponseCombinesExpiryAndValidation()

View File

@ -119,10 +119,15 @@ abstract class AnnotationClassLoader implements LoaderInterface
}
}
if (0 === $collection->count() && $class->hasMethod('__invoke') && $annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) {
$globals['path'] = '';
$globals['name'] = '';
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
if (0 === $collection->count() && $class->hasMethod('__invoke')) {
foreach ($this->reader->getClassAnnotations($class) as $annot) {
if ($annot instanceof $this->routeAnnotationClass) {
$globals['path'] = '';
$globals['name'] = '';
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
}
}
}
return $collection;

View File

@ -191,9 +191,9 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
);
$this->reader
->expects($this->exactly(2))
->method('getClassAnnotation')
->will($this->returnValue($this->getAnnotatedRoute($classRouteData)))
->expects($this->exactly(1))
->method('getClassAnnotations')
->will($this->returnValue(array($this->getAnnotatedRoute($classRouteData))))
;
$this->reader
->expects($this->once())
@ -205,8 +205,49 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
$route = $routeCollection->get($classRouteData['name']);
$this->assertSame($classRouteData['path'], $route->getPath(), '->load preserves class route path');
$this->assertEquals(array_merge($classRouteData['schemes'], $classRouteData['schemes']), $route->getSchemes(), '->load preserves class route schemes');
$this->assertEquals(array_merge($classRouteData['methods'], $classRouteData['methods']), $route->getMethods(), '->load preserves class route methods');
$this->assertEquals($classRouteData['schemes'], $route->getSchemes(), '->load preserves class route schemes');
$this->assertEquals($classRouteData['methods'], $route->getMethods(), '->load preserves class route methods');
}
public function testInvokableClassMultipleRouteLoad()
{
$classRouteData1 = array(
'name' => 'route1',
'path' => '/1',
'schemes' => array('https'),
'methods' => array('GET'),
);
$classRouteData2 = array(
'name' => 'route2',
'path' => '/2',
'schemes' => array('https'),
'methods' => array('GET'),
);
$this->reader
->expects($this->exactly(1))
->method('getClassAnnotations')
->will($this->returnValue(array($this->getAnnotatedRoute($classRouteData1), $this->getAnnotatedRoute($classRouteData2))))
;
$this->reader
->expects($this->once())
->method('getMethodAnnotations')
->will($this->returnValue(array()))
;
$routeCollection = $this->loader->load('Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BazClass');
$route = $routeCollection->get($classRouteData1['name']);
$this->assertSame($classRouteData1['path'], $route->getPath(), '->load preserves class route path');
$this->assertEquals($classRouteData1['schemes'], $route->getSchemes(), '->load preserves class route schemes');
$this->assertEquals($classRouteData1['methods'], $route->getMethods(), '->load preserves class route methods');
$route = $routeCollection->get($classRouteData2['name']);
$this->assertSame($classRouteData2['path'], $route->getPath(), '->load preserves class route path');
$this->assertEquals($classRouteData2['schemes'], $route->getSchemes(), '->load preserves class route schemes');
$this->assertEquals($classRouteData2['methods'], $route->getMethods(), '->load preserves class route methods');
}
public function testInvokableClassWithMethodRouteLoad()

View File

@ -29,7 +29,7 @@ class AnnotationDirectoryLoaderTest extends AbstractAnnotationLoaderTest
public function testLoad()
{
$this->reader->expects($this->exactly(4))->method('getClassAnnotation');
$this->reader->expects($this->exactly(3))->method('getClassAnnotation');
$this->reader
->expects($this->any())
@ -37,6 +37,12 @@ class AnnotationDirectoryLoaderTest extends AbstractAnnotationLoaderTest
->will($this->returnValue(array()))
;
$this->reader
->expects($this->any())
->method('getClassAnnotations')
->will($this->returnValue(array()))
;
$this->loader->load(__DIR__.'/../Fixtures/AnnotatedClasses');
}
@ -45,7 +51,6 @@ class AnnotationDirectoryLoaderTest extends AbstractAnnotationLoaderTest
$this->expectAnnotationsToBeReadFrom(array(
'Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BarClass',
'Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BazClass',
'Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BazClass',
'Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\FooClass',
));
@ -55,6 +60,12 @@ class AnnotationDirectoryLoaderTest extends AbstractAnnotationLoaderTest
->will($this->returnValue(array()))
;
$this->reader
->expects($this->any())
->method('getClassAnnotations')
->will($this->returnValue(array()))
;
$this->loader->load(__DIR__.'/../Fixtures/AnnotatedClasses');
}