Merge branch '3.0'

* 3.0:
  #17676 - making the proxy instantiation compatible with ProxyManager 2.x by detecting proxy features
  #17676 - making the proxy instantiation compatible with ProxyManager 2.x by detecting proxy features
  Fix bug when using an private aliased factory service
  [Form] fix tests added by #17798 by removing `choices_as_values`
  [Form] fix FQCN in tests added by #17798
  [DependencyInjection] Remove unused parameter of private property
  bug #17798 [Form] allow `choice_label` option to be `false`
  [Form] fix tests added by #17760 with FQCN
  ChoiceFormField of type "select" could be "disabled"
  Update contributing docs
  [Console] Fix escaping of trailing backslashes
  Fix constraint validator alias being required
  [DependencyInjection] Simplified code in AutowirePass
  [ci] clone with depth=1 to kill push-forced PRs
  Add check on If-Range header
This commit is contained in:
Fabien Potencier 2016-02-28 17:28:07 +01:00
commit a99d713b21
27 changed files with 871 additions and 74 deletions

View File

@ -1,5 +1,6 @@
| Q | A
| ------------- | ---
| Branch | master for features and deprecations / lowest applicable and maintained version otherwise
| Bug fix? | yes/no
| New feature? | yes/no
| BC breaks? | yes/no

View File

@ -2,6 +2,9 @@ language: php
sudo: false
git:
depth: 1
addons:
apt_packages:
- parallel

View File

@ -5,16 +5,20 @@ Symfony is an open source, community-driven project.
If you'd like to contribute, please read the following documents:
* [Contributing Code][1]: The document index related to contributions;
* [Reporting a Bug][1]
* [Submitting a Patch][2]
* [Symfony Core Team][3]
* [Security Issues][4]
* [Running Symfony Tests][5]
* [Our Backwards Compatibility Promise][6]
* [Coding Standards][7]
* [Conventions][8]
* [Submitting a Patch][2]: Guidelines for submitting a pull request;
* [Pull Request Template][3]: Template header to use in your pull request
description;
* [Backwards Compatibility][4]: Backward compatibility rules.
[1]: https://symfony.com/doc/current/contributing/code/index.html
[2]: https://symfony.com/doc/current/contributing/code/patches.html#check-list
[3]: https://symfony.com/doc/current/contributing/code/patches.html#make-a-pull-request
[4]: https://symfony.com/doc/current/contributing/code/bc.html#working-on-symfony-code
[1]: https://symfony.com/doc/current/contributing/code/bugs.html
[2]: https://symfony.com/doc/current/contributing/code/patches.html
[3]: https://symfony.com/doc/current/contributing/code/core_team.html
[4]: https://symfony.com/doc/current/contributing/code/security.html
[5]: https://symfony.com/doc/current/contributing/code/tests.html
[6]: https://symfony.com/doc/current/contributing/code/bc.html
[7]: https://symfony.com/doc/current/contributing/code/standards.html
[8]: https://symfony.com/doc/current/contributing/code/conventions.html

View File

@ -1,6 +1,5 @@
build: false
shallow_clone: true
platform: x86
clone_depth: 1
clone_folder: c:\projects\symfony
cache:

View File

@ -82,7 +82,7 @@
"doctrine/orm": "~2.4,>=2.4.5",
"doctrine/doctrine-bundle": "~1.4",
"monolog/monolog": "~1.11",
"ocramius/proxy-manager": "~0.4|~1.0",
"ocramius/proxy-manager": "~0.4|~1.0|~2.0",
"egulias/email-validator": "~1.2",
"symfony/polyfill-apcu": "~1.1",
"symfony/security-acl": "~2.8|~3.0",

View File

@ -74,10 +74,16 @@ class ProxyDumper implements DumperInterface
$methodName = 'get'.Container::camelize($id).'Service';
$proxyClass = $this->getProxyClassName($definition);
$generatedClass = $this->generateProxyClass($definition);
$constructorCall = $generatedClass->hasMethod('staticProxyConstructor')
? $proxyClass.'::staticProxyConstructor'
: 'new '.$proxyClass;
return <<<EOF
if (\$lazyLoad) {
$instantiation new $proxyClass(
$instantiation $constructorCall(
function (&\$wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface \$proxy) {
\$wrappedInstance = \$this->$methodName(false);
@ -97,11 +103,7 @@ EOF;
*/
public function getProxyCode(Definition $definition)
{
$generatedClass = new ClassGenerator($this->getProxyClassName($definition));
$this->proxyGenerator->generate(new \ReflectionClass($definition->getClass()), $generatedClass);
return $this->classGenerator->generate($generatedClass);
return $this->classGenerator->generate($this->generateProxyClass($definition));
}
/**
@ -115,4 +117,16 @@ EOF;
{
return str_replace('\\', '', $definition->getClass()).'_'.spl_object_hash($definition).$this->salt;
}
/**
* @return ClassGenerator
*/
private function generateProxyClass(Definition $definition)
{
$generatedClass = new ClassGenerator($this->getProxyClassName($definition));
$this->proxyGenerator->generate(new \ReflectionClass($definition->getClass()), $generatedClass);
return $generatedClass;
}
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Bridge\ProxyManager\Tests\LazyProxy\Dumper;
use ProxyManager\ProxyGenerator\LazyLoading\MethodGenerator\StaticProxyConstructor;
use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
@ -49,7 +50,11 @@ class PhpDumperTest extends \PHPUnit_Framework_TestCase
*/
public function testDumpContainerWithProxyServiceWillShareProxies()
{
require_once __DIR__.'/../Fixtures/php/lazy_service.php';
if (class_exists(StaticProxyConstructor::class)) { // detecting ProxyManager v2
require_once __DIR__.'/../Fixtures/php/lazy_service_with_hints.php';
} else {
require_once __DIR__.'/../Fixtures/php/lazy_service.php';
}
$container = new \LazyServiceProjectServiceContainer();

View File

@ -7,7 +7,7 @@ class ProjectServiceContainer extends Container
{
if ($lazyLoad) {
return $this->services['foo'] = new stdClass_%s(
return $this->services['foo'] =%sstdClass_%s(
function (&$wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface $proxy) {
$wrappedInstance = $this->getFooService(false);
@ -22,5 +22,5 @@ class ProjectServiceContainer extends Container
}
}
class stdClass_%s extends \stdClass implements \ProxyManager\Proxy\VirtualProxyInterface
class stdClass_%s extends \stdClass implements \ProxyManager\%s
{%a}%A

View File

@ -0,0 +1,187 @@
<?php
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
/**
* ProjectServiceContainer.
*
* This class has been auto-generated
* by the Symfony Dependency Injection Component.
*/
class LazyServiceProjectServiceContainer extends Container
{
/**
* Constructor.
*/
public function __construct()
{
$this->services = array();
}
/**
* Gets the 'foo' service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
* @param bool $lazyLoad whether to try lazy-loading the service with a proxy
*
* @return stdClass A stdClass instance.
*/
public function getFooService($lazyLoad = true)
{
if ($lazyLoad) {
return $this->services['foo'] = new stdClass_c1d194250ee2e2b7d2eab8b8212368a8(
function (&$wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface $proxy) {
$wrappedInstance = $this->getFooService(false);
$proxy->setProxyInitializer(null);
return true;
}
);
}
return new \stdClass();
}
}
class stdClass_c1d194250ee2e2b7d2eab8b8212368a8 extends \stdClass implements \ProxyManager\Proxy\LazyLoadingInterface, \ProxyManager\Proxy\ValueHolderInterface
{
/**
* @var \Closure|null initializer responsible for generating the wrapped object
*/
private $valueHolder5157dd96e88c0 = null;
/**
* @var \Closure|null initializer responsible for generating the wrapped object
*/
private $initializer5157dd96e8924 = null;
/**
* @override constructor for lazy initialization
*
* @param \Closure|null $initializer
*/
public function __construct($initializer)
{
$this->initializer5157dd96e8924 = $initializer;
}
/**
* @param string $name
*/
public function __get($name)
{
$this->initializer5157dd96e8924 && $this->initializer5157dd96e8924->__invoke($this->valueHolder5157dd96e88c0, $this, '__get', array('name' => $name));
return $this->valueHolder5157dd96e88c0->$name;
}
/**
* @param string $name
* @param mixed $value
*/
public function __set($name, $value)
{
$this->initializer5157dd96e8924 && $this->initializer5157dd96e8924->__invoke($this->valueHolder5157dd96e88c0, $this, '__set', array('name' => $name, 'value' => $value));
$this->valueHolder5157dd96e88c0->$name = $value;
}
/**
* @param string $name
*
* @return bool
*/
public function __isset($name)
{
$this->initializer5157dd96e8924 && $this->initializer5157dd96e8924->__invoke($this->valueHolder5157dd96e88c0, $this, '__isset', array('name' => $name));
return isset($this->valueHolder5157dd96e88c0->$name);
}
/**
* @param string $name
*/
public function __unset($name)
{
$this->initializer5157dd96e8924 && $this->initializer5157dd96e8924->__invoke($this->valueHolder5157dd96e88c0, $this, '__unset', array('name' => $name));
unset($this->valueHolder5157dd96e88c0->$name);
}
/**
*
*/
public function __clone()
{
$this->initializer5157dd96e8924 && $this->initializer5157dd96e8924->__invoke($this->valueHolder5157dd96e88c0, $this, '__clone', array());
$this->valueHolder5157dd96e88c0 = clone $this->valueHolder5157dd96e88c0;
}
/**
*
*/
public function __sleep()
{
$this->initializer5157dd96e8924 && $this->initializer5157dd96e8924->__invoke($this->valueHolder5157dd96e88c0, $this, '__sleep', array());
return array('valueHolder5157dd96e88c0');
}
/**
*
*/
public function __wakeup()
{
}
/**
* {@inheritdoc}
*/
public function setProxyInitializer(\Closure $initializer = null)
{
$this->initializer5157dd96e8924 = $initializer;
}
/**
* {@inheritdoc}
*/
public function getProxyInitializer()
{
return $this->initializer5157dd96e8924;
}
/**
* {@inheritdoc}
*/
public function initializeProxy() : bool
{
return $this->initializer5157dd96e8924 && $this->initializer5157dd96e8924->__invoke($this->valueHolder5157dd96e88c0, $this, 'initializeProxy', array());
}
/**
* {@inheritdoc}
*/
public function isProxyInitialized() : bool
{
return null !== $this->valueHolder5157dd96e88c0;
}
/**
* {@inheritdoc}
*/
public function getWrappedValueHolderValue()
{
return $this->valueHolder5157dd96e88c0;
}
}

View File

@ -69,7 +69,7 @@ class ProxyDumperTest extends \PHPUnit_Framework_TestCase
$code = $this->dumper->getProxyFactoryCode($definition, 'foo');
$this->assertStringMatchesFormat(
'%wif ($lazyLoad) {%wreturn $this->services[\'foo\'] = new '
'%wif ($lazyLoad) {%wreturn $this->services[\'foo\'] =%s'
.'SymfonyBridgeProxyManagerTestsLazyProxyPhpDumperProxyDumperTest_%s(%wfunction '
.'(&$wrappedInstance, \ProxyManager\Proxy\LazyLoadingInterface $proxy) {'
.'%w$wrappedInstance = $this->getFooService(false);%w$proxy->setProxyInitializer(null);'

View File

@ -18,7 +18,7 @@
"require": {
"php": ">=5.5.9",
"symfony/dependency-injection": "~2.8|~3.0",
"ocramius/proxy-manager": "~0.4|~1.0"
"ocramius/proxy-manager": "~0.4|~1.0|~2.0"
},
"require-dev": {
"symfony/config": "~2.8|~3.0"

View File

@ -27,6 +27,8 @@ class AddConstraintValidatorsPass implements CompilerPassInterface
if (isset($attributes[0]['alias'])) {
$validators[$attributes[0]['alias']] = $id;
}
$validators[$container->getDefinition($id)->getClass()] = $id;
}
$container->getDefinition('validator.validator_factory')->replaceArgument(1, $validators);

View File

@ -0,0 +1,87 @@
<?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.
*/
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConstraintValidatorsPass;
class AddConstraintValidatorsPassTest extends \PHPUnit_Framework_TestCase
{
public function testThatConstraintValidatorServicesAreProcessed()
{
$services = array(
'my_constraint_validator_service1' => array(0 => array('alias' => 'my_constraint_validator_alias1')),
'my_constraint_validator_service2' => array(),
);
$validatorFactoryDefinition = $this->getMock('Symfony\Component\DependencyInjection\Definition');
$container = $this->getMock(
'Symfony\Component\DependencyInjection\ContainerBuilder',
array('findTaggedServiceIds', 'getDefinition', 'hasDefinition')
);
$validatorDefinition1 = $this->getMock('Symfony\Component\DependencyInjection\Definition', array('getClass'));
$validatorDefinition2 = $this->getMock('Symfony\Component\DependencyInjection\Definition', array('getClass'));
$validatorDefinition1->expects($this->atLeastOnce())
->method('getClass')
->willReturn('My\Fully\Qualified\Class\Named\Validator1');
$validatorDefinition2->expects($this->atLeastOnce())
->method('getClass')
->willReturn('My\Fully\Qualified\Class\Named\Validator2');
$container->expects($this->any())
->method('getDefinition')
->with($this->anything())
->will($this->returnValueMap(array(
array('my_constraint_validator_service1', $validatorDefinition1),
array('my_constraint_validator_service2', $validatorDefinition2),
array('validator.validator_factory', $validatorFactoryDefinition),
)));
$container->expects($this->atLeastOnce())
->method('findTaggedServiceIds')
->will($this->returnValue($services));
$container->expects($this->atLeastOnce())
->method('hasDefinition')
->with('validator.validator_factory')
->will($this->returnValue(true));
$validatorFactoryDefinition->expects($this->once())
->method('replaceArgument')
->with(1, array(
'My\Fully\Qualified\Class\Named\Validator1' => 'my_constraint_validator_service1',
'my_constraint_validator_alias1' => 'my_constraint_validator_service1',
'My\Fully\Qualified\Class\Named\Validator2' => 'my_constraint_validator_service2',
));
$addConstraintValidatorsPass = new AddConstraintValidatorsPass();
$addConstraintValidatorsPass->process($container);
}
public function testThatCompilerPassIsIgnoredIfThereIsNoConstraintValidatorFactoryDefinition()
{
$definition = $this->getMock('Symfony\Component\DependencyInjection\Definition');
$container = $this->getMock(
'Symfony\Component\DependencyInjection\ContainerBuilder',
array('hasDefinition', 'findTaggedServiceIds', 'getDefinition')
);
$container->expects($this->never())->method('findTaggedServiceIds');
$container->expects($this->never())->method('getDefinition');
$container->expects($this->atLeastOnce())
->method('hasDefinition')
->with('validator.validator_factory')
->will($this->returnValue(false));
$definition->expects($this->never())->method('replaceArgument');
$addConstraintValidatorsPass = new AddConstraintValidatorsPass();
$addConstraintValidatorsPass->process($container);
}
}

View File

@ -33,7 +33,15 @@ class OutputFormatter implements OutputFormatterInterface
*/
public static function escape($text)
{
return preg_replace('/([^\\\\]?)</', '$1\\<', $text);
$text = preg_replace('/([^\\\\]?)</', '$1\\<', $text);
if ('\\' === substr($text, -1)) {
$len = strlen($text);
$text = rtrim($text, '\\');
$text .= str_repeat('<<', $len - strlen($text));
}
return $text;
}
/**
@ -131,7 +139,7 @@ class OutputFormatter implements OutputFormatterInterface
$message = (string) $message;
$offset = 0;
$output = '';
$tagRegex = '[a-z][a-z0-9_=;-]*';
$tagRegex = '[a-z][a-z0-9_=;-]*+';
preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $i => $match) {
$pos = $match[1];
@ -166,6 +174,10 @@ class OutputFormatter implements OutputFormatterInterface
$output .= $this->applyCurrentStyle(substr($message, $offset));
if (false !== strpos($output, '<<')) {
return strtr($output, array('\\<' => '<', '<<' => '\\'));
}
return str_replace('\\<', '<', $output);
}

View File

@ -98,8 +98,8 @@ class OutputFormatterTest extends \PHPUnit_Framework_TestCase
$formatter = new OutputFormatter(true);
$this->assertEquals(
"(\033[32mz>=2.0,<a2.3\033[39m)",
$formatter->format('(<info>'.$formatter->escape('z>=2.0,<a2.3').'</info>)')
"(\033[32mz>=2.0,<<<a2.3\\\033[39m)",
$formatter->format('(<info>'.$formatter->escape('z>=2.0,<\\<<a2.3\\').'</info>)')
);
$this->assertEquals(

View File

@ -148,45 +148,17 @@ class AutowirePass implements CompilerPassInterface
$this->types[$type] = $id;
}
// Cannot use reflection if the class isn't set
if (!$definition->getClass()) {
if (!$reflectionClass = $this->getReflectionClass($id, $definition)) {
return;
}
if ($reflectionClass = $this->getReflectionClass($id, $definition)) {
$this->extractInterfaces($id, $reflectionClass);
$this->extractAncestors($id, $reflectionClass);
foreach ($reflectionClass->getInterfaces() as $reflectionInterface) {
$this->set($reflectionInterface->name, $id);
}
}
/**
* Extracts the list of all interfaces implemented by a class.
*
* @param string $id
* @param \ReflectionClass $reflectionClass
*/
private function extractInterfaces($id, \ReflectionClass $reflectionClass)
{
foreach ($reflectionClass->getInterfaces() as $interfaceName => $reflectionInterface) {
$this->set($interfaceName, $id);
$this->extractInterfaces($id, $reflectionInterface);
}
}
/**
* Extracts all inherited types of a class.
*
* @param string $id
* @param \ReflectionClass $reflectionClass
*/
private function extractAncestors($id, \ReflectionClass $reflectionClass)
{
$this->set($reflectionClass->name, $id);
if ($reflectionParentClass = $reflectionClass->getParentClass()) {
$this->extractAncestors($id, $reflectionParentClass);
}
do {
$this->set($reflectionClass->name, $id);
} while ($reflectionClass = $reflectionClass->getParentClass());
}
/**
@ -272,6 +244,7 @@ class AutowirePass implements CompilerPassInterface
return $this->reflectionClasses[$id];
}
// Cannot use reflection if the class isn't set
if (!$class = $definition->getClass()) {
return;
}

View File

@ -315,7 +315,7 @@ class PhpDumper extends Dumper
throw new ServiceCircularReferenceException($id, array($id));
}
$code .= $this->addNewInstance($id, $sDefinition, '$'.$name, ' = ');
$code .= $this->addNewInstance($sDefinition, '$'.$name, ' = ');
if (!$this->hasReference($id, $sDefinition->getMethodCalls(), true) && !$this->hasReference($id, $sDefinition->getProperties(), true)) {
$code .= $this->addServiceMethodCalls(null, $sDefinition, $name);
@ -389,7 +389,7 @@ class PhpDumper extends Dumper
$instantiation .= ' = ';
}
$code = $this->addNewInstance($id, $definition, $return, $instantiation);
$code = $this->addNewInstance($definition, $return, $instantiation);
if (!$simple) {
$code .= "\n";
@ -676,7 +676,7 @@ EOF;
return $publicServices.$privateServices;
}
private function addNewInstance($id, Definition $definition, $return, $instantiation)
private function addNewInstance(Definition $definition, $return, $instantiation)
{
$class = $this->dumpValue($definition->getClass());

View File

@ -61,9 +61,10 @@ class AutowirePassTest extends \PHPUnit_Framework_TestCase
$pass = new AutowirePass();
$pass->process($container);
$this->assertCount(2, $container->getDefinition('g')->getArguments());
$this->assertCount(3, $container->getDefinition('g')->getArguments());
$this->assertEquals('f', (string) $container->getDefinition('g')->getArgument(0));
$this->assertEquals('f', (string) $container->getDefinition('g')->getArgument(1));
$this->assertEquals('f', (string) $container->getDefinition('g')->getArgument(2));
}
public function testCompleteExistingDefinition()
@ -332,13 +333,21 @@ interface EInterface extends DInterface
{
}
class F implements EInterface
interface IInterface
{
}
class I implements IInterface
{
}
class F extends I implements EInterface
{
}
class G
{
public function __construct(DInterface $d, EInterface $e)
public function __construct(DInterface $d, EInterface $e, IInterface $i)
{
}
}

View File

@ -16,6 +16,8 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
require_once __DIR__.'/../Fixtures/includes/foo.php';
class ReplaceAliasByActualDefinitionPassTest extends \PHPUnit_Framework_TestCase
{
public function testProcess()
@ -46,6 +48,26 @@ class ReplaceAliasByActualDefinitionPassTest extends \PHPUnit_Framework_TestCase
$this->assertSame('b_alias', (string) $resolvedFactory[0]);
}
/**
* @group legacy
*/
public function testPrivateAliasesInFactory()
{
$container = new ContainerBuilder();
$container->register('a', 'FooClass');
$container->register('b', 'FooClass')
->setFactoryService('a')
->setFactoryMethod('getInstance');
$container->register('c', 'stdClass')->setPublic(false);
$container->setAlias('c_alias', 'c');
$this->process($container);
$this->assertInstanceOf('FooClass', $container->get('b'));
}
/**
* @expectedException \InvalidArgumentException
*/

View File

@ -59,6 +59,10 @@ class ChoiceFormField extends FormField
*/
public function isDisabled()
{
if (parent::isDisabled() && 'select' === $this->type) {
return true;
}
foreach ($this->options as $option) {
if ($option['value'] == $this->value && $option['disabled']) {
return true;

View File

@ -120,6 +120,14 @@ class ChoiceFormFieldTest extends FormFieldTestCase
$this->assertEquals('bar', $field->getValue());
}
public function testSelectIsDisabled()
{
$node = $this->createSelectNode(array('foo' => false, 'bar' => true), array('disabled' => 'disabled'));
$field = new ChoiceFormField($node);
$this->assertTrue($field->isDisabled(), '->isDisabled() returns true for selects with a disabled attribute');
}
public function testMultipleSelects()
{
$node = $this->createSelectNode(array('foo' => false, 'bar' => false), array('multiple' => 'multiple'));

View File

@ -120,11 +120,21 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface
$key = $keys[$value];
$nextIndex = is_int($index) ? $index++ : call_user_func($index, $choice, $key, $value);
// BC normalize label to accept a false value
if (null === $label) {
// If the labels are null, use the original choice key by default
$label = (string) $key;
} elseif (false !== $label) {
// If "choice_label" is set to false and "expanded" is true, the value false
// should be passed on to the "label" option of the checkboxes/radio buttons
$dynamicLabel = call_user_func($label, $choice, $key, $value);
$label = false === $dynamicLabel ? false : (string) $dynamicLabel;
}
$view = new ChoiceView(
$choice,
$value,
// If the labels are null, use the original choice key by default
null === $label ? (string) $key : (string) call_user_func($label, $choice, $key, $value),
$label,
// The attributes may be a callable or a mapping from choice indices
// to nested arrays
is_callable($attr) ? call_user_func($attr, $choice, $key, $value) : (isset($attr[$key]) ? $attr[$key] : array())

View File

@ -340,7 +340,7 @@ class ChoiceType extends AbstractType
$resolver->setAllowedTypes('choices', array('null', 'array', '\Traversable'));
$resolver->setAllowedTypes('choice_translation_domain', array('null', 'bool', 'string'));
$resolver->setAllowedTypes('choice_loader', array('null', 'Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface'));
$resolver->setAllowedTypes('choice_label', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('choice_label', array('null', 'bool', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('choice_name', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('choice_value', array('null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));
$resolver->setAllowedTypes('choice_attr', array('null', 'array', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath'));

View File

@ -693,6 +693,126 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest
);
}
public function testSingleChoiceExpandedWithLabelsAsFalse()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'),
'choice_label' => false,
'multiple' => false,
'expanded' => true,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./div
[@class="radio"]
[
./label
[
./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked]
]
]
/following-sibling::div
[@class="radio"]
[
./label
[
./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)]
]
]
/following-sibling::input[@type="hidden"][@id="name__token"][@class="form-control"]
]
'
);
}
public function testSingleChoiceExpandedWithLabelsSetByCallable()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'),
'choice_label' => function ($choice, $label, $value) {
if ('&b' === $choice) {
return false;
}
return 'label.'.$value;
},
'multiple' => false,
'expanded' => true,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./div
[@class="radio"]
[
./label
[.=" [trans]label.&a[/trans]"]
[
./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked]
]
]
/following-sibling::div
[@class="radio"]
[
./label
[
./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)]
]
]
/following-sibling::div
[@class="radio"]
[
./label
[.=" [trans]label.&c[/trans]"]
[
./input[@type="radio"][@name="name"][@id="name_2"][@value="&c"][not(@checked)]
]
]
/following-sibling::input[@type="hidden"][@id="name__token"][@class="form-control"]
]
'
);
}
public function testSingleChoiceExpandedWithLabelsSetFalseByCallable()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'),
'choice_label' => function () {
return false;
},
'multiple' => false,
'expanded' => true,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./div
[@class="radio"]
[
./label
[
./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked]
]
]
/following-sibling::div
[@class="radio"]
[
./label
[
./input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)]
]
]
/following-sibling::input[@type="hidden"][@id="name__token"][@class="form-control"]
]
'
);
}
public function testSingleChoiceExpandedWithoutTranslation()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array(
@ -938,6 +1058,126 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest
);
}
public function testMultipleChoiceExpandedWithLabelsAsFalse()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a'), array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'),
'choice_label' => false,
'multiple' => true,
'expanded' => true,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./div
[@class="checkbox"]
[
./label
[
./input[@type="checkbox"][@name="name[]"][@id="name_0"][@value="&a"][@checked]
]
]
/following-sibling::div
[@class="checkbox"]
[
./label
[
./input[@type="checkbox"][@name="name[]"][@id="name_1"][@value="&b"][not(@checked)]
]
]
/following-sibling::input[@type="hidden"][@id="name__token"][@class="form-control"]
]
'
);
}
public function testMultipleChoiceExpandedWithLabelsSetByCallable()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a'), array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'),
'choice_label' => function ($choice, $label, $value) {
if ('&b' === $choice) {
return false;
}
return 'label.'.$value;
},
'multiple' => true,
'expanded' => true,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./div
[@class="checkbox"]
[
./label
[.=" [trans]label.&a[/trans]"]
[
./input[@type="checkbox"][@name="name[]"][@id="name_0"][@value="&a"][@checked]
]
]
/following-sibling::div
[@class="checkbox"]
[
./label
[
./input[@type="checkbox"][@name="name[]"][@id="name_1"][@value="&b"][not(@checked)]
]
]
/following-sibling::div
[@class="checkbox"]
[
./label
[.=" [trans]label.&c[/trans]"]
[
./input[@type="checkbox"][@name="name[]"][@id="name_2"][@value="&c"][not(@checked)]
]
]
/following-sibling::input[@type="hidden"][@id="name__token"][@class="form-control"]
]
'
);
}
public function testMultipleChoiceExpandedWithLabelsSetFalseByCallable()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a'), array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'),
'choice_label' => function () {
return false;
},
'multiple' => true,
'expanded' => true,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./div
[@class="checkbox"]
[
./label
[
./input[@type="checkbox"][@name="name[]"][@id="name_0"][@value="&a"][@checked]
]
]
/following-sibling::div
[@class="checkbox"]
[
./label
[
./input[@type="checkbox"][@name="name[]"][@id="name_1"][@value="&b"][not(@checked)]
]
]
/following-sibling::input[@type="hidden"][@id="name__token"][@class="form-control"]
]
'
);
}
public function testMultipleChoiceExpandedWithoutTranslation()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a', '&c'), array(

View File

@ -706,6 +706,160 @@ abstract class AbstractDivLayoutTest extends AbstractLayoutTest
);
}
public function testSingleChoiceExpandedWithLabelsAsFalse()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'),
'choice_label' => false,
'multiple' => false,
'expanded' => true,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked]
/following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)]
/following-sibling::input[@type="hidden"][@id="name__token"]
]
[count(./input)=3]
[count(./label)=1]
'
);
}
public function testSingleChoiceExpandedWithLabelsSetByCallable()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'),
'choice_label' => function ($choice, $label, $value) {
if ('&b' === $choice) {
return false;
}
return 'label.'.$value;
},
'multiple' => false,
'expanded' => true,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked]
/following-sibling::label[@for="name_0"][.="[trans]label.&a[/trans]"]
/following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)]
/following-sibling::input[@type="radio"][@name="name"][@id="name_2"][@value="&c"][not(@checked)]
/following-sibling::label[@for="name_2"][.="[trans]label.&c[/trans]"]
/following-sibling::input[@type="hidden"][@id="name__token"]
]
[count(./input)=4]
[count(./label)=3]
'
);
}
public function testSingleChoiceExpandedWithLabelsSetFalseByCallable()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'),
'choice_label' => function () {
return false;
},
'multiple' => false,
'expanded' => true,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked]
/following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)]
/following-sibling::input[@type="hidden"][@id="name__token"]
]
[count(./input)=3]
[count(./label)=1]
'
);
}
public function testMultipleChoiceExpandedWithLabelsAsFalse()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a'), array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'),
'choice_label' => false,
'multiple' => true,
'expanded' => true,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="checkbox"][@name="name[]"][@id="name_0"][@value="&a"][@checked]
/following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][@value="&b"][not(@checked)]
/following-sibling::input[@type="hidden"][@id="name__token"]
]
[count(./input)=3]
[count(./label)=1]
'
);
}
public function testMultipleChoiceExpandedWithLabelsSetByCallable()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a'), array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b', 'Choice&C' => '&c'),
'choice_label' => function ($choice, $label, $value) {
if ('&b' === $choice) {
return false;
}
return 'label.'.$value;
},
'multiple' => true,
'expanded' => true,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="checkbox"][@name="name[]"][@id="name_0"][@value="&a"][@checked]
/following-sibling::label[@for="name_0"][.="[trans]label.&a[/trans]"]
/following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][@value="&b"][not(@checked)]
/following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_2"][@value="&c"][not(@checked)]
/following-sibling::label[@for="name_2"][.="[trans]label.&c[/trans]"]
/following-sibling::input[@type="hidden"][@id="name__token"]
]
[count(./input)=4]
[count(./label)=3]
'
);
}
public function testMultipleChoiceExpandedWithLabelsSetFalseByCallable()
{
$form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', array('&a'), array(
'choices' => array('Choice&A' => '&a', 'Choice&B' => '&b'),
'choice_label' => function () {
return false;
},
'multiple' => true,
'expanded' => true,
));
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="checkbox"][@name="name[]"][@id="name_0"][@value="&a"][@checked]
/following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][@value="&b"][not(@checked)]
/following-sibling::input[@type="hidden"][@id="name__token"]
]
[count(./input)=3]
[count(./label)=1]
'
);
}
public function testFormEndWithRest()
{
$view = $this->factory->createNamedBuilder('name', 'Symfony\Component\Form\Extension\Core\Type\FormType')

View File

@ -221,7 +221,7 @@ class BinaryFileResponse extends Response
$this->maxlen = 0;
} elseif ($request->headers->has('Range')) {
// Process the range headers.
if (!$request->headers->has('If-Range') || $this->getEtag() === $request->headers->get('If-Range')) {
if (!$request->headers->has('If-Range') || $this->hasValidIfRangeHeader($request->headers->get('If-Range'))) {
$range = $request->headers->get('Range');
$fileSize = $this->file->getSize();
@ -254,6 +254,19 @@ class BinaryFileResponse extends Response
return $this;
}
private function hasValidIfRangeHeader($header)
{
if ($this->getEtag() === $header) {
return true;
}
if (null === $lastModified = $this->getLastModified()) {
return false;
}
return $lastModified->format('D, d M Y H:i:s').' GMT' === $header;
}
/**
* Sends the file.
*

View File

@ -80,6 +80,37 @@ class BinaryFileResponseTest extends ResponseTestCase
$this->assertEquals($responseRange, $response->headers->get('Content-Range'));
}
/**
* @dataProvider provideRanges
*/
public function testRequestsWithoutEtag($requestRange, $offset, $length, $responseRange)
{
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream'));
// do a request to get the LastModified
$request = Request::create('/');
$response->prepare($request);
$lastModified = $response->headers->get('Last-Modified');
// prepare a request for a range of the testing file
$request = Request::create('/');
$request->headers->set('If-Range', $lastModified);
$request->headers->set('Range', $requestRange);
$file = fopen(__DIR__.'/File/Fixtures/test.gif', 'r');
fseek($file, $offset);
$data = fread($file, $length);
fclose($file);
$this->expectOutputString($data);
$response = clone $response;
$response->prepare($request);
$response->sendContent();
$this->assertEquals(206, $response->getStatusCode());
$this->assertEquals($responseRange, $response->headers->get('Content-Range'));
}
public function provideRanges()
{
return array(
@ -91,6 +122,25 @@ class BinaryFileResponseTest extends ResponseTestCase
);
}
public function testRangeRequestsWithoutLastModifiedDate()
{
// prevent auto last modified
$response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, array('Content-Type' => 'application/octet-stream'), true, null, false, false);
// prepare a request for a range of the testing file
$request = Request::create('/');
$request->headers->set('If-Range', date('D, d M Y H:i:s').' GMT');
$request->headers->set('Range', 'bytes=1-4');
$this->expectOutputString(file_get_contents(__DIR__.'/File/Fixtures/test.gif'));
$response = clone $response;
$response->prepare($request);
$response->sendContent();
$this->assertEquals(200, $response->getStatusCode());
$this->assertNull($response->headers->get('Content-Range'));
}
/**
* @dataProvider provideFullFileRanges
*/