diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index a8ac7051086dd..1b4d03a1b8471 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -628,6 +628,8 @@ private function addPropertyAccessSection(ArrayNodeDefinition $rootNode) ->addDefaultsIfNotSet() ->info('Property access configuration') ->children() + ->scalarNode('cache')->end() + ->booleanNode('enable_annotations')->defaultFalse()->end() ->booleanNode('magic_call')->defaultFalse()->end() ->booleanNode('throw_exception_on_invalid_index')->defaultFalse()->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 8cd1fea64a0fa..2723374f9f13b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -20,11 +20,14 @@ use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\PropertyAccess\Mapping\Loader\XmlFileLoader as PropertyAccessXmlFileLoader; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Config\Resource\DirectoryResource; use Symfony\Component\Finder\Finder; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Config\FileLocator; +use Symfony\Component\PropertyAccess\Mapping\Loader\AnnotationLoader; +use Symfony\Component\PropertyAccess\Mapping\Loader\YamlFileLoader; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; @@ -138,7 +141,7 @@ public function load(array $configs, ContainerBuilder $container) } $this->registerAnnotationsConfiguration($config['annotations'], $container, $loader); - $this->registerPropertyAccessConfiguration($config['property_access'], $container); + $this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader); if ($this->isConfigEnabled($container, $config['serializer'])) { $this->registerSerializerConfiguration($config['serializer'], $container, $loader); @@ -915,13 +918,75 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde } } - private function registerPropertyAccessConfiguration(array $config, ContainerBuilder $container) + private function registerPropertyAccessConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) { + $loader->load('property_access.xml'); + $container ->getDefinition('property_accessor') ->replaceArgument(0, $config['magic_call']) ->replaceArgument(1, $config['throw_exception_on_invalid_index']) ; + + $chainLoader = $container->getDefinition('property_access.mapping.chain_loader'); + + $serializerLoaders = array(); + if (isset($config['enable_annotations']) && $config['enable_annotations']) { + $annotationLoader = new Definition( + AnnotationLoader::class, + array(new Reference('annotation_reader')) + ); + $annotationLoader->setPublic(false); + + $serializerLoaders[] = $annotationLoader; + } + + $bundles = $container->getParameter('kernel.bundles'); + foreach ($bundles as $bundle) { + $reflection = new \ReflectionClass($bundle); + $dirname = dirname($reflection->getFileName()); + + if (is_file($file = $dirname.'/Resources/config/property_access.xml')) { + $definition = new Definition(PropertyAccessXmlFileLoader::class, array(realpath($file))); + $definition->setPublic(false); + + $serializerLoaders[] = $definition; + $container->addResource(new FileResource($file)); + } + + if (is_file($file = $dirname.'/Resources/config/property_access.yml')) { + $definition = new Definition(YamlFileLoader::class, array(realpath($file))); + $definition->setPublic(false); + + $serializerLoaders[] = $definition; + $container->addResource(new FileResource($file)); + } + + if (is_dir($dir = $dirname.'/Resources/config/property_access')) { + foreach (Finder::create()->files()->in($dir)->name('*.xml') as $file) { + $definition = new Definition(PropertyAccessXmlFileLoader::class, array($file->getRealpath())); + $definition->setPublic(false); + + $serializerLoaders[] = $definition; + } + foreach (Finder::create()->files()->in($dir)->name('*.yml') as $file) { + $definition = new Definition(YamlFileLoader::class, array($file->getRealpath())); + $definition->setPublic(false); + + $serializerLoaders[] = $definition; + } + + $container->addResource(new DirectoryResource($dir)); + } + } + + $chainLoader->replaceArgument(0, $serializerLoaders); + + if (isset($config['cache']) && $config['cache']) { + $container->getDefinition('property_access.mapping.class_metadata_factory')->replaceArgument( + 1, new Reference($config['cache']) + ); + } } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml index d9e381c4806b8..43765b37af72d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml @@ -5,10 +5,22 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + + + + + + + + null + + + 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 f78174961ccd9..daa99ca7efa3d 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 @@ -192,7 +192,9 @@ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 94b6e315b8c20..11216d10c35ed 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -225,6 +225,7 @@ protected static function getBundleDefaultConfig() 'property_access' => array( 'magic_call' => false, 'throw_exception_on_invalid_index' => false, + 'enable_annotations' => false, ), 'property_info' => array( 'enabled' => false, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php index 6849f8fbd42eb..b18a95aba1a90 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -63,6 +63,11 @@ 'debug' => true, 'file_cache_dir' => '%kernel.cache_dir%/annotations', ), + 'property_access' => array( + 'magic_call' => false, + 'throw_exception_on_invalid_index' => false, + 'enable_annotations' => true, + ), 'serializer' => array( 'enabled' => true, 'enable_annotations' => true, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php index 4340e61fc0961..d553eff165476 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php @@ -4,5 +4,6 @@ 'property_access' => array( 'magic_call' => true, 'throw_exception_on_invalid_index' => true, + 'enable_annotations' => false, ), )); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml index e8e34d6e9c0de..bf97152ac3464 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml @@ -41,5 +41,6 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml index d345174e8b134..10c8f407df436 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -53,6 +53,10 @@ framework: enabled: true enable_annotations: true name_converter: serializer.name_converter.camel_case_to_snake_case + property_access: + enable_annotations: true + magic_call: false + throw_exception_on_invalid_index: false ide: file%%link%%format request: formats: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml index b5fd2718ab112..b4f69d7febeb1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml @@ -2,3 +2,4 @@ framework: property_access: magic_call: true throw_exception_on_invalid_index: true + enable_annotations: false diff --git a/src/Symfony/Component/PropertyAccess/Annotation/Property.php b/src/Symfony/Component/PropertyAccess/Annotation/Property.php new file mode 100644 index 0000000000000..a59031cfa1ed5 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Annotation/Property.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Annotation; + +/** + * Property accessor configuration annotation. + * + * @Annotation + * @Target({"PROPERTY"}) + * + * @author Luis Ramón López + */ +class Property +{ + /** + * Custom setter method for the property. + * + * @var string + */ + public $setter; + + /** + * Custom getter method for the property. + * + * @var string + */ + public $getter; + + /** + * Custom adder method for the property. + * + * @var string + */ + public $adder; + + /** + * Custom remover method for the property. + * + * @var string + */ + public $remover; +} diff --git a/src/Symfony/Component/PropertyAccess/Annotation/PropertyAdder.php b/src/Symfony/Component/PropertyAccess/Annotation/PropertyAdder.php new file mode 100644 index 0000000000000..71c638e4c40e6 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Annotation/PropertyAdder.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Annotation; + +/** + * Property accessor adder configuration annotation. + * + * @Annotation + * @Target({"METHOD"}) + * + * @author Luis Ramón López + */ +class PropertyAdder +{ + /** + * Associates this method to the adder of this property. + * + * @var string + */ + public $property; +} diff --git a/src/Symfony/Component/PropertyAccess/Annotation/PropertyGetter.php b/src/Symfony/Component/PropertyAccess/Annotation/PropertyGetter.php new file mode 100644 index 0000000000000..1e3d921633001 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Annotation/PropertyGetter.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Annotation; + +/** + * Property accessor getter configuration annotation. + * + * @Annotation + * @Target({"METHOD"}) + * + * @author Luis Ramón López + */ +class PropertyGetter +{ + /** + * Associates this method to the getter of this property. + * + * @var string + */ + public $property; +} diff --git a/src/Symfony/Component/PropertyAccess/Annotation/PropertyRemover.php b/src/Symfony/Component/PropertyAccess/Annotation/PropertyRemover.php new file mode 100644 index 0000000000000..81a99f835aaed --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Annotation/PropertyRemover.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Annotation; + +/** + * Property accessor remover configuration annotation. + * + * @Annotation + * @Target({"METHOD"}) + * + * @author Luis Ramón López + */ +class PropertyRemover +{ + /** + * Associates this method to the remover of this property. + * + * @var string + */ + public $property; +} diff --git a/src/Symfony/Component/PropertyAccess/Annotation/PropertySetter.php b/src/Symfony/Component/PropertyAccess/Annotation/PropertySetter.php new file mode 100644 index 0000000000000..120ad587bc614 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Annotation/PropertySetter.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Annotation; + +/** + * Property accessor setter configuration annotation. + * + * @Annotation + * @Target({"METHOD"}) + * + * @author Luis Ramón López + */ +class PropertySetter +{ + /** + * Associates this method to the setter of this property. + * + * @var string + */ + public $property; +} diff --git a/src/Symfony/Component/PropertyAccess/CHANGELOG.md b/src/Symfony/Component/PropertyAccess/CHANGELOG.md index 574106e521075..b109322ea70be 100644 --- a/src/Symfony/Component/PropertyAccess/CHANGELOG.md +++ b/src/Symfony/Component/PropertyAccess/CHANGELOG.md @@ -1,6 +1,10 @@ CHANGELOG ========= +3.2.0 +------ + * added custom method calling for properties. + 2.7.0 ------ diff --git a/src/Symfony/Component/PropertyAccess/Exception/MappingException.php b/src/Symfony/Component/PropertyAccess/Exception/MappingException.php new file mode 100644 index 0000000000000..d63d5a8364144 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Exception/MappingException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * MappingException. + * + * @author Luis Ramón López + */ +class MappingException extends RuntimeException +{ +} diff --git a/src/Symfony/Component/PropertyAccess/Exception/NoSuchMetadataException.php b/src/Symfony/Component/PropertyAccess/Exception/NoSuchMetadataException.php new file mode 100644 index 0000000000000..9ac9a835ba6bb --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Exception/NoSuchMetadataException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * @author Luis Ramón López + */ +class NoSuchMetadataException extends AccessException +{ +} diff --git a/src/Symfony/Component/PropertyAccess/Mapping/ClassMetadata.php b/src/Symfony/Component/PropertyAccess/Mapping/ClassMetadata.php new file mode 100644 index 0000000000000..b3675f21f4b45 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Mapping/ClassMetadata.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Mapping; + +/** + * {@inheritdoc} + * + * @author Luis Ramón López + */ +class ClassMetadata +{ + /** + * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getName()} instead. + */ + public $name; + + /** + * @var PropertyMetadata[] + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getPropertyMetadataCollection()} instead. + */ + public $propertyMetadataCollection = array(); + + /** + * @var \ReflectionClass + */ + private $reflClass; + + /** + * Constructs a metadata for the given class. + * + * @param string $class + */ + public function __construct($class) + { + $this->name = $class; + } + + /** + * Returns the name of the backing PHP class. + * + * @return string The name of the backing class. + */ + public function getName() + { + return $this->name; + } + + /** + * Adds an {@link AttributeMetadataInterface}. + * + * @param PropertyMetadata $propertyMetadata + */ + public function addPropertyMetadata(PropertyMetadata $propertyMetadata) + { + $this->propertyMetadataCollection[$propertyMetadata->getName()] = $propertyMetadata; + } + + /** + * Gets the list of {@link PropertyMetadata}. + * + * @return PropertyMetadata[] + */ + public function getPropertyMetadataCollection() + { + return $this->propertyMetadataCollection; + } + + /** + * Merges a {@link ClassMetadata} into the current one. + * + * @param ClassMetadata $classMetadata + */ + public function merge(ClassMetadata $classMetadata) + { + foreach ($classMetadata->getPropertyMetadataCollection() as $attributeMetadata) { + if (isset($this->propertyMetadataCollection[$attributeMetadata->getName()])) { + $this->propertyMetadataCollection[$attributeMetadata->getName()]->merge($attributeMetadata); + } else { + $this->addPropertyMetadata($attributeMetadata); + } + } + } + + /** + * Returns a {@link \ReflectionClass} instance for this class. + * + * @return \ReflectionClass + */ + public function getReflectionClass() + { + if (!$this->reflClass) { + $this->reflClass = new \ReflectionClass($this->getName()); + } + + return $this->reflClass; + } + + /** + * Returns the names of the properties that should be serialized. + * + * @return string[] + */ + public function __sleep() + { + return array( + 'name', + 'propertyMetadataCollection', + ); + } +} diff --git a/src/Symfony/Component/PropertyAccess/Mapping/Factory/BlackHoleMetadataFactory.php b/src/Symfony/Component/PropertyAccess/Mapping/Factory/BlackHoleMetadataFactory.php new file mode 100644 index 0000000000000..ea31be334f66e --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Mapping/Factory/BlackHoleMetadataFactory.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Mapping\Factory; + +/** + * Metadata factory that does not store metadata. + * + * This implementation is useful if you want to validate values against + * constraints only and you don't need to add constraints to classes and + * properties. + * + * @author Luis Ramón López + */ +class BlackHoleMetadataFactory implements MetadataFactoryInterface +{ + /** + * {@inheritdoc} + */ + public function getMetadataFor($value) + { + throw new \LogicException('This class does not support metadata.'); + } + + /** + * {@inheritdoc} + */ + public function hasMetadataFor($value) + { + return false; + } +} diff --git a/src/Symfony/Component/PropertyAccess/Mapping/Factory/LazyLoadingMetadataFactory.php b/src/Symfony/Component/PropertyAccess/Mapping/Factory/LazyLoadingMetadataFactory.php new file mode 100644 index 0000000000000..57f4f8f93ccf3 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Mapping/Factory/LazyLoadingMetadataFactory.php @@ -0,0 +1,166 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Mapping\Factory; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\PropertyAccess\Exception\NoSuchMetadataException; +use Symfony\Component\PropertyAccess\Mapping\ClassMetadata; +use Symfony\Component\PropertyAccess\Mapping\Loader\LoaderInterface; + +/** + * Creates new {@link ClassMetadataInterface} instances. + * + * Whenever {@link getMetadataFor()} is called for the first time with a given + * class name or object of that class, a new metadata instance is created and + * returned. On subsequent requests for the same class, the same metadata + * instance will be returned. + * + * You can optionally pass a {@link LoaderInterface} instance to the constructor. + * Whenever a new metadata instance is created, it is passed to the loader, + * which can configure the metadata based on configuration loaded from the + * filesystem or a database. If you want to use multiple loaders, wrap them in a + * {@link LoaderChain}. + * + * You can also optionally pass a {@link CacheInterface} instance to the + * constructor. This cache will be used for persisting the generated metadata + * between multiple PHP requests. + * + * @author Luis Ramón López + */ +class LazyLoadingMetadataFactory implements MetadataFactoryInterface +{ + /** + * The loader for loading the class metadata. + * + * @var LoaderInterface|null + */ + private $loader; + + /** + * The cache for caching class metadata. + * + * @var CacheItemPoolInterface|null + */ + private $cache; + + /** + * The loaded metadata, indexed by class name. + * + * @var ClassMetadata[] + */ + private $loadedClasses = array(); + + /** + * Creates a new metadata factory. + * + * @param LoaderInterface|null $loader The loader for configuring new metadata + * @param CacheItemPoolInterface|null $cache The PSR-6 cache for persisting metadata + * between multiple PHP requests + */ + public function __construct(LoaderInterface $loader = null, CacheItemPoolInterface $cache = null) + { + $this->loader = $loader; + $this->cache = $cache; + } + + /** + * {@inheritdoc} + * + * If the method was called with the same class name (or an object of that + * class) before, the same metadata instance is returned. + * + * If the factory was configured with a cache, this method will first look + * for an existing metadata instance in the cache. If an existing instance + * is found, it will be returned without further ado. + * + * Otherwise, a new metadata instance is created. If the factory was + * configured with a loader, the metadata is passed to the + * {@link LoaderInterface::loadClassMetadata()} method for further + * configuration. At last, the new object is returned. + */ + public function getMetadataFor($value) + { + if (!is_object($value) && !is_string($value)) { + throw new NoSuchMetadataException(sprintf('Cannot create metadata for non-objects. Got: %s', gettype($value))); + } + + $class = ltrim(is_object($value) ? get_class($value) : $value, '\\'); + + if (isset($this->loadedClasses[$class])) { + return $this->loadedClasses[$class]; + } + + if (null !== $this->cache) { + $item = $this->cache->getItem($this->escapeClassName($class)); + if ($item->isHit()) { + return $this->loadedClasses[$class] = $item->get(); + } + } + + if (!class_exists($class) && !interface_exists($class)) { + throw new NoSuchMetadataException(sprintf('The class or interface "%s" does not exist.', $class)); + } + + $metadata = new ClassMetadata($class); + + // Include metadata from the parent class + if ($parent = $metadata->getReflectionClass()->getParentClass()) { + $metadata->merge($this->getMetadataFor($parent->name)); + } + + // Include metadata from all implemented interfaces + foreach ($metadata->getReflectionClass()->getInterfaces() as $interface) { + $metadata->merge($this->getMetadataFor($interface->name)); + } + + if (null !== $this->loader) { + $this->loader->loadClassMetadata($metadata); + } + + if (null !== $this->cache) { + $item->set($metadata); + $this->cache->save($item); + } + + return $this->loadedClasses[$class] = $metadata; + } + + /** + * {@inheritdoc} + */ + public function hasMetadataFor($value) + { + if (!is_object($value) && !is_string($value)) { + return false; + } + + $class = ltrim(is_object($value) ? get_class($value) : $value, '\\'); + + if (class_exists($class) || interface_exists($class)) { + return true; + } + + return false; + } + + /** + * Replaces backslashes by dots in a class name. + * + * @param string $class + * + * @return string + */ + private function escapeClassName($class) + { + return str_replace('\\', '.', $class); + } +} diff --git a/src/Symfony/Component/PropertyAccess/Mapping/Factory/MetadataFactoryInterface.php b/src/Symfony/Component/PropertyAccess/Mapping/Factory/MetadataFactoryInterface.php new file mode 100644 index 0000000000000..a62d9c1faa691 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Mapping/Factory/MetadataFactoryInterface.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Mapping\Factory; + +use Symfony\Component\PropertyAccess\Mapping\PropertyMetadata; +use Symfony\Component\PropertyAccess\Exception; + +/** + * Returns {@link \Symfony\Component\PropertyAccess\Mapping\MetadataInterface} instances for values. + * + * @author Luis Ramón López + */ +interface MetadataFactoryInterface +{ + /** + * Returns the metadata for the given value. + * + * @param mixed $value Some value + * + * @return PropertyMetadata The metadata for the value + * + * @throws Exception\NoSuchPropertyException If no metadata exists for the given value + */ + public function getMetadataFor($value); + + /** + * Returns whether the class is able to return metadata for the given value. + * + * @param mixed $value Some value + * + * @return bool Whether metadata can be returned for that value + */ + public function hasMetadataFor($value); +} diff --git a/src/Symfony/Component/PropertyAccess/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/PropertyAccess/Mapping/Loader/AnnotationLoader.php new file mode 100644 index 0000000000000..7d855508187b6 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Mapping/Loader/AnnotationLoader.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Mapping\Loader; + +use Doctrine\Common\Annotations\Reader; +use Symfony\Component\PropertyAccess\Annotation\PropertyAdder; +use Symfony\Component\PropertyAccess\Annotation\PropertyGetter; +use Symfony\Component\PropertyAccess\Annotation\Property; +use Symfony\Component\PropertyAccess\Annotation\PropertyRemover; +use Symfony\Component\PropertyAccess\Annotation\PropertySetter; +use Symfony\Component\PropertyAccess\Mapping\PropertyMetadata; +use Symfony\Component\PropertyAccess\Mapping\ClassMetadata; + +/** + * Annotation loader. + * + * @author Kévin Dunglas + * @author Luis Ramón López + */ +class AnnotationLoader implements LoaderInterface +{ + private $reader; + + public function __construct(Reader $reader) + { + $this->reader = $reader; + } + + /** + * {@inheritdoc} + */ + public function loadClassMetadata(ClassMetadata $classMetadata) + { + $reflectionClass = $classMetadata->getReflectionClass(); + $className = $reflectionClass->name; + $loaded = false; + + $propertiesMetadata = $classMetadata->getPropertyMetadataCollection(); + + foreach ($reflectionClass->getProperties() as $property) { + if (!isset($propertiesMetadata[$property->name])) { + $propertiesMetadata[$property->name] = new PropertyMetadata($property->name); + $classMetadata->addPropertyMetadata($propertiesMetadata[$property->name]); + } + + if ($property->getDeclaringClass()->name === $className) { + foreach ($this->reader->getPropertyAnnotations($property) as $annotation) { + if ($annotation instanceof Property) { + $propertiesMetadata[$property->name]->setGetter($annotation->getter); + $propertiesMetadata[$property->name]->setSetter($annotation->setter); + $propertiesMetadata[$property->name]->setAdder($annotation->adder); + $propertiesMetadata[$property->name]->setRemover($annotation->remover); + } + + $loaded = true; + } + } + } + + foreach ($reflectionClass->getMethods() as $method) { + if ($method->getDeclaringClass()->name === $className) { + foreach ($this->reader->getMethodAnnotations($method) as $annotation) { + if ($annotation instanceof PropertyGetter) { + if (!isset($propertiesMetadata[$annotation->property])) { + $propertiesMetadata[$annotation->property] = new PropertyMetadata($annotation->property); + $classMetadata->addPropertyMetadata($propertiesMetadata[$annotation->property]); + } + $propertiesMetadata[$annotation->property]->setGetter($method->getName()); + } + if ($annotation instanceof PropertySetter) { + if (!isset($propertiesMetadata[$annotation->property])) { + $propertiesMetadata[$annotation->property] = new PropertyMetadata($annotation->property); + $classMetadata->addPropertyMetadata($propertiesMetadata[$annotation->property]); + } + $propertiesMetadata[$annotation->property]->setSetter($method->getName()); + } + if ($annotation instanceof PropertyAdder) { + if (!isset($propertiesMetadata[$annotation->property])) { + $propertiesMetadata[$annotation->property] = new PropertyMetadata($annotation->property); + $classMetadata->addPropertyMetadata($propertiesMetadata[$annotation->property]); + } + $propertiesMetadata[$annotation->property]->setAdder($method->getName()); + } + if ($annotation instanceof PropertyRemover) { + if (!isset($propertiesMetadata[$annotation->property])) { + $propertiesMetadata[$annotation->property] = new PropertyMetadata($annotation->property); + $classMetadata->addPropertyMetadata($propertiesMetadata[$annotation->property]); + } + $propertiesMetadata[$annotation->property]->setRemover($method->getName()); + } + + $loaded = true; + } + } + } + + return $loaded; + } +} diff --git a/src/Symfony/Component/PropertyAccess/Mapping/Loader/FileLoader.php b/src/Symfony/Component/PropertyAccess/Mapping/Loader/FileLoader.php new file mode 100644 index 0000000000000..d3dbef4e918a8 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Mapping/Loader/FileLoader.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Mapping\Loader; + +use Symfony\Component\PropertyAccess\Exception\MappingException; + +/** + * Base class for all file based loaders. + * + * @author Kévin Dunglas + */ +abstract class FileLoader implements LoaderInterface +{ + /** + * @var string + */ + protected $file; + + /** + * Constructor. + * + * @param string $file The mapping file to load + * + * @throws MappingException if the mapping file does not exist or is not readable + */ + public function __construct($file) + { + if (!is_file($file)) { + throw new MappingException(sprintf('The mapping file %s does not exist', $file)); + } + + if (!is_readable($file)) { + throw new MappingException(sprintf('The mapping file %s is not readable', $file)); + } + + $this->file = $file; + } +} diff --git a/src/Symfony/Component/PropertyAccess/Mapping/Loader/LoaderChain.php b/src/Symfony/Component/PropertyAccess/Mapping/Loader/LoaderChain.php new file mode 100644 index 0000000000000..3759661b5fa0b --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Mapping/Loader/LoaderChain.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Mapping\Loader; + +use Symfony\Component\PropertyAccess\Exception\MappingException; +use Symfony\Component\PropertyAccess\Mapping\ClassMetadata; + +/** + * Calls multiple {@link LoaderInterface} instances in a chain. + * + * This class accepts multiple instances of LoaderInterface to be passed to the + * constructor. When {@link loadClassMetadata()} is called, the same method is called + * in all of these loaders, regardless of whether any of them was + * successful or not. + * + * @author Bernhard Schussek + * @author Kévin Dunglas + */ +class LoaderChain implements LoaderInterface +{ + private $loaders; + + /** + * Accepts a list of LoaderInterface instances. + * + * @param LoaderInterface[] $loaders An array of LoaderInterface instances + * + * @throws MappingException If any of the loaders does not implement LoaderInterface + */ + public function __construct(array $loaders) + { + foreach ($loaders as $loader) { + if (!$loader instanceof LoaderInterface) { + throw new MappingException(sprintf('Class %s is expected to implement LoaderInterface', get_class($loader))); + } + } + + $this->loaders = $loaders; + } + + /** + * {@inheritdoc} + */ + public function loadClassMetadata(ClassMetadata $metadata) + { + $success = false; + + foreach ($this->loaders as $loader) { + $success = $loader->loadClassMetadata($metadata) || $success; + } + + return $success; + } +} diff --git a/src/Symfony/Component/PropertyAccess/Mapping/Loader/LoaderInterface.php b/src/Symfony/Component/PropertyAccess/Mapping/Loader/LoaderInterface.php new file mode 100644 index 0000000000000..e137f9c66028e --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Mapping/Loader/LoaderInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Mapping\Loader; + +use Symfony\Component\PropertyAccess\Mapping\ClassMetadata; + +/** + * Loads {@link ClassMetadataInterface}. + * + * @author Luis Ramón López + */ +interface LoaderInterface +{ + /** + * Load class metadata. + * + * @param ClassMetadata $classMetadata A metadata + * + * @return bool + */ + public function loadClassMetadata(ClassMetadata $classMetadata); +} diff --git a/src/Symfony/Component/PropertyAccess/Mapping/Loader/XmlFileLoader.php b/src/Symfony/Component/PropertyAccess/Mapping/Loader/XmlFileLoader.php new file mode 100644 index 0000000000000..8b69d2dec5d0c --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Mapping/Loader/XmlFileLoader.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Mapping\Loader; + +use Symfony\Component\Config\Util\XmlUtils; +use Symfony\Component\PropertyAccess\Exception\MappingException; +use Symfony\Component\PropertyAccess\Mapping\PropertyMetadata; +use Symfony\Component\PropertyAccess\Mapping\ClassMetadata; + +/** + * Loads XML mapping files. + * + * @author Kévin Dunglas + */ +class XmlFileLoader extends FileLoader +{ + /** + * An array of {@class \SimpleXMLElement} instances. + * + * @var \SimpleXMLElement[]|null + */ + private $classes; + + /** + * {@inheritdoc} + */ + public function loadClassMetadata(ClassMetadata $classMetadata) + { + if (null === $this->classes) { + $this->classes = array(); + $xml = $this->parseFile($this->file); + + foreach ($xml->class as $class) { + $this->classes[(string) $class['name']] = $class; + } + } + + $attributesMetadata = $classMetadata->getPropertyMetadataCollection(); + + if (isset($this->classes[$classMetadata->getName()])) { + $xml = $this->classes[$classMetadata->getName()]; + + foreach ($xml->property as $attribute) { + $attributeName = (string) $attribute['name']; + + if (isset($attributesMetadata[$attributeName])) { + $attributeMetadata = $attributesMetadata[$attributeName]; + } else { + $attributeMetadata = new PropertyMetadata($attributeName); + $classMetadata->addPropertyMetadata($attributeMetadata); + } + + if (isset($attribute['getter'])) { + $attributeMetadata->setGetter($attribute['getter']); + } + + if (isset($attribute['setter'])) { + $attributeMetadata->setSetter($attribute['setter']); + } + + if (isset($attribute['adder'])) { + $attributeMetadata->setAdder($attribute['adder']); + } + + if (isset($attribute['remover'])) { + $attributeMetadata->setRemover($attribute['remover']); + } + } + + return true; + } + + return false; + } + + /** + * Parses a XML File. + * + * @param string $file Path of file + * + * @return \SimpleXMLElement + * + * @throws MappingException + */ + private function parseFile($file) + { + try { + $dom = XmlUtils::loadFile($file, __DIR__.'/schema/dic/property-access-mapping/property-access-mapping-1.0.xsd'); + } catch (\Exception $e) { + throw new MappingException($e->getMessage(), $e->getCode(), $e); + } + + return simplexml_import_dom($dom); + } +} diff --git a/src/Symfony/Component/PropertyAccess/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/PropertyAccess/Mapping/Loader/YamlFileLoader.php new file mode 100644 index 0000000000000..19499a405c087 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Mapping/Loader/YamlFileLoader.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Mapping\Loader; + +use Symfony\Component\PropertyAccess\Exception\MappingException; +use Symfony\Component\PropertyAccess\Mapping\PropertyMetadata; +use Symfony\Component\PropertyAccess\Mapping\ClassMetadata; +use Symfony\Component\Yaml\Parser; + +/** + * YAML File Loader. + * + * @author Kévin Dunglas + * @author Luis Ramón López + */ +class YamlFileLoader extends FileLoader +{ + private $yamlParser; + + /** + * An array of YAML class descriptions. + * + * @var array + */ + private $classes = null; + + /** + * {@inheritdoc} + */ + public function loadClassMetadata(ClassMetadata $classMetadata) + { + if (null === $this->classes) { + if (!stream_is_local($this->file)) { + throw new MappingException(sprintf('This is not a local file "%s".', $this->file)); + } + + if (null === $this->yamlParser) { + $this->yamlParser = new Parser(); + } + + $classes = $this->yamlParser->parse(file_get_contents($this->file)); + + if (empty($classes)) { + return false; + } + + // not an array + if (!is_array($classes)) { + throw new MappingException(sprintf('The file "%s" must contain a YAML array.', $this->file)); + } + + $this->classes = $classes; + } + + if (isset($this->classes[$classMetadata->getName()])) { + $yaml = $this->classes[$classMetadata->getName()]; + + if (isset($yaml['properties']) && is_array($yaml['properties'])) { + $attributesMetadata = $classMetadata->getPropertyMetadataCollection(); + + foreach ($yaml['properties'] as $attribute => $data) { + if (isset($attributesMetadata[$attribute])) { + $attributeMetadata = $attributesMetadata[$attribute]; + } else { + $attributeMetadata = new PropertyMetadata($attribute); + $classMetadata->addPropertyMetadata($attributeMetadata); + } + + if (isset($data['getter'])) { + if (!is_string($data['getter'])) { + throw new MappingException('The "getter" value must be a string in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName()); + } + + $attributeMetadata->setGetter($data['getter']); + } + + if (isset($data['setter'])) { + if (!is_string($data['setter'])) { + throw new MappingException('The "setter" value must be a string in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName()); + } + + $attributeMetadata->setSetter($data['setter']); + } + + if (isset($data['adder'])) { + if (!is_string($data['adder'])) { + throw new MappingException('The "adder" value must be a string in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName()); + } + + $attributeMetadata->setAdder($data['adder']); + } + + if (isset($data['remover'])) { + if (!is_string($data['remover'])) { + throw new MappingException('The "remover" value must be a string in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName()); + } + + $attributeMetadata->setRemover($data['remover']); + } + } + } + + return true; + } + + return false; + } +} diff --git a/src/Symfony/Component/PropertyAccess/Mapping/Loader/schema/dic/property-access-mapping/property-access-mapping-1.0.xsd b/src/Symfony/Component/PropertyAccess/Mapping/Loader/schema/dic/property-access-mapping/property-access-mapping-1.0.xsd new file mode 100644 index 0000000000000..027c3d27d0496 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Mapping/Loader/schema/dic/property-access-mapping/property-access-mapping-1.0.xsd @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Component/PropertyAccess/Mapping/PropertyMetadata.php b/src/Symfony/Component/PropertyAccess/Mapping/PropertyMetadata.php new file mode 100644 index 0000000000000..ed613beadb0c4 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Mapping/PropertyMetadata.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Mapping; + +/** + * Stores metadata needed for overriding properties access methods. + * + * @author Luis Ramón López + */ +class PropertyMetadata +{ + /** + * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getName()} instead. + */ + public $name; + + /** + * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getGetter()} instead. + */ + public $getter; + + /** + * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getSetter()} instead. + */ + public $setter; + + /** + * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getAdder()} instead. + */ + public $adder; + + /** + * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getRemover()} instead. + */ + public $remover; + + /** + * Constructs a metadata for the given attribute. + * + * @param string $name + */ + public function __construct($name = null) + { + $this->name = $name; + } + + /** + * Gets the attribute name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Gets the setter method name. + * + * @return string + */ + public function getSetter() + { + return $this->setter; + } + + /** + * Sets the setter method name. + */ + public function setSetter($setter) + { + $this->setter = $setter; + } + /** + * Gets the getter method name. + * + * @return string + */ + public function getGetter() + { + return $this->getter; + } + + /** + * Sets the getter method name. + */ + public function setGetter($getter) + { + $this->getter = $getter; + } + + /** + * Gets the adder method name. + * + * @return string + */ + public function getAdder() + { + return $this->adder; + } + + /** + * Sets the adder method name. + */ + public function setAdder($adder) + { + $this->adder = $adder; + } + + /** + * Gets the remover method name. + * + * @return string + */ + public function getRemover() + { + return $this->remover; + } + + /** + * Sets the remover method name. + */ + public function setRemover($remover) + { + $this->remover = $remover; + } + + /** + * Merges another PropertyMetadata with the current one. + * + * @param PropertyMetadata $propertyMetadata + */ + public function merge(PropertyMetadata $propertyMetadata) + { + // Overwrite only if not defined + if (null === $this->getter) { + $this->getter = $propertyMetadata->getGetter(); + } + if (null === $this->setter) { + $this->setter = $propertyMetadata->getSetter(); + } + if (null === $this->adder) { + $this->adder = $propertyMetadata->getAdder(); + } + if (null === $this->remover) { + $this->remover = $propertyMetadata->getRemover(); + } + } + + /** + * Returns the names of the properties that should be serialized. + * + * @return string[] + */ + public function __sleep() + { + return array('name', 'getter', 'setter', 'adder', 'remover'); + } +} diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index efd6f4653c252..a6ab9c54c7206 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -22,6 +22,8 @@ use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; +use Symfony\Component\PropertyAccess\Mapping\Factory\MetadataFactoryInterface; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; /** * Default implementation of {@link PropertyAccessorInterface}. @@ -29,6 +31,7 @@ * @author Bernhard Schussek * @author Kévin Dunglas * @author Nicolas Grekas + * @author Luis Ramón López */ class PropertyAccessor implements PropertyAccessorInterface { @@ -141,10 +144,16 @@ class PropertyAccessor implements PropertyAccessorInterface * @var array */ private $writePropertyCache = array(); + private static $previousErrorHandler = false; private static $errorHandler = array(__CLASS__, 'handleError'); private static $resultProto = array(self::VALUE => null); + /** + * @var ClassMetadataFactoryInterface + */ + private $classMetadataFactory; + /** * @var array */ @@ -154,15 +163,17 @@ class PropertyAccessor implements PropertyAccessorInterface * Should not be used by application code. Use * {@link PropertyAccess::createPropertyAccessor()} instead. * - * @param bool $magicCall - * @param bool $throwExceptionOnInvalidIndex - * @param CacheItemPoolInterface $cacheItemPool + * @param bool $magicCall + * @param bool $throwExceptionOnInvalidIndex + * @param CacheItemPoolInterface $cacheItemPool + * @param ClassMetadataFactoryInterface $classMetadataFactory */ - public function __construct($magicCall = false, $throwExceptionOnInvalidIndex = false, CacheItemPoolInterface $cacheItemPool = null) + public function __construct($magicCall = false, $throwExceptionOnInvalidIndex = false, CacheItemPoolInterface $cacheItemPool = null, MetadataFactoryInterface $classMetadataFactory = null) { $this->magicCall = $magicCall; $this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex; $this->cacheItemPool = $cacheItemPool instanceof NullAdapter ? null : $cacheItemPool; // Replace the NullAdapter by the null value + $this->classMetadataFactory = $classMetadataFactory; } /** @@ -544,17 +555,29 @@ private function getReadAccessInfo($class, $property) } } + /** @var $metadata */ + $metadata = null; $access = array(); $reflClass = new \ReflectionClass($class); - $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property); + $hasProperty = $reflClass->hasProperty($property); + $access[self::ACCESS_HAS_PROPERTY] = $hasProperty; + + if ($this->classMetadataFactory) { + $metadata = $this->classMetadataFactory->getMetadataFor($class)->getPropertyMetadataCollection(); + $metadata = isset($metadata[$property]) ? $metadata[$property] : null; + } + $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; - if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) { + if ($metadata && $metadata->getGetter()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $metadata->getGetter(); + } elseif ($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()) { @@ -721,67 +744,93 @@ private function getWriteAccessInfo($class, $property, $value) } } + $metadata = null; $access = array(); $reflClass = new \ReflectionClass($class); - $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property); - $camelized = $this->camelize($property); - $singulars = (array) Inflector::singularize($camelized); + $hasProperty = $reflClass->hasProperty($property); + $access[self::ACCESS_HAS_PROPERTY] = $hasProperty; + + $traversable = is_array($value) || $value instanceof \Traversable; + $done = false; + + if ($this->classMetadataFactory) { + $metadata = $this->classMetadataFactory->getMetadataFor($class)->getPropertyMetadataCollection(); + $metadata = isset($metadata[$property]) ? $metadata[$property] : null; + + if ($metadata) { + if ($traversable && $metadata->getAdder() && $metadata->getRemover()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER; + $access[self::ACCESS_ADDER] = $metadata->getAdder(); + $access[self::ACCESS_REMOVER] = $metadata->getRemover(); + $done = true; + } elseif ($metadata->getSetter()) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; + $access[self::ACCESS_NAME] = $metadata->getSetter(); + $done = true; + } + } + } - if (is_array($value) || $value instanceof \Traversable) { - $methods = $this->findAdderAndRemover($reflClass, $singulars); + if (!$done) { + $camelized = $this->camelize($property); + $singulars = (array)Inflector::singularize($camelized); - if (null !== $methods) { - $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER; - $access[self::ACCESS_ADDER] = $methods[0]; - $access[self::ACCESS_REMOVER] = $methods[1]; + if ($traversable) { + $methods = $this->findAdderAndRemover($reflClass, $singulars); + + if (null !== $methods) { + $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'.$camelized; - $getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item) - - 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 ($access[self::ACCESS_HAS_PROPERTY] && $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; - } elseif (null !== $methods = $this->findAdderAndRemover($reflClass, $singulars)) { - $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; - $access[self::ACCESS_NAME] = sprintf( - 'The property "%s" in class "%s" can be defined with the methods "%s()" but '. - 'the new value must be an array or an instance of \Traversable, '. - '"%s" given.', - $property, - $reflClass->name, - implode('()", "', $methods), - is_object($value) ? get_class($value) : gettype($value) - ); - } 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 - ); + if (!isset($access[self::ACCESS_TYPE])) { + $setter = 'set' . $camelized; + $getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item) + + 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 ($access[self::ACCESS_HAS_PROPERTY] && $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; + } elseif (null !== $methods = $this->findAdderAndRemover($reflClass, $singulars)) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; + $access[self::ACCESS_NAME] = sprintf( + 'The property "%s" in class "%s" can be defined with the methods "%s()" but ' . + 'the new value must be an array or an instance of \Traversable, ' . + '"%s" given.', + $property, + $reflClass->name, + implode('()", "', $methods), + is_object($value) ? get_class($value) : gettype($value) + ); + } 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 + ); + } } } diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php index 3225cf9bc6b40..f34f34a947d6a 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php @@ -12,11 +12,13 @@ namespace Symfony\Component\PropertyAccess; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\PropertyAccess\Mapping\Factory\MetadataFactoryInterface; /** * A configurable builder to create a PropertyAccessor. * * @author Jérémie Augustin + * @author Luis Ramón López */ class PropertyAccessorBuilder { @@ -35,6 +37,11 @@ class PropertyAccessorBuilder */ private $cacheItemPool; + /** + * @var MetadataFactoryInterface + */ + private $metadataFactoryInterface = null; + /** * Enables the use of "__call" by the PropertyAccessor. * @@ -128,6 +135,29 @@ public function getCacheItemPool() return $this->cacheItemPool; } + /** + * Allows to take into account metadata in order to override getter/setter/adder and remover method + * calls to properties. + * + * @param MetadataFactoryInterface|null $metadataFactoryInterface + * + * @return PropertyAccessorBuilder The builder object + */ + public function setMetadataFactory(MetadataFactoryInterface $metadataFactoryInterface = null) + { + $this->metadataFactoryInterface = $metadataFactoryInterface; + + return $this; + } + + /** + * @return MetadataFactoryInterface|null the current object that retrieves metadata or null if not used + */ + public function getMetadataFactory() + { + return $this->metadataFactoryInterface; + } + /** * Builds and returns a new PropertyAccessor object. * @@ -135,6 +165,6 @@ public function getCacheItemPool() */ public function getPropertyAccessor() { - return new PropertyAccessor($this->magicCall, $this->throwExceptionOnInvalidIndex, $this->cacheItemPool); + return new PropertyAccessor($this->magicCall, $this->throwExceptionOnInvalidIndex, $this->cacheItemPool, $this->metadataFactoryInterface); } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/Dummy.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/Dummy.php new file mode 100644 index 0000000000000..dc1ce60818ba5 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/Dummy.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +use Symfony\Component\PropertyAccess\Annotation\Property; +use Symfony\Component\PropertyAccess\Annotation\PropertyGetter; + +/** + * Fixtures for testing metadata. + */ +class Dummy extends DummyParent +{ + /** + * @Property(getter="getter1", setter="setter1", adder="adder1", remover="remover1") + */ + protected $foo; + + /** + * @Property(getter="getter2") + */ + protected $bar; + + /** + * @return mixed + */ + public function getter1() + { + return $this->foo; + } + + /** + * @param mixed $foo + */ + public function setter1($foo) + { + $this->foo = $foo; + } + + /** + * @return mixed + */ + public function getter2() + { + return $this->bar; + } + + /** + * @param mixed $bar + */ + public function setBar($bar) + { + $this->bar = $bar; + } + + /** + * @PropertyGetter(property="test") + */ + public function testChild() + { + return 'child'; + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/DummyParent.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/DummyParent.php new file mode 100644 index 0000000000000..2475d14c96f82 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/DummyParent.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +use Symfony\Component\PropertyAccess\Annotation\PropertyGetter; + +/** + * Fixtures for testing metadata. + */ +class DummyParent +{ + /** + * @PropertyGetter(property="test") + */ + public function testParent() + { + return 'parent'; + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php index e63af3a8bac5d..2276e03c4ffb8 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php @@ -11,11 +11,14 @@ namespace Symfony\Component\PropertyAccess\Tests\Fixtures; +use Symfony\Component\PropertyAccess\Annotation\Property; +use Symfony\Component\PropertyAccess\Annotation\PropertyGetter; +use Symfony\Component\PropertyAccess\Annotation\PropertySetter; + class TestClass { public $publicProperty; protected $protectedProperty; - private $privateProperty; private $publicAccessor; private $publicMethodAccessor; @@ -28,7 +31,14 @@ class TestClass private $publicGetter; private $date; - public function __construct($value) + private $quantity; + + /** + * @Property(getter="customGetterTest", setter="customSetterTest") + */ + private $customGetterSetter; + + public function __construct($value, $quantity = 2, $pricePerUnit = 10) { $this->publicProperty = $value; $this->publicAccessor = $value; @@ -40,6 +50,9 @@ public function __construct($value) $this->publicIsAccessor = $value; $this->publicHasAccessor = $value; $this->publicGetter = $value; + $this->customGetterSetter = $value; + $this->quantity = $quantity; + $this->pricePerUnit = $pricePerUnit; } public function setPublicAccessor($value) @@ -184,4 +197,40 @@ public function getDate() { return $this->date; } + + public function customGetterTest() + { + return $this->customGetterSetter; + } + + public function customSetterTest($value) + { + $this->customGetterSetter = $value; + } + + /** + * @return int + */ + public function getQuantity() + { + return $this->quantity; + } + + /** + * @PropertyGetter(property="total") + */ + public function getTotal() + { + return $this->quantity * $this->pricePerUnit; + } + + /** + * @PropertySetter(property="total") + * + * @param mixed $total + */ + public function setTotal($total) + { + $this->quantity = $total / $this->pricePerUnit; + } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/empty-mapping.yml b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/empty-mapping.yml new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/invalid-mapping.yml b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/invalid-mapping.yml new file mode 100644 index 0000000000000..19102815663d2 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/invalid-mapping.yml @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/property-access.xml b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/property-access.xml new file mode 100644 index 0000000000000..990b2ad9dfbc5 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/property-access.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/property-access.yml b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/property-access.yml new file mode 100644 index 0000000000000..4c78d1bc4be62 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/property-access.yml @@ -0,0 +1,9 @@ +'Symfony\Component\PropertyAccess\Tests\Fixtures\Dummy': + properties: + foo: + getter: getter1 + setter: setter1 + adder: adder1 + remover: remover1 + bar: + getter: getter2 diff --git a/src/Symfony/Component/PropertyAccess/Tests/Mapping/ClassMetadataTest.php b/src/Symfony/Component/PropertyAccess/Tests/Mapping/ClassMetadataTest.php new file mode 100644 index 0000000000000..e54a392befa76 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Mapping/ClassMetadataTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Mapping; + +use Symfony\Component\PropertyAccess\Mapping\ClassMetadata; + +/** + * @author Kévin Dunglas + */ +class ClassMetadataTest extends \PHPUnit_Framework_TestCase +{ + public function testInterface() + { + $classMetadata = new ClassMetadata('name'); + $this->assertInstanceOf('Symfony\Component\PropertyAccess\Mapping\ClassMetadata', $classMetadata); + } + + public function testAttributeMetadata() + { + $classMetadata = new ClassMetadata('c'); + + $a1 = $this->getMock('Symfony\Component\PropertyAccess\Mapping\PropertyMetadata'); + $a1->method('getName')->willReturn('a1'); + + $a2 = $this->getMock('Symfony\Component\PropertyAccess\Mapping\PropertyMetadata'); + $a2->method('getName')->willReturn('a2'); + + $classMetadata->addPropertyMetadata($a1); + $classMetadata->addPropertyMetadata($a2); + + $this->assertEquals(array('a1' => $a1, 'a2' => $a2), $classMetadata->getPropertyMetadataCollection()); + } + + public function testSerialize() + { + $classMetadata = new ClassMetadata('a'); + + $a1 = $this->getMock('Symfony\Component\PropertyAccess\Mapping\PropertyMetadata'); + $a1->method('getName')->willReturn('b1'); + $a1->method('__sleep')->willReturn([]); + + $a2 = $this->getMock('Symfony\Component\PropertyAccess\Mapping\PropertyMetadata'); + $a2->method('getName')->willReturn('b2'); + $a2->method('__sleep')->willReturn([]); + + $classMetadata->addPropertyMetadata($a1); + $classMetadata->addPropertyMetadata($a2); + + $serialized = serialize($classMetadata); + $this->assertEquals($classMetadata, unserialize($serialized)); + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Mapping/Factory/BlackHoleMetadataFactoryTest.php b/src/Symfony/Component/PropertyAccess/Tests/Mapping/Factory/BlackHoleMetadataFactoryTest.php new file mode 100644 index 0000000000000..d49b37eae8496 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Mapping/Factory/BlackHoleMetadataFactoryTest.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Mapping\Factory; + +use Symfony\Component\PropertyAccess\Mapping\Factory\BlackHoleMetadataFactory; + +class BlackHoleMetadataFactoryTest extends \PHPUnit_Framework_TestCase +{ + /** + * @expectedException \LogicException + */ + public function testGetMetadataForThrowsALogicException() + { + $metadataFactory = new BlackHoleMetadataFactory(); + $metadataFactory->getMetadataFor('foo'); + } + + public function testHasMetadataForReturnsFalse() + { + $metadataFactory = new BlackHoleMetadataFactory(); + + $this->assertFalse($metadataFactory->hasMetadataFor('foo')); + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php b/src/Symfony/Component/PropertyAccess/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php new file mode 100644 index 0000000000000..7549b652c0bad --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Mapping\Factory; + +use Symfony\Component\PropertyAccess\Mapping\ClassMetadata; +use Symfony\Component\PropertyAccess\Mapping\Factory\LazyLoadingMetadataFactory; +use Symfony\Component\PropertyAccess\Mapping\Loader\LoaderInterface; +use Symfony\Component\PropertyAccess\Mapping\PropertyMetadata; + +class LazyLoadingMetadataFactoryTest extends \PHPUnit_Framework_TestCase +{ + const CLASSNAME = 'Symfony\Component\PropertyAccess\Tests\Fixtures\Dummy'; + const PARENTCLASS = 'Symfony\Component\PropertyAccess\Tests\Fixtures\DummyParent'; + + public function testLoadClassMetadata() + { + $factory = new LazyLoadingMetadataFactory(new TestLoader()); + $metadata = $factory->getMetadataFor(self::PARENTCLASS); + + $properties = array( + self::PARENTCLASS => new PropertyMetadata(self::PARENTCLASS), + ); + + $this->assertEquals($properties, $metadata->getPropertyMetadataCollection()); + } + + public function testMergeParentMetadata() + { + $factory = new LazyLoadingMetadataFactory(new TestLoader()); + $metadata = $factory->getMetadataFor(self::CLASSNAME); + + $properties = array( + self::PARENTCLASS => new PropertyMetadata(self::PARENTCLASS), + self::CLASSNAME => new PropertyMetadata(self::CLASSNAME), + ); + + $this->assertEquals($properties, $metadata->getPropertyMetadataCollection()); + } + + public function testWriteMetadataToCache() + { + $cache = $this->getMock('Psr\Cache\CacheItemPoolInterface'); + $factory = new LazyLoadingMetadataFactory(new TestLoader(), $cache); + + $properties = array( + self::PARENTCLASS => new PropertyMetadata(self::PARENTCLASS), + ); + + $cacheItem = $this->getMock('Psr\Cache\CacheItemInterface'); + + $cache->expects($this->once()) + ->method('getItem') + ->with($this->equalTo($this->escapeClassName(self::PARENTCLASS))) + ->will($this->returnValue($cacheItem)); + + $cacheItem->expects($this->once()) + ->method('isHit') + ->will($this->returnValue(false)); + + $cacheItem->expects($this->once()) + ->method('set') + ->will($this->returnCallback(function ($metadata) use ($properties) { + $this->assertEquals($properties, $metadata->getPropertyMetadataCollection()); + })); + + $cache->expects($this->once()) + ->method('save') + ->with($this->equalTo($cacheItem)) + ->will($this->returnValue(true)); + + $metadata = $factory->getMetadataFor(self::PARENTCLASS); + + $this->assertEquals(self::PARENTCLASS, $metadata->getName()); + $this->assertEquals($properties, $metadata->getPropertyMetadataCollection()); + } + + public function testReadMetadataFromCache() + { + $loader = $this->getMock('Symfony\Component\PropertyAccess\Mapping\Loader\LoaderInterface'); + $cache = $this->getMock('Psr\Cache\CacheItemPoolInterface'); + $factory = new LazyLoadingMetadataFactory($loader, $cache); + + $metadata = new ClassMetadata(self::PARENTCLASS); + $metadata->addPropertyMetadata(new PropertyMetadata()); + + $loader->expects($this->never()) + ->method('loadClassMetadata'); + + $cacheItem = $this->getMock('Psr\Cache\CacheItemInterface'); + + $cache->expects($this->once()) + ->method('getItem') + ->with($this->equalTo($this->escapeClassName(self::PARENTCLASS))) + ->will($this->returnValue($cacheItem)); + + $cacheItem->expects($this->once()) + ->method('isHit') + ->will($this->returnValue(true)); + + $cacheItem->expects($this->once()) + ->method('get') + ->will($this->returnValue($metadata)); + + $cacheItem->expects($this->never()) + ->method('set'); + + $cache->expects($this->never()) + ->method('save'); + + $this->assertEquals($metadata, $factory->getMetadataFor(self::PARENTCLASS)); + } + + /** + * Replaces backslashes by dots in a class name. + * + * @param string $class + * + * @return string + */ + private function escapeClassName($class) + { + return str_replace('\\', '.', $class); + } +} + +class TestLoader implements LoaderInterface +{ + public function loadClassMetadata(ClassMetadata $metadata) + { + $metadata->addPropertyMetadata(new PropertyMetadata($metadata->getName())); + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Mapping/Loader/AnnotationLoaderTest.php b/src/Symfony/Component/PropertyAccess/Tests/Mapping/Loader/AnnotationLoaderTest.php new file mode 100644 index 0000000000000..0cfa2c1fbaf89 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Mapping/Loader/AnnotationLoaderTest.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Mapping\Loader; + +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\Annotations\AnnotationRegistry; +use Symfony\Component\PropertyAccess\Mapping\ClassMetadata; +use Symfony\Component\PropertyAccess\Mapping\Loader\AnnotationLoader; +use Symfony\Component\PropertyAccess\Tests\Mapping\TestClassMetadataFactory; + +/** + * @author Kévin Dunglas + * @author Luis Ramón López + */ +class AnnotationLoaderTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var AnnotationLoader + */ + private $loader; + + protected function setUp() + { + $this->loader = new AnnotationLoader(new AnnotationReader()); + } + + public function testInterface() + { + $this->assertInstanceOf('Symfony\Component\PropertyAccess\Mapping\Loader\LoaderInterface', $this->loader); + } + + public function testLoadClassMetadataReturnsTrueIfSuccessful() + { + $classMetadata = new ClassMetadata('Symfony\Component\PropertyAccess\Tests\Fixtures\Dummy'); + AnnotationRegistry::registerAutoloadNamespace('Symfony\Component\PropertyAccess\Annotation', __DIR__.'/../../../../../..'); + $this->assertTrue($this->loader->loadClassMetadata($classMetadata)); + } + + public function testLoadMetadata() + { + $classMetadata = new ClassMetadata('Symfony\Component\PropertyAccess\Tests\Fixtures\Dummy'); + AnnotationRegistry::registerAutoloadNamespace('Symfony\Component\PropertyAccess\Annotation', __DIR__.'/../../../../../..'); + $this->loader->loadClassMetadata($classMetadata); + + $this->assertEquals(TestClassMetadataFactory::createClassMetadata(), $classMetadata); + } + +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/PropertyAccess/Tests/Mapping/Loader/XmlFileLoaderTest.php new file mode 100644 index 0000000000000..4162dec218808 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Mapping\Loader; + +use Symfony\Component\PropertyAccess\Mapping\Loader\XmlFileLoader; +use Symfony\Component\PropertyAccess\Mapping\ClassMetadata; +use Symfony\Component\PropertyAccess\Tests\Mapping\TestClassMetadataFactory; + +/** + * @author Kévin Dunglas + * @author Luis Ramón López + */ +class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var XmlFileLoader + */ + private $loader; + /** + * @var ClassMetadata + */ + private $metadata; + + protected function setUp() + { + $this->loader = new XmlFileLoader(__DIR__.'/../../Fixtures/property-access.xml'); + $this->metadata = new ClassMetadata('Symfony\Component\PropertyAccess\Tests\Fixtures\Dummy'); + } + + public function testInterface() + { + $this->assertInstanceOf('Symfony\Component\PropertyAccess\Mapping\Loader\LoaderInterface', $this->loader); + } + + public function testLoadClassMetadataReturnsTrueIfSuccessful() + { + $this->assertTrue($this->loader->loadClassMetadata($this->metadata)); + } + + public function testLoadClassMetadata() + { + $this->loader->loadClassMetadata($this->metadata); + + $this->assertEquals(TestClassMetadataFactory::createXmlClassMetadata(), $this->metadata); + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/PropertyAccess/Tests/Mapping/Loader/YamlFileLoaderTest.php new file mode 100644 index 0000000000000..63e299b2de76d --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Mapping\Loader; + +use Symfony\Component\PropertyAccess\Mapping\Loader\YamlFileLoader; +use Symfony\Component\PropertyAccess\Mapping\ClassMetadata; +use Symfony\Component\PropertyAccess\Tests\Mapping\TestClassMetadataFactory; + +/** + * @author Kévin Dunglas + * @author Luis Ramón López + */ +class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var YamlFileLoader + */ + private $loader; + /** + * @var ClassMetadata + */ + private $metadata; + + protected function setUp() + { + $this->loader = new YamlFileLoader(__DIR__.'/../../Fixtures/property-access.yml'); + $this->metadata = new ClassMetadata('Symfony\Component\PropertyAccess\Tests\Fixtures\Dummy'); + } + + public function testInterface() + { + $this->assertInstanceOf('Symfony\Component\PropertyAccess\Mapping\Loader\LoaderInterface', $this->loader); + } + + public function testLoadClassMetadataReturnsTrueIfSuccessful() + { + $this->assertTrue($this->loader->loadClassMetadata($this->metadata)); + } + + public function testLoadClassMetadataReturnsFalseWhenEmpty() + { + $loader = new YamlFileLoader(__DIR__.'/../../Fixtures/empty-mapping.yml'); + $this->assertFalse($loader->loadClassMetadata($this->metadata)); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\MappingException + */ + public function testLoadClassMetadataReturnsThrowsInvalidMapping() + { + $loader = new YamlFileLoader(__DIR__.'/../../Fixtures/invalid-mapping.yml'); + $loader->loadClassMetadata($this->metadata); + } + + public function testLoadClassMetadata() + { + $this->loader->loadClassMetadata($this->metadata); + + $this->assertEquals(TestClassMetadataFactory::createXmlClassMetadata(), $this->metadata); + } + +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Mapping/PropertyMetadataTest.php b/src/Symfony/Component/PropertyAccess/Tests/Mapping/PropertyMetadataTest.php new file mode 100644 index 0000000000000..827d7c57169e1 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Mapping/PropertyMetadataTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Mapping; + +use Symfony\Component\PropertyAccess\Mapping\PropertyMetadata; + +/** + * @author Kévin Dunglas + */ +class PropertyMetadataTest extends \PHPUnit_Framework_TestCase +{ + public function testInterface() + { + $propertyMetadata = new PropertyMetadata('name'); + $this->assertInstanceOf('Symfony\Component\PropertyAccess\Mapping\PropertyMetadata', $propertyMetadata); + } + + public function testGetName() + { + $propertyMetadata = new PropertyMetadata('name'); + $this->assertEquals('name', $propertyMetadata->getName()); + } + + public function testGetter() + { + $propertyMetadata = new PropertyMetadata('name'); + $propertyMetadata->setGetter('one'); + + $this->assertEquals('one', $propertyMetadata->getGetter()); + } + + public function testSetter() + { + $propertyMetadata = new PropertyMetadata('name'); + $propertyMetadata->setSetter('one'); + + $this->assertEquals('one', $propertyMetadata->getSetter()); + } + + public function testAdder() + { + $propertyMetadata = new PropertyMetadata('name'); + $propertyMetadata->setAdder('one'); + + $this->assertEquals('one', $propertyMetadata->getAdder()); + } + + public function testRemover() + { + $propertyMetadata = new PropertyMetadata('name'); + $propertyMetadata->setRemover('one'); + + $this->assertEquals('one', $propertyMetadata->getRemover()); + } + + public function testMerge() + { + $propertyMetadata1 = new PropertyMetadata('a1'); + $propertyMetadata1->setGetter('a'); + $propertyMetadata1->setSetter('b'); + + $propertyMetadata2 = new PropertyMetadata('a2'); + $propertyMetadata2->setGetter('c'); + $propertyMetadata2->setAdder('d'); + $propertyMetadata2->setRemover('e'); + + $propertyMetadata1->merge($propertyMetadata2); + + $this->assertEquals('a', $propertyMetadata1->getGetter()); + $this->assertEquals('b', $propertyMetadata1->getSetter()); + $this->assertEquals('d', $propertyMetadata1->getAdder()); + $this->assertEquals('e', $propertyMetadata1->getRemover()); + } + + public function testSerialize() + { + $propertyMetadata = new PropertyMetadata('attribute'); + $propertyMetadata->setGetter('a'); + $propertyMetadata->setSetter('b'); + $propertyMetadata->setAdder('c'); + $propertyMetadata->setRemover('d'); + + $serialized = serialize($propertyMetadata); + $this->assertEquals($propertyMetadata, unserialize($serialized)); + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Mapping/TestClassMetadataFactory.php b/src/Symfony/Component/PropertyAccess/Tests/Mapping/TestClassMetadataFactory.php new file mode 100644 index 0000000000000..251259031872c --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Mapping/TestClassMetadataFactory.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Mapping; + +use Symfony\Component\PropertyAccess\Mapping\PropertyMetadata; +use Symfony\Component\PropertyAccess\Mapping\ClassMetadata; + +/** + * @author Kévin Dunglas + */ +class TestClassMetadataFactory +{ + public static function createClassMetadata() + { + $expected = new ClassMetadata('Symfony\Component\PropertyAccess\Tests\Fixtures\Dummy'); + + $expected->getReflectionClass(); + + $foo = new PropertyMetadata('foo'); + $foo->setGetter('getter1'); + $foo->setSetter('setter1'); + $foo->setAdder('adder1'); + $foo->setRemover('remover1'); + $expected->addPropertyMetadata($foo); + + $bar = new PropertyMetadata('bar'); + $bar->setGetter('getter2'); + $expected->addPropertyMetadata($bar); + + $test = new PropertyMetadata('test'); + $test->setGetter('testChild'); + $expected->addPropertyMetadata($test); + + return $expected; + } + + public static function createXMLClassMetadata() + { + $expected = new ClassMetadata('Symfony\Component\PropertyAccess\Tests\Fixtures\Dummy'); + + $foo = new PropertyMetadata('foo'); + $foo->setGetter('getter1'); + $foo->setSetter('setter1'); + $foo->setAdder('adder1'); + $foo->setRemover('remover1'); + $expected->addPropertyMetadata($foo); + + $bar = new PropertyMetadata('bar'); + $bar->setGetter('getter2'); + $expected->addPropertyMetadata($bar); + + return $expected; + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorBuilderTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorBuilderTest.php index 2c65e6adf6ddd..7f53fe9880987 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorBuilderTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorBuilderTest.php @@ -49,6 +49,14 @@ public function testIsMagicCallEnable() $this->assertFalse($this->builder->disableMagicCall()->isMagicCallEnabled()); } + public function testMetadataFactory() + { + $metadataFactory = $this->getMock('Symfony\Component\PropertyAccess\Mapping\Factory\MetadataFactoryInterface'); + $this->assertNull($this->builder->getMetadataFactory()); + $this->assertSame($metadataFactory, $this->builder->setMetadataFactory($metadataFactory)->getMetadataFactory()); + $this->assertNull($this->builder->setMetadataFactory(null)->getMetadataFactory()); + } + public function testGetPropertyAccessor() { $this->assertInstanceOf(PropertyAccessor::class, $this->builder->getPropertyAccessor()); diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php index 17518468ebad8..54f7f6642b912 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php @@ -11,13 +11,28 @@ namespace Symfony\Component\PropertyAccess\Tests; +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\Annotations\AnnotationRegistry; +use Symfony\Component\PropertyAccess\Annotation\PropertyAdder; +use Symfony\Component\PropertyAccess\Annotation\PropertyGetter; +use Symfony\Component\PropertyAccess\Annotation\PropertyRemover; +use Symfony\Component\PropertyAccess\Mapping\Factory\LazyLoadingMetadataFactory; +use Symfony\Component\PropertyAccess\Mapping\Loader\AnnotationLoader; +use Symfony\Component\PropertyAccess\PropertyAccessor; + class PropertyAccessorCollectionTest_Car { private $axes; + /** + * @Symfony\Component\PropertyAccess\Annotation\Property(adder="addAxisTest", remover="removeAxisTest") + */ + private $customAxes; + public function __construct($axes = null) { $this->axes = $axes; + $this->customAxes = $axes; } // In the test, use a name that StringUtil can't uniquely singularify @@ -26,6 +41,16 @@ public function addAxis($axis) $this->axes[] = $axis; } + // In the test, use a name that StringUtil can't uniquely singularify + /** + * @PropertyAdder(property="customVirtualAxes") + * @param $axis + */ + public function addAxisTest($axis) + { + $this->customAxes[] = $axis; + } + public function removeAxis($axis) { foreach ($this->axes as $key => $value) { @@ -37,10 +62,34 @@ public function removeAxis($axis) } } + /** + * @PropertyRemover(property="customVirtualAxes") + * @param $axis + */ + public function removeAxisTest($axis) + { + foreach ($this->customAxes as $key => $value) { + if ($value === $axis) { + unset($this->customAxes[$key]); + + return; + } + } + } + public function getAxes() { return $this->axes; } + + /** + * @PropertyGetter(property="customVirtualAxes") + * @return null + */ + public function getCustomAxes() + { + return $this->customAxes; + } } class PropertyAccessorCollectionTest_CarOnlyAdder @@ -146,6 +195,50 @@ public function testSetValueCallsAdderAndRemoverForNestedCollections() $this->propertyAccessor->setValue($car, 'structure.axes', $axesAfter); } + public function testSetValueCallsCustomAdderAndRemoverForCollections() + { + $axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth', 4 => 'fifth')); + $axesMerged = $this->getContainer(array(1 => 'first', 2 => 'second', 3 => 'third')); + $axesAfter = $this->getContainer(array(1 => 'second', 5 => 'first', 6 => 'third')); + $axesMergedCopy = is_object($axesMerged) ? clone $axesMerged : $axesMerged; + + // Don't use a mock in order to test whether the collections are + // modified while iterating them + $car = new PropertyAccessorCollectionTest_Car($axesBefore); + + AnnotationRegistry::registerAutoloadNamespace('Symfony\Component\PropertyAccess\Annotation', __DIR__.'/../../../..'); + $this->propertyAccessor = new PropertyAccessor(false, false, null, new LazyLoadingMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + + $this->propertyAccessor->setValue($car, 'customAxes', $axesMerged); + + $this->assertEquals($axesAfter, $car->getCustomAxes()); + + // The passed collection was not modified + $this->assertEquals($axesMergedCopy, $axesMerged); + } + + public function testSetValueCallsCustomAdderAndRemoverForCollectionsMethodAnnotation() + { + $axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth', 4 => 'fifth')); + $axesMerged = $this->getContainer(array(1 => 'first', 2 => 'second', 3 => 'third')); + $axesAfter = $this->getContainer(array(1 => 'second', 5 => 'first', 6 => 'third')); + $axesMergedCopy = is_object($axesMerged) ? clone $axesMerged : $axesMerged; + + // Don't use a mock in order to test whether the collections are + // modified while iterating them + $car = new PropertyAccessorCollectionTest_Car($axesBefore); + + AnnotationRegistry::registerAutoloadNamespace('Symfony\Component\PropertyAccess\Annotation', __DIR__.'/../../../..'); + $this->propertyAccessor = new PropertyAccessor(false, false, null, new LazyLoadingMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + + $this->propertyAccessor->setValue($car, 'customVirtualAxes', $axesMerged); + + $this->assertEquals($axesAfter, $car->getCustomAxes()); + + // The passed collection was not modified + $this->assertEquals($axesMergedCopy, $axesMerged); + } + /** * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException * @expectedExceptionMessage Neither the property "axes" nor one of the methods "addAx()"/"removeAx()", "addAxe()"/"removeAxe()", "addAxis()"/"removeAxis()", "setAxes()", "axes()", "__set()" or "__call()" exist and have public access in class "Mock_PropertyAccessorCollectionTest_CarNoAdderAndRemover diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index a3a82b0b63cba..c96402b8c81ce 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -11,8 +11,12 @@ namespace Symfony\Component\PropertyAccess\Tests; +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\Annotations\AnnotationRegistry; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; +use Symfony\Component\PropertyAccess\Mapping\Factory\LazyLoadingMetadataFactory; +use Symfony\Component\PropertyAccess\Mapping\Loader\AnnotationLoader; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicCall; @@ -198,6 +202,20 @@ public function testGetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $p $this->propertyAccessor->getValue($objectOrArray, $path); } + public function testGetWithCustomGetter() + { + AnnotationRegistry::registerAutoloadNamespace('Symfony\Component\PropertyAccess\Annotation', __DIR__.'/../../../..'); + $this->propertyAccessor = new PropertyAccessor(false, false, null, new LazyLoadingMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + $this->assertSame('webmozart', $this->propertyAccessor->getValue(new TestClass('webmozart'), 'customGetterSetter')); + } + + public function testGetWithCustomGetterMethodAnnotation() + { + AnnotationRegistry::registerAutoloadNamespace('Symfony\Component\PropertyAccess\Annotation', __DIR__.'/../../../..'); + $this->propertyAccessor = new PropertyAccessor(false, false, null, new LazyLoadingMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + $this->assertSame(200, $this->propertyAccessor->getValue(new TestClass('webmozart', 10, 20), 'total')); + } + /** * @dataProvider getValidPropertyPaths */ @@ -298,6 +316,30 @@ public function testSetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $p $this->propertyAccessor->setValue($objectOrArray, $path, 'value'); } + public function testSetValueWithCustomSetter() + { + AnnotationRegistry::registerAutoloadNamespace('Symfony\Component\PropertyAccess\Annotation', __DIR__.'/../../../..'); + $this->propertyAccessor = new PropertyAccessor(false, false, null, new LazyLoadingMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + + $custom = new TestClass('webmozart'); + + $this->propertyAccessor->setValue($custom, 'customGetterSetter', 'it works!'); + + $this->assertEquals('it works!', $custom->customGetterTest()); + } + + public function testSetValueWithCustomSetterMethodAnnotation() + { + AnnotationRegistry::registerAutoloadNamespace('Symfony\Component\PropertyAccess\Annotation', __DIR__.'/../../../..'); + $this->propertyAccessor = new PropertyAccessor(false, false, null, new LazyLoadingMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + + $custom = new TestClass('webmozart', 10, 20); + + $this->propertyAccessor->setValue($custom, 'total', 5); + + $this->assertEquals(5, $custom->getTotal()); + } + public function testGetValueWhenArrayValueIsNull() { $this->propertyAccessor = new PropertyAccessor(false, true); diff --git a/src/Symfony/Component/PropertyAccess/composer.json b/src/Symfony/Component/PropertyAccess/composer.json index e095cbe35fe91..6b594d2c65263 100644 --- a/src/Symfony/Component/PropertyAccess/composer.json +++ b/src/Symfony/Component/PropertyAccess/composer.json @@ -21,7 +21,11 @@ "symfony/inflector": "~3.1" }, "require-dev": { - "symfony/cache": "~3.1" + "doctrine/cache": "~1.0", + "doctrine/annotations": "~1.2", + "symfony/cache": "~3.1", + "symfony/config": "~2.8|~3.0", + "symfony/yaml": "~2.8|~3.0" }, "suggest": { "psr/cache-implementation": "To cache access methods."