diff --git a/src/Symfony/Bridge/Twig/Command/DebugCommand.php b/src/Symfony/Bridge/Twig/Command/DebugCommand.php index 5720096c46..0d77fed339 100644 --- a/src/Symfony/Bridge/Twig/Command/DebugCommand.php +++ b/src/Symfony/Bridge/Twig/Command/DebugCommand.php @@ -108,19 +108,27 @@ EOF } $rows = array(); + $firstNamespace = true; + $prevHasSeparator = false; foreach ($this->getLoaderPaths() as $namespace => $paths) { - if (count($paths) > 1) { + if (!$firstNamespace && !$prevHasSeparator && count($paths) > 1) { $rows[] = array('', ''); } + $firstNamespace = false; foreach ($paths as $path) { - $rows[] = array($namespace, '- '.$path); + $rows[] = array($namespace, $path.DIRECTORY_SEPARATOR); $namespace = ''; } if (count($paths) > 1) { $rows[] = array('', ''); + $prevHasSeparator = true; + } else { + $prevHasSeparator = false; } } - array_pop($rows); + if ($prevHasSeparator) { + array_pop($rows); + } $io->section('Loader Paths'); $io->table(array('Namespace', 'Paths'), $rows); diff --git a/src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php b/src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php new file mode 100644 index 0000000000..77d522f523 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Command; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Twig\Command\DebugCommand; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Tester\CommandTester; +use Twig\Loader\FilesystemLoader; +use Twig\Environment; + +class DebugCommandTest extends TestCase +{ + public function testDebugCommand() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(array(), array('decorated' => false)); + + $this->assertEquals(0, $ret, 'Returns 0 in case of success'); + $this->assertContains('Functions', trim($tester->getDisplay())); + } + + public function testLineSeparatorInLoaderPaths() + { + // these paths aren't realistic, + // they're configured to force the line separator + $tester = $this->createCommandTester(array( + 'Acme' => array('extractor', 'extractor'), + '!Acme' => array('extractor', 'extractor'), + FilesystemLoader::MAIN_NAMESPACE => array('extractor', 'extractor'), + )); + $ret = $tester->execute(array(), array('decorated' => false)); + $ds = DIRECTORY_SEPARATOR; + $loaderPaths = <<assertEquals(0, $ret, 'Returns 0 in case of success'); + $this->assertContains($loaderPaths, trim($tester->getDisplay(true))); + } + + private function createCommandTester(array $paths = array()) + { + $filesystemLoader = new FilesystemLoader(array(), dirname(__DIR__).'/Fixtures'); + foreach ($paths as $namespace => $relDirs) { + foreach ($relDirs as $relDir) { + $filesystemLoader->addPath($relDir, $namespace); + } + } + $command = new DebugCommand(new Environment($filesystemLoader)); + + $application = new Application(); + $application->add($command); + $command = $application->find('debug:twig'); + + return new CommandTester($command); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php index 84227cf47e..0b36054fd0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php @@ -70,7 +70,7 @@ Suppose that you have the following security configuration in your application: security: encoders: Symfony\Component\Security\Core\User\User: plaintext - AppBundle\Entity\User: bcrypt + App\Entity\User: bcrypt If you execute the command non-interactively, the first available configured @@ -82,16 +82,16 @@ generated to encode the password: Pass the full user class path as the second argument to encode passwords for your own entities: - php %command.full_name% --no-interaction [password] AppBundle\Entity\User + php %command.full_name% --no-interaction [password] App\Entity\User Executing the command interactively allows you to generate a random salt for encoding the password: - php %command.full_name% [password] AppBundle\Entity\User + php %command.full_name% [password] App\Entity\User In case your encoder doesn't require a salt, add the empty-salt option: - php %command.full_name% --empty-salt [password] AppBundle\Entity\User + php %command.full_name% --empty-salt [password] App\Entity\User EOF ) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index a2feb22c8e..b0d7353834 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -363,8 +363,8 @@ class MainConfiguration implements ConfigurationInterface ->children() ->arrayNode('encoders') ->example(array( - 'AppBundle\Entity\User1' => 'bcrypt', - 'AppBundle\Entity\User2' => array( + 'App\Entity\User1' => 'bcrypt', + 'App\Entity\User2' => array( 'algorithm' => 'bcrypt', 'cost' => 13, ), diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php index b64d9af361..62c3a834f6 100644 --- a/src/Symfony/Component/Console/Command/Command.php +++ b/src/Symfony/Component/Console/Command/Command.php @@ -220,7 +220,7 @@ class Command if (function_exists('cli_set_process_title')) { if (!@cli_set_process_title($this->processTitle)) { if ('Darwin' === PHP_OS) { - $output->writeln('Running "cli_get_process_title" as an unprivileged user is not supported on MacOS.'); + $output->writeln('Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.', OutputInterface::VERBOSITY_VERY_VERBOSE); } else { cli_set_process_title($this->processTitle); } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index d790aff7d6..660f1f4def 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -1662,8 +1662,10 @@ EOF; return '$this'; } - if ($this->container->hasDefinition($id) && ($definition = $this->container->getDefinition($id)) && !$definition->isSynthetic()) { - if (null !== $reference && ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $reference->getInvalidBehavior()) { + if ($this->container->hasDefinition($id) && $definition = $this->container->getDefinition($id)) { + if ($definition->isSynthetic()) { + $code = sprintf('$this->get(\'%s\'%s)', $id, null !== $reference ? ', '.$reference->getInvalidBehavior() : ''); + } elseif (null !== $reference && ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $reference->getInvalidBehavior()) { $code = 'null'; if (!$definition->isShared()) { return $code; diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index a840256990..7fde6ec969 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -1399,6 +1399,21 @@ class ContainerBuilderTest extends TestCase $this->assertSame('via-bindings', $container->get('foo')->class2->identifier); } + public function testUninitializedSyntheticReference() + { + $container = new ContainerBuilder(); + $container->register('foo', 'stdClass')->setPublic(true)->setSynthetic(true); + $container->register('bar', 'stdClass')->setPublic(true)->setShared(false) + ->setProperty('foo', new Reference('foo', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE)); + + $container->compile(); + + $this->assertEquals((object) array('foo' => null), $container->get('bar')); + + $container->set('foo', (object) array(123)); + $this->assertEquals((object) array('foo' => (object) array(123)), $container->get('bar')); + } + public function testIdCanBeAnObjectAsLongAsItCanBeCastToString() { $id = new Reference('another_service'); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 52e7ff18fa..29cbf1f6a4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -953,6 +953,29 @@ class PhpDumperTest extends TestCase $this->assertInstanceOf('stdClass', $container->get('bar')); } + public function testUninitializedSyntheticReference() + { + $container = new ContainerBuilder(); + $container->register('foo', 'stdClass')->setPublic(true)->setSynthetic(true); + $container->register('bar', 'stdClass')->setPublic(true)->setShared(false) + ->setProperty('foo', new Reference('foo', ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE)); + + $container->compile(); + + $dumper = new PhpDumper($container); + eval('?>'.$dumper->dump(array( + 'class' => 'Symfony_DI_PhpDumper_Test_UninitializedSyntheticReference', + 'inline_class_loader_parameter' => 'inline_requires', + ))); + + $container = new \Symfony_DI_PhpDumper_Test_UninitializedSyntheticReference(); + + $this->assertEquals((object) array('foo' => null), $container->get('bar')); + + $container->set('foo', (object) array(123)); + $this->assertEquals((object) array('foo' => (object) array(123)), $container->get('bar')); + } + /** * This test checks the trigger of a deprecation note and should not be removed in major releases. * diff --git a/src/Symfony/Component/HttpFoundation/Response.php b/src/Symfony/Component/HttpFoundation/Response.php index 8e11e91970..b7433173c7 100644 --- a/src/Symfony/Component/HttpFoundation/Response.php +++ b/src/Symfony/Component/HttpFoundation/Response.php @@ -329,12 +329,17 @@ class Response } // headers - foreach ($this->headers->allPreserveCase() as $name => $values) { + foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) { foreach ($values as $value) { header($name.': '.$value, false, $this->statusCode); } } + // cookies + foreach ($this->headers->getCookies() as $cookie) { + header('Set-Cookie: '.$cookie->getName().strstr($cookie, '='), false, $this->statusCode); + } + // status header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode); diff --git a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_urlencode.expected b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_urlencode.expected index 4e9c4c075f..14e44a398a 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_urlencode.expected +++ b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_urlencode.expected @@ -4,7 +4,7 @@ Array [0] => Content-Type: text/plain; charset=utf-8 [1] => Cache-Control: no-cache, private [2] => Date: Sat, 12 Nov 1955 20:04:00 GMT - [3] => Set-Cookie: %3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D=%3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D; path=/ + [3] => Set-Cookie: ?*():@&+$/%#[]=%3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D; path=/ [4] => Set-Cookie: ?*():@&+$/%#[]=%3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D; path=/ ) shutdown diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index 68b4154b10..5f291f7cbf 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -433,7 +433,7 @@ class OptionsResolver implements Options )); } - $this->allowedValues[$option] = is_array($allowedValues) ? $allowedValues : array($allowedValues); + $this->allowedValues[$option] = \is_array($allowedValues) ? $allowedValues : array($allowedValues); // Make sure the option is processed unset($this->resolved[$option]); @@ -785,14 +785,13 @@ class OptionsResolver implements Options } if (!$valid) { - throw new InvalidOptionsException(sprintf( - 'The option "%s" with value %s is expected to be of type '. - '"%s", but is of type "%s".', - $option, - $this->formatValue($value), - implode('" or "', $this->allowedTypes[$option]), - implode('|', array_keys($invalidTypes)) - )); + $keys = array_keys($invalidTypes); + + if (1 === \count($keys) && '[]' === substr($keys[0], -2)) { + throw new InvalidOptionsException(sprintf('The option "%s" with value %s is expected to be of type "%s", but one of the elements is of type "%s".', $option, $this->formatValue($value), implode('" or "', $this->allowedTypes[$option]), $keys[0])); + } + + throw new InvalidOptionsException(sprintf('The option "%s" with value %s is expected to be of type "%s", but is of type "%s".', $option, $this->formatValue($value), implode('" or "', $this->allowedTypes[$option]), implode('|', array_keys($invalidTypes)))); } } @@ -868,32 +867,10 @@ class OptionsResolver implements Options return $value; } - /** - * @param string $type - * @param mixed $value - * @param array &$invalidTypes - * - * @return bool - */ - private function verifyTypes($type, $value, array &$invalidTypes) + private function verifyTypes(string $type, $value, array &$invalidTypes): bool { - if ('[]' === substr($type, -2) && is_array($value)) { - $originalType = $type; - $type = substr($type, 0, -2); - $invalidValues = array_filter( // Filter out valid values, keeping invalid values in the resulting array - $value, - function ($value) use ($type) { - return !self::isValueValidType($type, $value); - } - ); - - if (!$invalidValues) { - return true; - } - - $invalidTypes[$this->formatTypeOf($value, $originalType)] = true; - - return false; + if (\is_array($value) && '[]' === substr($type, -2)) { + return $this->verifyArrayType($type, $value, $invalidTypes); } if (self::isValueValidType($type, $value)) { @@ -907,6 +884,43 @@ class OptionsResolver implements Options return false; } + private function verifyArrayType(string $type, array $value, array &$invalidTypes, int $level = 0): bool + { + $type = substr($type, 0, -2); + + $suffix = '[]'; + while (\strlen($suffix) <= $level * 2) { + $suffix .= '[]'; + } + + if ('[]' === substr($type, -2)) { + $success = true; + foreach ($value as $item) { + if (!\is_array($item)) { + $invalidTypes[$this->formatTypeOf($item, null).$suffix] = true; + + return false; + } + + if (!$this->verifyArrayType($type, $item, $invalidTypes, $level + 1)) { + $success = false; + } + } + + return $success; + } + + foreach ($value as $item) { + if (!self::isValueValidType($type, $item)) { + $invalidTypes[$this->formatTypeOf($item, $type).$suffix] = $value; + + return false; + } + } + + return true; + } + /** * Returns whether a resolved option with the given name exists. * @@ -987,13 +1001,13 @@ class OptionsResolver implements Options while ('[]' === substr($type, -2)) { $type = substr($type, 0, -2); $value = array_shift($value); - if (!is_array($value)) { + if (!\is_array($value)) { break; } $suffix .= '[]'; } - if (is_array($value)) { + if (\is_array($value)) { $subTypes = array(); foreach ($value as $val) { $subTypes[$this->formatTypeOf($val, null)] = true; @@ -1003,7 +1017,7 @@ class OptionsResolver implements Options } } - return (is_object($value) ? get_class($value) : gettype($value)).$suffix; + return (\is_object($value) ? get_class($value) : gettype($value)).$suffix; } /** @@ -1017,19 +1031,19 @@ class OptionsResolver implements Options */ private function formatValue($value): string { - if (is_object($value)) { + if (\is_object($value)) { return get_class($value); } - if (is_array($value)) { + if (\is_array($value)) { return 'array'; } - if (is_string($value)) { + if (\is_string($value)) { return '"'.$value.'"'; } - if (is_resource($value)) { + if (\is_resource($value)) { return 'resource'; } @@ -1065,8 +1079,21 @@ class OptionsResolver implements Options return implode(', ', $values); } - private static function isValueValidType($type, $value) + private static function isValueValidType(string $type, $value): bool { return (function_exists($isFunction = 'is_'.$type) && $isFunction($value)) || $value instanceof $type; } + + private function getInvalidValues(array $arrayValues, string $type): array + { + $invalidValues = array(); + + foreach ($arrayValues as $key => $value) { + if (!self::isValueValidType($type, $value)) { + $invalidValues[$key] = $value; + } + } + + return $invalidValues; + } } diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index 12dc77b3c6..ac12984447 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -483,7 +483,7 @@ class OptionsResolverTest extends TestCase /** * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - * @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[]", but is of type "DateTime[]". + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[]", but one of the elements is of type "DateTime[]". */ public function testResolveFailsIfInvalidTypedArray() { @@ -507,7 +507,7 @@ class OptionsResolverTest extends TestCase /** * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - * @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[]", but is of type "integer|stdClass|array|DateTime[]". + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[]", but one of the elements is of type "stdClass[]". */ public function testResolveFailsIfTypedArrayContainsInvalidTypes() { @@ -524,7 +524,7 @@ class OptionsResolverTest extends TestCase /** * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - * @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[][]", but is of type "double[][]". + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[][]", but one of the elements is of type "double[][]". */ public function testResolveFailsWithCorrectLevelsButWrongScalar() { @@ -1586,4 +1586,151 @@ class OptionsResolverTest extends TestCase count($this->resolver); } + + public function testNestedArrays() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'int[][]'); + + $this->assertEquals(array( + 'foo' => array( + array( + 1, 2, + ), + ), + ), $this->resolver->resolve( + array( + 'foo' => array( + array(1, 2), + ), + ) + )); + } + + public function testNested2Arrays() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'int[][][][]'); + + $this->assertEquals(array( + 'foo' => array( + array( + array( + array( + 1, 2, + ), + ), + ), + ), + ), $this->resolver->resolve( + array( + 'foo' => array( + array( + array( + array(1, 2), + ), + ), + ), + ) + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "float[][][][]", but one of the elements is of type "integer[][][][]". + */ + public function testNestedArraysException() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'float[][][][]'); + + $this->resolver->resolve( + array( + 'foo' => array( + array( + array( + array(1, 2), + ), + ), + ), + ) + ); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[][]", but one of the elements is of type "boolean[][]". + */ + public function testNestedArrayException1() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'int[][]'); + $this->resolver->resolve(array( + 'foo' => array( + array(1, true, 'str', array(2, 3)), + ), + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "int[][]", but one of the elements is of type "boolean[][]". + */ + public function testNestedArrayException2() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'int[][]'); + $this->resolver->resolve(array( + 'foo' => array( + array(true, 'str', array(2, 3)), + ), + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "string[][][]", but one of the elements is of type "string[][]". + */ + public function testNestedArrayException3() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'string[][][]'); + $this->resolver->resolve(array( + 'foo' => array( + array('str', array(1, 2)), + ), + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "string[][][]", but one of the elements is of type "integer[][][]". + */ + public function testNestedArrayException4() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'string[][][]'); + $this->resolver->resolve(array( + 'foo' => array( + array( + array('str'), array(1, 2), ), + ), + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value array is expected to be of type "string[]", but one of the elements is of type "array[]". + */ + public function testNestedArrayException5() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedTypes('foo', 'string[]'); + $this->resolver->resolve(array( + 'foo' => array( + array( + array('str'), array(1, 2), ), + ), + )); + } } diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php index f11569f162..f6f3ba06d1 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php @@ -161,25 +161,21 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property $ucFirstProperty = ucfirst($property); - try { - switch (true) { - case $docBlock = $this->getDocBlockFromProperty($class, $property): - $data = array($docBlock, self::PROPERTY, null); - break; + switch (true) { + case $docBlock = $this->getDocBlockFromProperty($class, $property): + $data = array($docBlock, self::PROPERTY, null); + break; - case list($docBlock) = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR): - $data = array($docBlock, self::ACCESSOR, null); - break; + case list($docBlock) = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR): + $data = array($docBlock, self::ACCESSOR, null); + break; - case list($docBlock, $prefix) = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR): - $data = array($docBlock, self::MUTATOR, $prefix); - break; + case list($docBlock, $prefix) = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR): + $data = array($docBlock, self::MUTATOR, $prefix); + break; - default: - $data = array(null, null, null); - } - } catch (\InvalidArgumentException $e) { - $data = array(null, null, null); + default: + $data = array(null, null, null); } return $this->docBlocks[$propertyHash] = $data; @@ -194,7 +190,11 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property return null; } - return $this->docBlockFactory->create($reflectionProperty, $this->contextFactory->createFromReflector($reflectionProperty->getDeclaringClass())); + try { + return $this->docBlockFactory->create($reflectionProperty, $this->contextFactory->createFromReflector($reflectionProperty->getDeclaringClass())); + } catch (\InvalidArgumentException $e) { + return null; + } } private function getDocBlockFromMethod(string $class, string $ucFirstProperty, int $type): ?array @@ -226,6 +226,10 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property return null; } - return array($this->docBlockFactory->create($reflectionMethod, $this->contextFactory->createFromReflector($reflectionMethod)), $prefix); + try { + return array($this->docBlockFactory->create($reflectionMethod, $this->contextFactory->createFromReflector($reflectionMethod)), $prefix); + } catch (\InvalidArgumentException $e) { + return null; + } } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index 0b769c4531..080df2893a 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -184,6 +184,29 @@ class PhpDocExtractorTest extends TestCase { $this->assertNull($this->extractor->getShortDescription(EmptyDocBlock::class, 'foo')); } + + public function dockBlockFallbackTypesProvider() + { + return array( + 'pub' => array( + 'pub', array(new Type(Type::BUILTIN_TYPE_STRING)), + ), + 'protAcc' => array( + 'protAcc', array(new Type(Type::BUILTIN_TYPE_INT)), + ), + 'protMut' => array( + 'protMut', array(new Type(Type::BUILTIN_TYPE_BOOL)), + ), + ); + } + + /** + * @dataProvider dockBlockFallbackTypesProvider + */ + public function testDocBlockFallback($property, $types) + { + $this->assertEquals($types, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\DockBlockFallback', $property)); + } } class EmptyDocBlock diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DockBlockFallback.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DockBlockFallback.php new file mode 100644 index 0000000000..3f9c303b59 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DockBlockFallback.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests\Fixtures; + +/** + * PhpDocExtractor should fallback from property -> accessor -> mutator when looking up dockblocks. + * + * @author Martin Rademacher + */ +class DockBlockFallback +{ + /** @var string $pub */ + public $pub = 'pub'; + + protected $protAcc; + protected $protMut; + + public function getPub() + { + return $this->pub; + } + + public function setPub($pub) + { + $this->pub = $pub; + } + + /** + * @return int + */ + public function getProtAcc() + { + return $this->protAcc; + } + + public function setProt($protAcc) + { + $this->protAcc = $protAcc; + } + + public function getProtMut() + { + return $this->protMut; + } + + /** + * @param bool $protMut + */ + public function setProtMut($protMut) + { + $this->protMut = $protMut; + } +}