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." pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy