diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/PropertyInfoPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/PropertyInfoPass.php new file mode 100644 index 0000000000..819827ac8c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/PropertyInfoPass.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Adds extractors to the property_info service. + * + * @author Kévin Dunglas + */ +class PropertyInfoPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('property_info')) { + return; + } + + $listExtractors = $this->findAndSortTaggedServices('property_info.list_extractor', $container); + $container->getDefinition('property_info')->replaceArgument(0, $listExtractors); + + $typeExtractors = $this->findAndSortTaggedServices('property_info.type_extractor', $container); + $container->getDefinition('property_info')->replaceArgument(1, $typeExtractors); + + $descriptionExtractors = $this->findAndSortTaggedServices('property_info.description_extractor', $container); + $container->getDefinition('property_info')->replaceArgument(2, $descriptionExtractors); + + $accessExtractors = $this->findAndSortTaggedServices('property_info.access_extractor', $container); + $container->getDefinition('property_info')->replaceArgument(3, $accessExtractors); + } + + /** + * Finds all services with the given tag name and order them by their priority. + * + * @param string $tagName + * @param ContainerBuilder $container + * + * @return array + */ + private function findAndSortTaggedServices($tagName, ContainerBuilder $container) + { + $services = $container->findTaggedServiceIds($tagName); + + $sortedServices = array(); + foreach ($services as $serviceId => $tags) { + foreach ($tags as $attributes) { + $priority = isset($attributes['priority']) ? $attributes['priority'] : 0; + $sortedServices[$priority][] = new Reference($serviceId); + } + } + + if (empty($sortedServices)) { + return array(); + } + + krsort($sortedServices); + + // Flatten the array + return call_user_func_array('array_merge', $sortedServices); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SerializerPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SerializerPass.php index aa449dd4c8..6188feab55 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SerializerPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SerializerPass.php @@ -38,6 +38,16 @@ class SerializerPass implements CompilerPassInterface $container->getDefinition('serializer')->replaceArgument(1, $encoders); } + /** + * Finds all services with the given tag name and order them by their priority. + * + * @param string $tagName + * @param ContainerBuilder $container + * + * @return array + * + * @throws \RuntimeException + */ private function findAndSortTaggedServices($tagName, ContainerBuilder $container) { $services = $container->findTaggedServiceIds($tagName); @@ -48,8 +58,8 @@ class SerializerPass implements CompilerPassInterface $sortedServices = array(); foreach ($services as $serviceId => $tags) { - foreach ($tags as $tag) { - $priority = isset($tag['priority']) ? $tag['priority'] : 0; + foreach ($tags as $attributes) { + $priority = isset($attributes['priority']) ? $attributes['priority'] : 0; $sortedServices[$priority][] = new Reference($serviceId); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index aa8ecfcfc0..309cc6c48c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -178,6 +178,7 @@ class Configuration implements ConfigurationInterface $this->addAnnotationsSection($rootNode); $this->addSerializerSection($rootNode); $this->addPropertyAccessSection($rootNode); + $this->addPropertyInfoSection($rootNode); return $treeBuilder; } @@ -723,4 +724,16 @@ class Configuration implements ConfigurationInterface ->end() ; } + + private function addPropertyInfoSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('property_info') + ->info('Property info configuration') + ->canBeEnabled() + ->end() + ->end() + ; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index ce16580f2a..6d9c5f8921 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -131,6 +131,10 @@ class FrameworkExtension extends Extension $this->registerSerializerConfiguration($config['serializer'], $container, $loader); } + if (isset($config['property_info'])) { + $this->registerPropertyInfoConfiguration($config['property_info'], $container, $loader); + } + $loader->load('debug_prod.xml'); $definition = $container->findDefinition('debug.debug_handlers_listener'); @@ -986,6 +990,28 @@ class FrameworkExtension extends Extension } } + /** + * Loads property info configuration. + * + * @param array $config + * @param ContainerBuilder $container + * @param XmlFileLoader $loader + */ + private function registerPropertyInfoConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + { + if (!$config['enabled']) { + return; + } + + $loader->load('property_info.xml'); + + if (class_exists('phpDocumentor\Reflection\ClassReflector')) { + $definition = $container->register('property_info.php_doc_extractor', 'Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor'); + $definition->addTag('property_info.description_extractor', array('priority' => -1000)); + $definition->addTag('property_info.type_extractor', array('priority' => -1001)); + } + } + /** * Returns the base path for the XSD files. * diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 3a96f71e11..7d26d00f58 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -15,6 +15,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConstraintVal use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddValidatorInitializersPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConsoleCommandPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\FormPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\PropertyInfoPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TemplatingPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RoutingResolverPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass; @@ -90,6 +91,7 @@ class FrameworkBundle extends Bundle $container->addCompilerPass(new TranslationDumperPass()); $container->addCompilerPass(new FragmentRendererPass(), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new SerializerPass()); + $container->addCompilerPass(new PropertyInfoPass()); if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new UnusedTagsPass(), PassConfig::TYPE_AFTER_REMOVING); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml new file mode 100644 index 0000000000..d4ce4f5d24 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 4190dad7cb..4e382f677f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -33,6 +33,7 @@ + @@ -220,4 +221,8 @@ + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/PropertyInfoPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/PropertyInfoPassTest.php new file mode 100644 index 0000000000..3d41fb12b0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/PropertyInfoPassTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; + +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\PropertyInfoPass; +use Symfony\Component\DependencyInjection\Reference; + +class PropertyInfoPassTest extends \PHPUnit_Framework_TestCase +{ + public function testServicesAreOrderedAccordingToPriority() + { + $services = array( + 'n3' => array('tag' => array()), + 'n1' => array('tag' => array('priority' => 200)), + 'n2' => array('tag' => array('priority' => 100)), + ); + + $expected = array( + new Reference('n1'), + new Reference('n2'), + new Reference('n3'), + ); + + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder', array('findTaggedServiceIds')); + + $container->expects($this->any()) + ->method('findTaggedServiceIds') + ->will($this->returnValue($services)); + + $propertyInfoPass = new PropertyInfoPass(); + + $method = new \ReflectionMethod( + 'Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\PropertyInfoPass', + 'findAndSortTaggedServices' + ); + $method->setAccessible(true); + + $actual = $method->invoke($propertyInfoPass, 'tag', $container); + + $this->assertEquals($expected, $actual); + } + + public function testReturningEmptyArrayWhenNoService() + { + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder', array('findTaggedServiceIds')); + + $container->expects($this->any()) + ->method('findTaggedServiceIds') + ->will($this->returnValue(array())); + + $propertyInfoPass = new PropertyInfoPass(); + + $method = new \ReflectionMethod( + 'Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\PropertyInfoPass', + 'findAndSortTaggedServices' + ); + $method->setAccessible(true); + + $actual = $method->invoke($propertyInfoPass, 'tag', $container); + + $this->assertEquals(array(), $actual); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/SerializerPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/SerializerPassTest.php index a098c4fd62..27f1636e8c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/SerializerPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/SerializerPassTest.php @@ -87,7 +87,7 @@ class SerializerPassTest extends \PHPUnit_Framework_TestCase $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder', array('findTaggedServiceIds')); - $container->expects($this->atLeastOnce()) + $container->expects($this->any()) ->method('findTaggedServiceIds') ->will($this->returnValue($services)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 04b6c95b46..48e9d9a0c5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -179,6 +179,9 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase 'magic_call' => false, 'throw_exception_on_invalid_index' => false, ), + 'property_info' => array( + 'enabled' => false, + ), 'assets' => array( 'version' => null, 'version_format' => '%%s?%%s', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_info.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_info.php new file mode 100644 index 0000000000..847e15fa19 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_info.php @@ -0,0 +1,7 @@ +loadFromExtension('framework', array( + 'property_info' => array( + 'enabled' => true, + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_info.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_info.xml new file mode 100644 index 0000000000..825bc46e74 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_info.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_info.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_info.yml new file mode 100644 index 0000000000..fbdf7a7b0d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_info.yml @@ -0,0 +1,3 @@ +framework: + property_info: + enabled: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index a722322993..575668d652 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -488,6 +488,18 @@ abstract class FrameworkExtensionTest extends TestCase $this->assertFalse($container->hasDefinition('serializer')); } + public function testPropertyInfoDisabled() + { + $container = $this->createContainerFromFile('default_config'); + $this->assertFalse($container->has('property_info')); + } + + public function testPropertyInfoEnabled() + { + $container = $this->createContainerFromFile('property_info'); + $this->assertTrue($container->has('property_info')); + } + protected function createContainer(array $data = array()) { return new ContainerBuilder(new ParameterBag(array_merge(array( diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 3f529db8c1..f392453ff5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -47,7 +47,9 @@ "symfony/expression-language": "~2.6|~3.0.0", "symfony/process": "~2.0,>=2.0.5|~3.0.0", "symfony/validator": "~2.5|~3.0.0", - "symfony/yaml": "~2.0,>=2.0.5|~3.0.0" + "symfony/yaml": "~2.0,>=2.0.5|~3.0.0", + "symfony/property-info": "~2.8|~3.0.0", + "phpdocumentor/reflection": "^1.0.7" }, "suggest": { "symfony/console": "For using the console commands", @@ -55,7 +57,8 @@ "symfony/form": "For using forms", "symfony/serializer": "For using the serializer service", "symfony/validator": "For using validation", - "symfony/yaml": "For using the debug:config and lint:yaml commands" + "symfony/yaml": "For using the debug:config and lint:yaml commands", + "symfony/property-info": "For using the property_info_extractor service" }, "autoload": { "psr-4": { "Symfony\\Bundle\\FrameworkBundle\\": "" }