From dd9d076a0b3a6ddfa57ec305e5a4433e52a20621 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 29 Oct 2015 22:04:12 +0100 Subject: [PATCH 01/12] JsonDescriptor - encode container params only once --- .../FrameworkBundle/Console/Descriptor/JsonDescriptor.php | 2 +- .../Tests/Console/Descriptor/AbstractDescriptorTest.php | 1 + .../Tests/Console/Descriptor/ObjectsProvider.php | 7 +++++++ .../Tests/Fixtures/Descriptor/array_parameter.json | 3 +++ .../Tests/Fixtures/Descriptor/array_parameter.md | 4 ++++ .../Tests/Fixtures/Descriptor/array_parameter.txt | 1 + .../Tests/Fixtures/Descriptor/array_parameter.xml | 2 ++ 7 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.json create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.md create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.txt create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.xml diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php index f61af1cc95..791593de07 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php @@ -157,7 +157,7 @@ class JsonDescriptor extends Descriptor { $key = isset($options['parameter']) ? $options['parameter'] : ''; - $this->writeData(array($key => $this->formatParameter($parameter)), $options); + $this->writeData(array($key => $parameter), $options); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php index 8a9800bc86..192ba44bf7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php @@ -113,6 +113,7 @@ abstract class AbstractDescriptorTest extends \PHPUnit_Framework_TestCase $data = $this->getDescriptionTestData(ObjectsProvider::getContainerParameter()); $data[0][] = array('parameter' => 'database_name'); + $data[1][] = array('parameter' => 'twig.form.resources'); return $data; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php index df3f338fbb..52a6665416 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php @@ -72,9 +72,16 @@ class ObjectsProvider { $builder = new ContainerBuilder(); $builder->setParameter('database_name', 'symfony'); + $builder->setParameter('twig.form.resources', array( + 'bootstrap_3_horizontal_layout.html.twig', + 'bootstrap_3_layout.html.twig', + 'form_div_layout.html.twig', + 'form_table_layout.html.twig', + )); return array( 'parameter' => $builder, + 'array_parameter' => $builder, ); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.json new file mode 100644 index 0000000000..cb6809159a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.json @@ -0,0 +1,3 @@ +{ + "twig.form.resources": ["bootstrap_3_horizontal_layout.html.twig", "bootstrap_3_layout.html.twig", "form_div_layout.html.twig", "form_table_layout.html.twig"] +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.md new file mode 100644 index 0000000000..593be0cab7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.md @@ -0,0 +1,4 @@ +twig.form.resources +=================== + +["bootstrap_3_horizontal_layout.html.twig","bootstrap_3_layo... diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.txt new file mode 100644 index 0000000000..182037f246 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.txt @@ -0,0 +1 @@ +["bootstrap_3_horizontal_layout.html.twig","bootstrap_3_layo... diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.xml new file mode 100644 index 0000000000..0e16f57fc9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.xml @@ -0,0 +1,2 @@ + +["bootstrap_3_horizontal_layout.html.twig","bootstrap_3_layo... From b15bdca96d143b9f3b9a78e33c9391a3a3acb8c6 Mon Sep 17 00:00:00 2001 From: Warnar Boekkooi Date: Wed, 4 Nov 2015 14:39:01 +0800 Subject: [PATCH 02/12] [Serializer] PropertyNormalizer shouldn't set static properties --- .../Normalizer/PropertyNormalizer.php | 6 +++++- .../Normalizer/PropertyNormalizerTest.php | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php index c8e83d1f79..abbc74f27c 100644 --- a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php @@ -50,7 +50,7 @@ class PropertyNormalizer extends AbstractNormalizer $allowedAttributes = $this->getAllowedAttributes($object, $context, true); foreach ($reflectionObject->getProperties() as $property) { - if (in_array($property->name, $this->ignoredAttributes)) { + if (in_array($property->name, $this->ignoredAttributes) || $property->isStatic()) { continue; } @@ -110,6 +110,10 @@ class PropertyNormalizer extends AbstractNormalizer if ($allowed && !$ignored && $reflectionClass->hasProperty($propertyName)) { $property = $reflectionClass->getProperty($propertyName); + if ($property->isStatic()) { + continue; + } + // Override visibility if (!$property->isPublic()) { $property->setAccessible(true); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php index 62e6d5d925..fd30381800 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -409,6 +409,14 @@ class PropertyNormalizerTest extends \PHPUnit_Framework_TestCase ); } + public function testDenormalizeShouldIgnoreStaticProperty() + { + $obj = $this->normalizer->denormalize(array('outOfScope' => true), __NAMESPACE__.'\PropertyDummy'); + + $this->assertEquals(new PropertyDummy(), $obj); + $this->assertEquals('out_of_scope', PropertyDummy::$outOfScope); + } + /** * @expectedException \Symfony\Component\Serializer\Exception\LogicException * @expectedExceptionMessage Cannot normalize attribute "bar" because injected serializer is not a normalizer @@ -429,10 +437,16 @@ class PropertyNormalizerTest extends \PHPUnit_Framework_TestCase { $this->assertFalse($this->normalizer->supportsNormalization(new \ArrayObject())); } + + public function testNoStaticPropertySupport() + { + $this->assertFalse($this->normalizer->supportsNormalization(new StaticPropertyDummy())); + } } class PropertyDummy { + public static $outOfScope = 'out_of_scope'; public $foo; private $bar; protected $camelCase; @@ -491,3 +505,9 @@ class PropertyCamelizedDummy $this->kevinDunglas = $kevinDunglas; } } + +class StaticPropertyDummy +{ + private static $property = 'value'; +} + From d8d4405e50f00f3e32023fd50f753165ccd17630 Mon Sep 17 00:00:00 2001 From: Warnar Boekkooi Date: Wed, 4 Nov 2015 15:08:11 +0800 Subject: [PATCH 03/12] [Serializer] GetSetNormalizer shouldn't set/get static methods --- .../Normalizer/GetSetMethodNormalizer.php | 15 ++++--- .../Normalizer/GetSetMethodNormalizerTest.php | 39 +++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index 44a71cf1b4..183417c254 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -114,7 +114,7 @@ class GetSetMethodNormalizer extends AbstractNormalizer if ($allowed && !$ignored) { $setter = 'set'.ucfirst($attribute); - if (in_array($setter, $classMethods)) { + if (in_array($setter, $classMethods) && !$reflectionClass->getMethod($setter)->isStatic()) { $object->$setter($value); } } @@ -170,10 +170,13 @@ class GetSetMethodNormalizer extends AbstractNormalizer { $methodLength = strlen($method->name); - return ( - ((0 === strpos($method->name, 'get') && 3 < $methodLength) || - (0 === strpos($method->name, 'is') && 2 < $methodLength)) && - 0 === $method->getNumberOfRequiredParameters() - ); + return + !$method->isStatic() && + ( + ((0 === strpos($method->name, 'get') && 3 < $methodLength) || + (0 === strpos($method->name, 'is') && 2 < $methodLength)) && + 0 === $method->getNumberOfRequiredParameters() + ) + ; } } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index 3baf58703f..1d487c2fdd 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -549,11 +549,24 @@ class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase ); } + public function testDenormalizeShouldNotSetStaticAttribute() + { + $obj = $this->normalizer->denormalize(array('staticObject' => true), __NAMESPACE__.'\GetSetDummy'); + + $this->assertEquals(new GetSetDummy(), $obj); + $this->assertNull(GetSetDummy::getStaticObject()); + } + public function testNoTraversableSupport() { $this->assertFalse($this->normalizer->supportsNormalization(new \ArrayObject())); } + public function testNoStaticGetSetSupport() + { + $this->assertFalse($this->normalizer->supportsNormalization(new ObjectWithJustStaticSetterDummy())); + } + public function testPrivateSetter() { $obj = $this->normalizer->denormalize(array('foo' => 'foobar'), __NAMESPACE__.'\ObjectWithPrivateSetterDummy'); @@ -568,6 +581,7 @@ class GetSetDummy private $baz; protected $camelCase; protected $object; + private static $staticObject; public function getFoo() { @@ -628,6 +642,16 @@ class GetSetDummy { return $this->object; } + + public static function getStaticObject() + { + return self::$staticObject; + } + + public static function setStaticObject($object) + { + self::$staticObject = $object; + } } class GetConstructorDummy @@ -799,3 +823,18 @@ class ObjectWithPrivateSetterDummy { } } + +class ObjectWithJustStaticSetterDummy +{ + private static $foo = 'bar'; + + public static function getFoo() + { + return self::$foo; + } + + public static function setFoo($foo) + { + self::$foo = $foo; + } +} From 352dfb9890362f222dc0f15981cd17ca3f552934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 3 Nov 2015 10:17:55 -0800 Subject: [PATCH 04/12] [PropertyAccess] Fix dynamic property accessing. --- src/Symfony/Component/PropertyAccess/PropertyAccessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index c9466b8cbd..a55ccf3399 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -404,7 +404,7 @@ class PropertyAccessor implements PropertyAccessorInterface // returns true, consequently the following line will result in a // fatal error. - $object->{$access[self::ACCESS_NAME]} = $value; + $object->$property = $value; } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { $object->{$access[self::ACCESS_NAME]}($value); } else { From 916f9e0671388d33ef70ccd5d290679f3eab328d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 4 Nov 2015 20:02:36 +0100 Subject: [PATCH 05/12] [PropertyAccess] Test access to dynamic properties --- .../Tests/PropertyAccessorTest.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 5b127e1bc4..a38a3ef967 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -375,4 +375,32 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase $this->assertEquals('foobar', $object->getProperty()); } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testSetValue($objectOrArray, $path) + { + $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); + + $this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path)); + } + + public function getValidPropertyPaths() + { + return array( + array(array('Bernhard', 'Schussek'), '[0]', 'Bernhard'), + array(array('Bernhard', 'Schussek'), '[1]', 'Schussek'), + array(array('firstName' => 'Bernhard'), '[firstName]', 'Bernhard'), + array(array('index' => array('firstName' => 'Bernhard')), '[index][firstName]', 'Bernhard'), + array((object) array('firstName' => 'Bernhard'), 'firstName', 'Bernhard'), + array((object) array('property' => array('firstName' => 'Bernhard')), 'property[firstName]', 'Bernhard'), + array(array('index' => (object) array('firstName' => 'Bernhard')), '[index].firstName', 'Bernhard'), + array((object) array('property' => (object) array('firstName' => 'Bernhard')), 'property.firstName', 'Bernhard'), + + // Missing indices + array(array('index' => array()), '[index][firstName]', null), + array(array('root' => array('index' => array())), '[root][index][firstName]', null), + ); + } } From 405d4a8ead76a1c05425d903a0214834d8537d43 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 27 Sep 2015 17:20:43 +0200 Subject: [PATCH 06/12] trigger deprecation warning when using empty_value The `empty_value` option is deprecated in the `choice`, `date`, and `time` form types. Therefore, a deprecation warning must be triggered when the users configures a value for this option. The `datetime` form type does not need to be updated as it passes configured values to the `date` and `time` form types which trigger deprecation warnings anyway. --- .../Form/Extension/Core/Type/ChoiceType.php | 16 ++++++++-------- .../Form/Extension/Core/Type/DateType.php | 15 ++++++++------- .../Form/Extension/Core/Type/TimeType.php | 16 ++++++++-------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 5102efb55c..528834d1e3 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -248,13 +248,8 @@ class ChoiceType extends AbstractType return ''; }; - $emptyValue = function (Options $options) { - return $options['required'] ? null : ''; - }; - - // for BC with the "empty_value" option $placeholder = function (Options $options) { - return $options['empty_value']; + return $options['required'] ? null : ''; }; $choiceListNormalizer = function (Options $options, $choiceList) use ($choiceListFactory) { @@ -287,6 +282,12 @@ class ChoiceType extends AbstractType }; $placeholderNormalizer = function (Options $options, $placeholder) { + if (!is_object($options['empty_value']) || !$options['empty_value'] instanceof \Exception) { + @trigger_error('The form option "empty_value" is deprecated since version 2.6 and will be removed in 3.0. Use "placeholder" instead.', E_USER_DEPRECATED); + + $placeholder = $options['empty_value']; + } + if ($options['multiple']) { // never use an empty value for this case return; @@ -328,7 +329,7 @@ class ChoiceType extends AbstractType 'preferred_choices' => array(), 'group_by' => null, 'empty_data' => $emptyData, - 'empty_value' => $emptyValue, // deprecated + 'empty_value' => new \Exception(), // deprecated 'placeholder' => $placeholder, 'error_bubbling' => false, 'compound' => $compound, @@ -340,7 +341,6 @@ class ChoiceType extends AbstractType )); $resolver->setNormalizer('choice_list', $choiceListNormalizer); - $resolver->setNormalizer('empty_value', $placeholderNormalizer); $resolver->setNormalizer('placeholder', $placeholderNormalizer); $resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer); diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php index 4b79dcb662..fb3e9a7182 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/DateType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/DateType.php @@ -175,15 +175,17 @@ class DateType extends AbstractType return $options['widget'] !== 'single_text'; }; - $emptyValue = $placeholderDefault = function (Options $options) { + $placeholder = $placeholderDefault = function (Options $options) { return $options['required'] ? null : ''; }; - $placeholder = function (Options $options) { - return $options['empty_value']; - }; - $placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) { + if (!is_object($options['empty_value']) || !$options['empty_value'] instanceof \Exception) { + @trigger_error('The form option "empty_value" is deprecated since version 2.6 and will be removed in 3.0. Use "placeholder" instead.', E_USER_DEPRECATED); + + $placeholder = $options['empty_value']; + } + if (is_array($placeholder)) { $default = $placeholderDefault($options); @@ -213,7 +215,7 @@ class DateType extends AbstractType 'format' => $format, 'model_timezone' => null, 'view_timezone' => null, - 'empty_value' => $emptyValue, // deprecated + 'empty_value' => new \Exception(), // deprecated 'placeholder' => $placeholder, 'html5' => true, // Don't modify \DateTime classes by reference, we treat @@ -228,7 +230,6 @@ class DateType extends AbstractType 'compound' => $compound, )); - $resolver->setNormalizer('empty_value', $placeholderNormalizer); $resolver->setNormalizer('placeholder', $placeholderNormalizer); $resolver->setAllowedValues('input', array( diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php index 49f77c5bd1..8002f0b4ee 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/TimeType.php @@ -163,16 +163,17 @@ class TimeType extends AbstractType return $options['widget'] !== 'single_text'; }; - $emptyValue = $placeholderDefault = function (Options $options) { + $placeholder = $placeholderDefault = function (Options $options) { return $options['required'] ? null : ''; }; - // for BC with the "empty_value" option - $placeholder = function (Options $options) { - return $options['empty_value']; - }; - $placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) { + if (!is_object($options['empty_value']) || !$options['empty_value'] instanceof \Exception) { + @trigger_error('The form option "empty_value" is deprecated since version 2.6 and will be removed in 3.0. Use "placeholder" instead.', E_USER_DEPRECATED); + + $placeholder = $options['empty_value']; + } + if (is_array($placeholder)) { $default = $placeholderDefault($options); @@ -199,7 +200,7 @@ class TimeType extends AbstractType 'with_seconds' => false, 'model_timezone' => null, 'view_timezone' => null, - 'empty_value' => $emptyValue, // deprecated + 'empty_value' => new \Exception(), // deprecated 'placeholder' => $placeholder, 'html5' => true, // Don't modify \DateTime classes by reference, we treat @@ -214,7 +215,6 @@ class TimeType extends AbstractType 'compound' => $compound, )); - $resolver->setNormalizer('empty_value', $placeholderNormalizer); $resolver->setNormalizer('placeholder', $placeholderNormalizer); $resolver->setAllowedValues('input', array( From aa4cc90a87719a3fdccea233156a52cef3139b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 3 Nov 2015 10:24:23 -0800 Subject: [PATCH 07/12] [PropertyAccess] Port of the performance optimization from 2.3 --- .../PropertyAccess/PropertyAccessor.php | 286 +++++++++++++----- 1 file changed, 209 insertions(+), 77 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 81d0eed0f7..008f55001f 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -20,12 +20,24 @@ use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; * Default implementation of {@link PropertyAccessorInterface}. * * @author Bernhard Schussek + * @author Kévin Dunglas */ class PropertyAccessor implements PropertyAccessorInterface { const VALUE = 0; const IS_REF = 1; const IS_REF_CHAINED = 2; + const ACCESS_HAS_PROPERTY = 0; + const ACCESS_TYPE = 1; + const ACCESS_NAME = 2; + const ACCESS_REF = 3; + const ACCESS_ADDER = 4; + const ACCESS_REMOVER = 5; + const ACCESS_TYPE_METHOD = 0; + const ACCESS_TYPE_PROPERTY = 1; + const ACCESS_TYPE_MAGIC = 2; + const ACCESS_TYPE_ADDER_AND_REMOVER = 3; + const ACCESS_TYPE_NOT_FOUND = 4; /** * @var bool @@ -37,6 +49,16 @@ class PropertyAccessor implements PropertyAccessorInterface */ private $ignoreInvalidIndices; + /** + * @var array + */ + private $readPropertyCache = array(); + + /** + * @var array + */ + private $writePropertyCache = array(); + /** * Should not be used by application code. Use * {@link PropertyAccess::createPropertyAccessor()} instead. @@ -78,7 +100,7 @@ class PropertyAccessor implements PropertyAccessorInterface self::IS_REF => true, self::IS_REF_CHAINED => true, )); - + $propertyMaxIndex = count($propertyValues) - 1; for ($i = $propertyMaxIndex; $i >= 0; --$i) { @@ -330,51 +352,31 @@ class PropertyAccessor implements PropertyAccessorInterface throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you intended to write the property path as "[%s]" instead.', $property, $property)); } - $camelized = $this->camelize($property); - $reflClass = new \ReflectionClass($object); - $getter = 'get'.$camelized; - $getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item) - $isser = 'is'.$camelized; - $hasser = 'has'.$camelized; - $classHasProperty = $reflClass->hasProperty($property); + $access = $this->getReadAccessInfo($object, $property); - if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) { - $result[self::VALUE] = $object->$getter(); - } elseif ($this->isMethodAccessible($reflClass, $getsetter, 0)) { - $result[self::VALUE] = $object->$getsetter(); - } elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) { - $result[self::VALUE] = $object->$isser(); - } elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) { - $result[self::VALUE] = $object->$hasser(); - } elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) { - $result[self::VALUE] = &$object->$property; - $result[self::IS_REF] = true; - } elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) { - $result[self::VALUE] = $object->$property; - } elseif (!$classHasProperty && property_exists($object, $property)) { + if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) { + $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}(); + } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) { + if ($access[self::ACCESS_REF]) { + $result[self::VALUE] = &$object->{$access[self::ACCESS_NAME]}; + $result[self::IS_REF] = true; + } else { + $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}; + } + } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) { // Needed to support \stdClass instances. We need to explicitly // exclude $classHasProperty, otherwise if in the previous clause // a *protected* property was found on the class, property_exists() // returns true, consequently the following line will result in a // fatal error. + $result[self::VALUE] = &$object->$property; $result[self::IS_REF] = true; - } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) { + } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { // we call the getter and hope the __call do the job - $result[self::VALUE] = $object->$getter(); + $result[self::VALUE] = $object->{$access[self::ACCESS_NAME]}(); } else { - $methods = array($getter, $getsetter, $isser, $hasser, '__get'); - if ($this->magicCall) { - $methods[] = '__call'; - } - - throw new NoSuchPropertyException(sprintf( - 'Neither the property "%s" nor one of the methods "%s()" '. - 'exist and have public access in class "%s".', - $property, - implode('()", "', $methods), - $reflClass->name - )); + throw new NoSuchPropertyException($access[self::ACCESS_NAME]); } // Objects are always passed around by reference @@ -385,6 +387,81 @@ class PropertyAccessor implements PropertyAccessorInterface return $result; } + /** + * Guesses how to read the property value. + * + * @param string $object + * @param string $property + * + * @return array + */ + private function getReadAccessInfo($object, $property) + { + $key = get_class($object).'::'.$property; + + if (isset($this->readPropertyCache[$key])) { + $access = $this->readPropertyCache[$key]; + } else { + $access = array(); + + $reflClass = new \ReflectionClass($object); + $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property); + $camelProp = $this->camelize($property); + $getter = 'get'.$camelProp; + $getsetter = lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item) + $isser = 'is'.$camelProp; + $hasser = 'has'.$camelProp; + $classHasProperty = $reflClass->hasProperty($property); + + if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $getter; + } elseif ($reflClass->hasMethod($getsetter) && $reflClass->getMethod($getsetter)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $getsetter; + } elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $isser; + } elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $hasser; + } elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; + $access[self::ACCESS_NAME] = $property; + $access[self::ACCESS_REF] = false; + } elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; + $access[self::ACCESS_NAME] = $property; + $access[self::ACCESS_REF] = true; + + $result[self::VALUE] = &$object->$property; + $result[self::IS_REF] = true; + } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) { + // we call the getter and hope the __call do the job + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC; + $access[self::ACCESS_NAME] = $getter; + } else { + $methods = array($getter, $getsetter, $isser, $hasser, '__get'); + if ($this->magicCall) { + $methods[] = '__call'; + } + + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; + $access[self::ACCESS_NAME] = sprintf( + 'Neither the property "%s" nor one of the methods "%s()" '. + 'exist and have public access in class "%s".', + $property, + implode('()", "', $methods), + $reflClass->name + ); + } + + $this->readPropertyCache[$key] = $access; + } + + return $access; + } + /** * Sets the value of an index in a given array-accessible value. * @@ -419,55 +496,26 @@ class PropertyAccessor implements PropertyAccessorInterface throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); } - $reflClass = new \ReflectionClass($object); - $camelized = $this->camelize($property); - $singulars = (array) StringUtil::singularify($camelized); + $access = $this->getWriteAccessInfo($object, $property, $value); - if (is_array($value) || $value instanceof \Traversable) { - $methods = $this->findAdderAndRemover($reflClass, $singulars); - - // Use addXxx() and removeXxx() to write the collection - if (null !== $methods) { - $this->writeCollection($object, $property, $value, $methods[0], $methods[1]); - - return; - } - } - - $setter = 'set'.$camelized; - $getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item) - $classHasProperty = $reflClass->hasProperty($property); - - if ($this->isMethodAccessible($reflClass, $setter, 1)) { - $object->$setter($value); - } elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) { - $object->$getsetter($value); - } elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) { - $object->$property = $value; - } elseif ($this->isMethodAccessible($reflClass, '__set', 2)) { - $object->$property = $value; - } elseif (!$classHasProperty && property_exists($object, $property)) { + if (self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE]) { + $object->{$access[self::ACCESS_NAME]}($value); + } elseif (self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE]) { + $object->{$access[self::ACCESS_NAME]} = $value; + } elseif (self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE]) { + $this->writeCollection($object, $property, $value, $access[self::ACCESS_ADDER], $access[self::ACCESS_REMOVER]); + } elseif (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) { // Needed to support \stdClass instances. We need to explicitly // exclude $classHasProperty, otherwise if in the previous clause // a *protected* property was found on the class, property_exists() // returns true, consequently the following line will result in a // fatal error. + $object->$property = $value; - } elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) { - // we call the getter and hope the __call do the job - $object->$setter($value); + } elseif (self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]) { + $object->{$access[self::ACCESS_NAME]}($value); } else { - throw new NoSuchPropertyException(sprintf( - 'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '. - '"__set()" or "__call()" exist and have public access in class "%s".', - $property, - implode('', array_map(function ($singular) { - return '"add'.$singular.'()"/"remove'.$singular.'()", '; - }, $singulars)), - $setter, - $getsetter, - $reflClass->name - )); + throw new NoSuchPropertyException($access[self::ACCESS_NAME]); } } @@ -519,6 +567,90 @@ class PropertyAccessor implements PropertyAccessorInterface } } + /** + * Guesses how to write the property value. + * + * @param string $object + * @param string $property + * @param mixed $value + * + * @return array + */ + private function getWriteAccessInfo($object, $property, $value) + { + $key = get_class($object).'::'.$property; + $guessedAdders = ''; + + if (isset($this->writePropertyCache[$key])) { + $access = $this->writePropertyCache[$key]; + } else { + $access = array(); + + $reflClass = new \ReflectionClass($object); + $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property); + $camelized = $this->camelize($property); + $singulars = (array) StringUtil::singularify($camelized); + + if (is_array($value) || $value instanceof \Traversable) { + $methods = $this->findAdderAndRemover($reflClass, $singulars); + + if (null === $methods) { + // It is sufficient to include only the adders in the error + // message. If the user implements the adder but not the remover, + // an exception will be thrown in findAdderAndRemover() that + // the remover has to be implemented as well. + $guessedAdders = '"add'.implode('()", "add', $singulars).'()", '; + } else { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER; + $access[self::ACCESS_ADDER] = $methods[0]; + $access[self::ACCESS_REMOVER] = $methods[1]; + } + } + + if (!isset($access[self::ACCESS_TYPE])) { + $setter = 'set'.$this->camelize($property); + $getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item) + + $classHasProperty = $reflClass->hasProperty($property); + + if ($this->isMethodAccessible($reflClass, $setter, 1)) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $setter; + } elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $getsetter; + } elseif ($this->isMethodAccessible($reflClass, '__set', 2)) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; + $access[self::ACCESS_NAME] = $property; + } elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; + $access[self::ACCESS_NAME] = $property; + } elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) { + // we call the getter and hope the __call do the job + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC; + $access[self::ACCESS_NAME] = $setter; + } else { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; + $access[self::ACCESS_NAME] = sprintf( + 'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '. + '"__set()" or "__call()" exist and have public access in class "%s".', + $property, + implode('', array_map(function ($singular) { + return '"add'.$singular.'()"/"remove'.$singular.'()", '; + }, $singulars)), + $setter, + $getsetter, + $reflClass->name + ); + } + } + + $this->writePropertyCache[$key] = $access; + } + + return $access; + } + /** * Returns whether a property is writable in the given object. * From 9e41d2785188f89450cfec6754cc938581d36375 Mon Sep 17 00:00:00 2001 From: Hugo Hamon Date: Fri, 6 Nov 2015 11:15:01 +0100 Subject: [PATCH 08/12] [Bridge] [PhpUnit] fixes documentation markup. --- src/Symfony/Bridge/PhpUnit/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bridge/PhpUnit/README.md b/src/Symfony/Bridge/PhpUnit/README.md index afc9d8dd3b..7b3a7ef677 100644 --- a/src/Symfony/Bridge/PhpUnit/README.md +++ b/src/Symfony/Bridge/PhpUnit/README.md @@ -12,7 +12,7 @@ It comes with the following features: By default any non-legacy-tagged or any non-@-silenced deprecation notices will make tests fail. -This can be changed by setting the SYMFONY_DEPRECATIONS_HELPER environment +This can be changed by setting the `SYMFONY_DEPRECATIONS_HELPER` environment variable to `weak`. This will make the bridge ignore deprecation notices and is useful to projects that must use deprecated interfaces for backward compatibility reasons. @@ -33,7 +33,7 @@ A summary of deprecation notices is displayed at the end of the test suite: Usage ----- -Add this bridge to the `require-dev` section of your composer.json file +Add this bridge to the `require-dev` section of your `composer.json` file (not in `require`) with e.g. `composer require --dev "symfony/phpunit-bridge"`. When running `phpunit`, you will see a summary of deprecation notices at the end From edd5633374857e31d57c391bea88b8d4d343d55c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 6 Nov 2015 09:22:03 +0100 Subject: [PATCH 09/12] [VarDumper] Fix PHP7 type-hints compat --- .../VarDumper/Caster/ReflectionCaster.php | 6 ++++- .../Tests/Caster/ReflectionCasterTest.php | 27 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php index a302835f08..cacd2113d7 100644 --- a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php @@ -167,7 +167,11 @@ class ReflectionCaster )); try { - if ($c->isArray()) { + if (method_exists($c, 'hasType')) { + if ($c->hasType()) { + $a[$prefix.'typeHint'] = $c->getType()->__toString(); + } + } elseif ($c->isArray()) { $a[$prefix.'typeHint'] = 'array'; } elseif (method_exists($c, 'isCallable') && $c->isCallable()) { $a[$prefix.'typeHint'] = 'callable'; diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php index 4120d341da..5c306a6710 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php @@ -81,17 +81,38 @@ EOTXT /** * @requires PHP 7.0 */ - public function testReturnType() + public function testReflectionParameterScalar() { - $f = eval('return function ():int {};'); + $f = eval('return function (int $a) {};'); + $var = new \ReflectionParameter($f, 0); $this->assertDumpMatchesFormat( <<<'EOTXT' +ReflectionParameter { + +name: "a" + position: 0 + typeHint: "int" +} +EOTXT + , $var + ); + } + + /** + * @requires PHP 7.0 + */ + public function testReturnType() + { + $f = eval('return function ():int {};'); + $line = __LINE__ - 1; + + $this->assertDumpMatchesFormat( + << Date: Tue, 20 Oct 2015 00:12:28 +0200 Subject: [PATCH 10/12] fix race condition at mkdir (#16258) --- src/Symfony/Component/HttpKernel/HttpCache/Store.php | 10 +++++++--- .../HttpKernel/Profiler/FileProfilerStorage.php | 10 ++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Store.php b/src/Symfony/Component/HttpKernel/HttpCache/Store.php index 4901e2cf29..eca3adec1b 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Store.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Store.php @@ -32,12 +32,16 @@ class Store implements StoreInterface * Constructor. * * @param string $root The path to the cache directory + * + * @throws \RuntimeException */ public function __construct($root) { $this->root = $root; if (!is_dir($this->root)) { - mkdir($this->root, 0777, true); + if (false === @mkdir($this->root, 0777, true) && !is_dir($this->root)) { + throw new \RuntimeException(sprintf('Unable to create the store directory (%s).', $this->root)); + } } $this->keyCache = new \SplObjectStorage(); $this->locks = array(); @@ -74,7 +78,7 @@ class Store implements StoreInterface public function lock(Request $request) { $path = $this->getPath($this->getCacheKey($request).'.lck'); - if (!is_dir(dirname($path)) && false === @mkdir(dirname($path), 0777, true)) { + if (!is_dir(dirname($path)) && false === @mkdir(dirname($path), 0777, true) && !is_dir(dirname($path))) { return false; } @@ -338,7 +342,7 @@ class Store implements StoreInterface private function save($key, $data) { $path = $this->getPath($key); - if (!is_dir(dirname($path)) && false === @mkdir(dirname($path), 0777, true)) { + if (!is_dir(dirname($path)) && false === @mkdir(dirname($path), 0777, true) && !is_dir(dirname($path))) { return false; } diff --git a/src/Symfony/Component/HttpKernel/Profiler/FileProfilerStorage.php b/src/Symfony/Component/HttpKernel/Profiler/FileProfilerStorage.php index bda87e80e1..8677b57e4f 100644 --- a/src/Symfony/Component/HttpKernel/Profiler/FileProfilerStorage.php +++ b/src/Symfony/Component/HttpKernel/Profiler/FileProfilerStorage.php @@ -41,8 +41,8 @@ class FileProfilerStorage implements ProfilerStorageInterface } $this->folder = substr($dsn, 5); - if (!is_dir($this->folder)) { - mkdir($this->folder, 0777, true); + if (!is_dir($this->folder) && false === @mkdir($this->folder, 0777, true) && !is_dir($this->folder)) { + throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $this->folder)); } } @@ -125,6 +125,8 @@ class FileProfilerStorage implements ProfilerStorageInterface /** * {@inheritdoc} + * + * @throws \RuntimeException */ public function write(Profile $profile) { @@ -134,8 +136,8 @@ class FileProfilerStorage implements ProfilerStorageInterface if (!$profileIndexed) { // Create directory $dir = dirname($file); - if (!is_dir($dir)) { - mkdir($dir, 0777, true); + if (!is_dir($dir) && false === @mkdir($dir, 0777, true) && !is_dir($dir)) { + throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $dir)); } } From 481bf6603df62c7a41c7e3abe6f0bc33a4ed8dbd Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 9 Nov 2015 11:28:05 +0100 Subject: [PATCH 11/12] [ci] Add version tag in phpunit wrapper to trigger cache-reset on demand --- phpunit | 12 ++++++++++++ src/Symfony/Component/HttpKernel/phpunit.xml.dist | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/phpunit b/phpunit index 79810f626e..2c1cbbd933 100755 --- a/phpunit +++ b/phpunit @@ -1,6 +1,18 @@ #!/usr/bin/env php + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// Please update when phpunit needs to be reinstalled with fresh deps: +// Cache-Id-Version: 2015-11-09 12:13 UTC + use Symfony\Component\Process\ProcessUtils; error_reporting(-1); diff --git a/src/Symfony/Component/HttpKernel/phpunit.xml.dist b/src/Symfony/Component/HttpKernel/phpunit.xml.dist index 7901a0b8b5..5b17270141 100644 --- a/src/Symfony/Component/HttpKernel/phpunit.xml.dist +++ b/src/Symfony/Component/HttpKernel/phpunit.xml.dist @@ -24,4 +24,14 @@ + + + + + + Symfony\Component\HttpFoundation + + + + From f93e0c23d1d9e34cc959ebed94366cc8f3056d64 Mon Sep 17 00:00:00 2001 From: Emil Andersson Date: Mon, 9 Nov 2015 10:45:21 +0100 Subject: [PATCH 12/12] [ci] Phpunit tests wont run if composer is installed in a wrapper --- phpunit | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/phpunit b/phpunit index 2c1cbbd933..6830e3a77a 100755 --- a/phpunit +++ b/phpunit @@ -22,19 +22,11 @@ require __DIR__.'/src/Symfony/Component/Process/ProcessUtils.php'; $PHPUNIT_VERSION = PHP_VERSION_ID >= 70000 ? '5.0' : '4.8'; $PHPUNIT_DIR = __DIR__.'/.phpunit'; $PHP = defined('PHP_BINARY') ? PHP_BINARY : 'php'; - -if (!file_exists($COMPOSER = __DIR__.'/composer.phar')) { - $COMPOSER = rtrim('\\' === DIRECTORY_SEPARATOR ? `where.exe composer.phar` : (`which composer.phar` ?: `which composer`)); - if (!file_exists($COMPOSER)) { - stream_copy_to_stream( - fopen('https://getcomposer.org/composer.phar', 'rb'), - fopen($COMPOSER = __DIR__.'/composer.phar', 'wb') - ); - } -} - $PHP = ProcessUtils::escapeArgument($PHP); -$COMPOSER = $PHP.' '.ProcessUtils::escapeArgument($COMPOSER); + +$COMPOSER = file_exists($COMPOSER = __DIR__.'/composer.phar') || ($COMPOSER = rtrim('\\' === DIRECTORY_SEPARATOR ? `where.exe composer.phar` : `which composer.phar`)) + ? $PHP.' '.ProcessUtils::escapeArgument($COMPOSER) + : 'composer'; if (!file_exists("$PHPUNIT_DIR/phpunit-$PHPUNIT_VERSION/phpunit") || md5_file(__FILE__) !== @file_get_contents("$PHPUNIT_DIR/.md5")) { // Build a standalone phpunit without symfony/yaml