diff --git a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php index aa1d95ecc6..2a41422e00 100644 --- a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php +++ b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php @@ -95,9 +95,19 @@ class DoctrineExtractor implements PropertyListExtractorInterface, PropertyTypeE if (isset($associationMapping['indexBy'])) { $indexProperty = $associationMapping['indexBy']; + /** @var ClassMetadataInfo $subMetadata */ $subMetadata = $this->classMetadataFactory->getMetadataFor($associationMapping['targetEntity']); $typeOfField = $subMetadata->getTypeOfField($indexProperty); + if (null === $typeOfField) { + $associationMapping = $subMetadata->getAssociationMapping($indexProperty); + + /** @var ClassMetadataInfo $subMetadata */ + $indexProperty = $subMetadata->getSingleAssociationReferencedJoinColumnName($indexProperty); + $subMetadata = $this->classMetadataFactory->getMetadataFor($associationMapping['targetEntity']); + $typeOfField = $subMetadata->getTypeOfField($indexProperty); + } + $collectionKeyType = $this->getPhpType($typeOfField); } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php index fd6c23279c..f212ab25e6 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php @@ -61,6 +61,7 @@ class DoctrineExtractorTest extends TestCase 'foo', 'bar', 'indexedBar', + 'indexedFoo', ), $this->extractor->getProperties('Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy') ); @@ -141,6 +142,14 @@ class DoctrineExtractorTest extends TestCase new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') ))), + array('indexedFoo', array(new Type( + Type::BUILTIN_TYPE_OBJECT, + false, + 'Doctrine\Common\Collections\Collection', + true, + new Type(Type::BUILTIN_TYPE_STRING), + new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') + ))), array('simpleArray', array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)))), array('customFoo', null), array('notMapped', null), diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php index 793be8f9aa..9517f6cc40 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php @@ -14,6 +14,7 @@ namespace Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\OneToMany; use Doctrine\ORM\Mapping\ManyToMany; use Doctrine\ORM\Mapping\ManyToOne; @@ -45,6 +46,11 @@ class DoctrineDummy */ protected $indexedBar; + /** + * @OneToMany(targetEntity="DoctrineRelation", mappedBy="foo", indexBy="foo") + */ + protected $indexedFoo; + /** * @Column(type="guid") */ diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineRelation.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineRelation.php index 6e94e028fa..85660d3d6b 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineRelation.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineRelation.php @@ -14,6 +14,7 @@ namespace Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\ManyToOne; /** * @Entity @@ -32,4 +33,10 @@ class DoctrineRelation * @Column(type="guid") */ protected $rguid; + + /** + * @Column(type="guid") + * @ManyToOne(targetEntity="DoctrineDummy", inversedBy="indexedFoo") + */ + protected $foo; } diff --git a/src/Symfony/Bridge/Twig/README.md b/src/Symfony/Bridge/Twig/README.md index eb084147c3..602f5a54c3 100644 --- a/src/Symfony/Bridge/Twig/README.md +++ b/src/Symfony/Bridge/Twig/README.md @@ -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 diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig index cf479ed750..dc4858e2f3 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig @@ -106,8 +106,7 @@ {%- endblock dateinterval_widget %} {% block percent_widget -%} -
{{ 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 %} -{% endif %} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces_text.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces_text.html.twig index ffe7f31ad5..318a5bbeeb 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces_text.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces_text.html.twig @@ -17,7 +17,13 @@
+ {%- filter escape('html') -%} + {{- include('@Twig/Exception/traces.txt.twig', { exception: exception, format: 'html' }, with_context = false) }} + {% endfilter %} ++ {% endif %}
+ * + * @internal */ trait ProxyTrait { diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index ac8b5a5fcc..b8e05d9e41 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -240,7 +240,7 @@ trait RedisTrait $cursor = null; do { $keys = $host instanceof \Predis\Client ? $host->scan($cursor, 'MATCH', $namespace.'*', 'COUNT', 1000) : $host->scan($cursor, $namespace.'*', 1000); - if (isset($keys[1]) && is_array($keys[1])) { + if (isset($keys[1]) && \is_array($keys[1])) { $cursor = $keys[0]; $keys = $keys[1]; } diff --git a/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php index 35c87c2207..f60323becd 100644 --- a/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php @@ -121,7 +121,7 @@ class JsonDescriptor extends Descriptor { return array( 'name' => '--'.$option->getName(), - 'shortcut' => $option->getShortcut() ? '-'.implode('|-', explode('|', $option->getShortcut())) : '', + 'shortcut' => $option->getShortcut() ? '-'.str_replace('|', '|-', $option->getShortcut()) : '', 'accept_value' => $option->acceptValue(), 'is_value_required' => $option->isValueRequired(), 'is_multiple' => $option->isArray(), diff --git a/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php b/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php index 106bff5114..d52ba55342 100644 --- a/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php @@ -70,7 +70,7 @@ class MarkdownDescriptor extends Descriptor { $name = '--'.$option->getName(); if ($option->getShortcut()) { - $name .= '|-'.implode('|-', explode('|', $option->getShortcut())).''; + $name .= '|-'.str_replace('|', '|-', $option->getShortcut()).''; } $this->write( diff --git a/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php b/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php index 0d239868c4..a2eaca3b18 100644 --- a/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php @@ -218,7 +218,7 @@ class XmlDescriptor extends Descriptor $pos = strpos($option->getShortcut(), '|'); if (false !== $pos) { $objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos)); - $objectXML->setAttribute('shortcuts', '-'.implode('|-', explode('|', $option->getShortcut()))); + $objectXML->setAttribute('shortcuts', '-'.str_replace('|', '|-', $option->getShortcut())); } else { $objectXML->setAttribute('shortcut', $option->getShortcut() ? '-'.$option->getShortcut() : ''); } diff --git a/src/Symfony/Component/Console/EventListener/ErrorListener.php b/src/Symfony/Component/Console/EventListener/ErrorListener.php index 3774f9e666..909d6ea3a1 100644 --- a/src/Symfony/Component/Console/EventListener/ErrorListener.php +++ b/src/Symfony/Component/Console/EventListener/ErrorListener.php @@ -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) diff --git a/src/Symfony/Component/Console/Input/InputOption.php b/src/Symfony/Component/Console/Input/InputOption.php index e4c855cae1..184e369b86 100644 --- a/src/Symfony/Component/Console/Input/InputOption.php +++ b/src/Symfony/Component/Console/Input/InputOption.php @@ -195,7 +195,7 @@ class InputOption * * @return bool */ - public function equals(InputOption $option) + public function equals(self $option) { return $option->getName() === $this->getName() && $option->getShortcut() === $this->getShortcut() diff --git a/src/Symfony/Component/Console/Tests/EventListener/ErrorListenerTest.php b/src/Symfony/Component/Console/Tests/EventListener/ErrorListenerTest.php index 17eaae0908..3794a2660a 100644 --- a/src/Symfony/Component/Console/Tests/EventListener/ErrorListenerTest.php +++ b/src/Symfony/Component/Console/Tests/EventListener/ErrorListenerTest.php @@ -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); diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index e536efac37..f4590b29a4 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -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); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php index 5ae3c53689..9cbe941581 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -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), + ); + } } diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json index 4440335932..0f43316313 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json @@ -21,7 +21,8 @@ "compound", "data_class", "empty_data", - "error_bubbling" + "error_bubbling", + "trim" ] }, "parent": { @@ -44,7 +45,6 @@ "property_path", "required", "translation_domain", - "trim", "upload_max_size_message" ] }, diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt index 3a0a452fd4..34120481ed 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt @@ -11,7 +11,7 @@ Symfony\Component\Form\Extension\Core\Type\ChoiceType (Block prefix: "choice") choice_name data_class attr csrf_message choice_translation_domain empty_data auto_initialize csrf_protection choice_value error_bubbling block_name csrf_token_id - choices by_reference csrf_token_manager + choices trim by_reference csrf_token_manager expanded data group_by disabled multiple help @@ -25,7 +25,6 @@ Symfony\Component\Form\Extension\Core\Type\ChoiceType (Block prefix: "choice") property_path required translation_domain - trim upload_max_size_message --------------------------- -------------------- ------------------------- ----------------------- diff --git a/src/Symfony/Component/HttpFoundation/File/MimeType/FileBinaryMimeTypeGuesser.php b/src/Symfony/Component/HttpFoundation/File/MimeType/FileBinaryMimeTypeGuesser.php index b00f58c4e8..83be44af67 100644 --- a/src/Symfony/Component/HttpFoundation/File/MimeType/FileBinaryMimeTypeGuesser.php +++ b/src/Symfony/Component/HttpFoundation/File/MimeType/FileBinaryMimeTypeGuesser.php @@ -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; } /** diff --git a/src/Symfony/Component/HttpFoundation/Response.php b/src/Symfony/Component/HttpFoundation/Response.php index c733c5e1dd..4de83a915f 100644 --- a/src/Symfony/Component/HttpFoundation/Response.php +++ b/src/Symfony/Component/HttpFoundation/Response.php @@ -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 */ diff --git a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php index 027b2b1761..672cc893fe 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php @@ -72,7 +72,7 @@ class ResponseCacheStrategy implements ResponseCacheStrategyInterface $response->setLastModified(null); } - if (!$response->isFresh()) { + if (!$response->isFresh() || !$response->isCacheable()) { $this->cacheable = false; } diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php index 5e4c322223..6d67a17739 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php @@ -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() diff --git a/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php b/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php index 88443e3b9c..36a41c039b 100644 --- a/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php +++ b/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php @@ -120,12 +120,16 @@ abstract class AnnotationClassLoader implements LoaderInterface } } - /** @var $annot RouteAnnotation */ - if (0 === $collection->count() && $class->hasMethod('__invoke') && $annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) { - $globals['path'] = null; - $globals['name'] = ''; - $globals['localized_paths'] = array(); - $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'] = ''; + $globals['localized_paths'] = array(); + + $this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke')); + } + } } return $collection; diff --git a/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php index 14e634ca71..ed517e692c 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php @@ -13,7 +13,7 @@ namespace Symfony\Component\Routing\Tests\Loader; use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\AnnotationRegistry; -use PHPUnit\Framework\TestCase; +use Symfony\Component\Routing\Annotation\Route as RouteAnnotation; use Symfony\Component\Routing\Loader\AnnotationClassLoader; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\AbstractClassController; @@ -35,7 +35,7 @@ use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\PrefixedActionLo use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\PrefixedActionPathController; use Symfony\Component\Routing\Tests\Fixtures\AnnotationFixtures\RouteWithPrefixController; -class AnnotationClassLoaderTest extends TestCase +class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest { /** * @var AnnotationClassLoader @@ -166,6 +166,51 @@ class AnnotationClassLoaderTest extends TestCase $this->assertEquals('/en/action', $routes->get('action.en')->getPath()); } + 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'), + ); + + $reader = $this->getReader(); + $reader + ->expects($this->exactly(1)) + ->method('getClassAnnotations') + ->will($this->returnValue(array(new RouteAnnotation($classRouteData1), new RouteAnnotation($classRouteData2)))) + ; + $reader + ->expects($this->once()) + ->method('getMethodAnnotations') + ->will($this->returnValue(array())) + ; + $loader = new class($reader) extends AnnotationClassLoader { + protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot) {} + }; + + $routeCollection = $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 testMissingPrefixLocale() { $this->expectException(\LogicException::class); diff --git a/src/Symfony/Component/Routing/Tests/Loader/AnnotationDirectoryLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/AnnotationDirectoryLoaderTest.php index 1e8ee39401..8a6668e0c2 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/AnnotationDirectoryLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/AnnotationDirectoryLoaderTest.php @@ -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'); } diff --git a/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php index 770fa5c579..85ffcb7f54 100644 --- a/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php @@ -160,10 +160,6 @@ class LazyLoadingMetadataFactory implements MetadataFactoryInterface $class = ltrim(is_object($value) ? get_class($value) : $value, '\\'); - if (class_exists($class) || interface_exists($class)) { - return true; - } - - return false; + return class_exists($class) || interface_exists($class, false); } }