From e6aa7f1832fc3b8846a919976c6fb3cb9f225f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Ram=C3=B3n=20L=C3=B3pez?= Date: Tue, 28 Mar 2017 00:11:42 +0200 Subject: [PATCH 1/4] Added custom property accessors --- .../Annotation/AdderAccessor.php | 30 +++ .../Annotation/GetterAccessor.php | 30 +++ .../Annotation/PropertyAccessor.php | 51 +++++ .../Annotation/RemoverAccessor.php | 30 +++ .../Annotation/SetterAccessor.php | 30 +++ .../Exception/MappingException.php | 21 ++ .../Exception/NoSuchMetadataException.php | 19 ++ .../PropertyAccess/Mapping/ClassMetadata.php | 136 +++++++++++++ .../Factory/BlackHoleMetadataFactory.php | 40 ++++ .../Factory/LazyLoadingMetadataFactory.php | 123 ++++++++++++ .../Factory/MetadataFactoryInterface.php | 43 ++++ .../Mapping/Loader/AnnotationLoader.php | 108 ++++++++++ .../Mapping/Loader/FileLoader.php | 47 +++++ .../Mapping/Loader/LoaderChain.php | 63 ++++++ .../Mapping/Loader/LoaderInterface.php | 31 +++ .../Mapping/Loader/XmlFileLoader.php | 104 ++++++++++ .../Mapping/Loader/YamlFileLoader.php | 117 +++++++++++ .../property-access-mapping-1.0.xsd | 57 ++++++ .../Mapping/PropertyMetadata.php | 189 ++++++++++++++++++ .../PropertyAccess/PropertyAccessor.php | 95 +++++++-- .../PropertyAccessorBuilder.php | 32 ++- .../PropertyAccess/Tests/Fixtures/Dummy.php | 71 +++++++ .../Tests/Fixtures/DummyParent.php | 28 +++ .../Tests/Fixtures/TestClass.php | 53 ++++- .../Tests/Fixtures/empty-mapping.yml | 0 .../Tests/Fixtures/invalid-mapping.yml | 1 + .../Tests/Fixtures/property-access.xml | 12 ++ .../Tests/Fixtures/property-access.yml | 9 + .../Tests/Mapping/ClassMetadataTest.php | 63 ++++++ .../Factory/BlackHoleMetadataFactoryTest.php | 34 ++++ .../LazyLoadingMetadataFactoryTest.php | 57 ++++++ .../Mapping/Loader/AnnotationLoaderTest.php | 58 ++++++ .../Mapping/Loader/XmlFileLoaderTest.php | 56 ++++++ .../Mapping/Loader/YamlFileLoaderTest.php | 72 +++++++ .../Tests/Mapping/PropertyMetadataTest.php | 96 +++++++++ .../Mapping/TestClassMetadataFactory.php | 63 ++++++ .../Tests/PropertyAccessorBuilderTest.php | 9 + .../Tests/PropertyAccessorCollectionTest.php | 175 ++++++++++++++++ .../Tests/PropertyAccessorTest.php | 37 ++++ .../Component/PropertyAccess/composer.json | 11 +- 40 files changed, 2282 insertions(+), 19 deletions(-) create mode 100644 src/Symfony/Component/PropertyAccess/Annotation/AdderAccessor.php create mode 100644 src/Symfony/Component/PropertyAccess/Annotation/GetterAccessor.php create mode 100644 src/Symfony/Component/PropertyAccess/Annotation/PropertyAccessor.php create mode 100644 src/Symfony/Component/PropertyAccess/Annotation/RemoverAccessor.php create mode 100644 src/Symfony/Component/PropertyAccess/Annotation/SetterAccessor.php create mode 100644 src/Symfony/Component/PropertyAccess/Exception/MappingException.php create mode 100644 src/Symfony/Component/PropertyAccess/Exception/NoSuchMetadataException.php create mode 100644 src/Symfony/Component/PropertyAccess/Mapping/ClassMetadata.php create mode 100644 src/Symfony/Component/PropertyAccess/Mapping/Factory/BlackHoleMetadataFactory.php create mode 100644 src/Symfony/Component/PropertyAccess/Mapping/Factory/LazyLoadingMetadataFactory.php create mode 100644 src/Symfony/Component/PropertyAccess/Mapping/Factory/MetadataFactoryInterface.php create mode 100644 src/Symfony/Component/PropertyAccess/Mapping/Loader/AnnotationLoader.php create mode 100644 src/Symfony/Component/PropertyAccess/Mapping/Loader/FileLoader.php create mode 100644 src/Symfony/Component/PropertyAccess/Mapping/Loader/LoaderChain.php create mode 100644 src/Symfony/Component/PropertyAccess/Mapping/Loader/LoaderInterface.php create mode 100644 src/Symfony/Component/PropertyAccess/Mapping/Loader/XmlFileLoader.php create mode 100644 src/Symfony/Component/PropertyAccess/Mapping/Loader/YamlFileLoader.php create mode 100644 src/Symfony/Component/PropertyAccess/Mapping/Loader/schema/dic/property-access-mapping/property-access-mapping-1.0.xsd create mode 100644 src/Symfony/Component/PropertyAccess/Mapping/PropertyMetadata.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/Dummy.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/DummyParent.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/empty-mapping.yml create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/invalid-mapping.yml create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/property-access.xml create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/property-access.yml create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Mapping/ClassMetadataTest.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Mapping/Factory/BlackHoleMetadataFactoryTest.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Mapping/Loader/AnnotationLoaderTest.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Mapping/Loader/XmlFileLoaderTest.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Mapping/Loader/YamlFileLoaderTest.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Mapping/PropertyMetadataTest.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Mapping/TestClassMetadataFactory.php diff --git a/src/Symfony/Component/PropertyAccess/Annotation/AdderAccessor.php b/src/Symfony/Component/PropertyAccess/Annotation/AdderAccessor.php new file mode 100644 index 0000000000000..c2e1e121c33d9 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Annotation/AdderAccessor.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 AdderAccessor +{ + /** + * Associates this method to the adder of this property. + * + * @var string + */ + public $property; +} diff --git a/src/Symfony/Component/PropertyAccess/Annotation/GetterAccessor.php b/src/Symfony/Component/PropertyAccess/Annotation/GetterAccessor.php new file mode 100644 index 0000000000000..e1a38ea04543a --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Annotation/GetterAccessor.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 GetterAccessor +{ + /** + * Associates this method to the getter of this property. + * + * @var string + */ + public $property; +} diff --git a/src/Symfony/Component/PropertyAccess/Annotation/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/Annotation/PropertyAccessor.php new file mode 100644 index 0000000000000..eb0e0b5a61212 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Annotation/PropertyAccessor.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 PropertyAccessor +{ + /** + * 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/RemoverAccessor.php b/src/Symfony/Component/PropertyAccess/Annotation/RemoverAccessor.php new file mode 100644 index 0000000000000..9fa40bd819440 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Annotation/RemoverAccessor.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 RemoverAccessor +{ + /** + * Associates this method to the remover of this property. + * + * @var string + */ + public $property; +} diff --git a/src/Symfony/Component/PropertyAccess/Annotation/SetterAccessor.php b/src/Symfony/Component/PropertyAccess/Annotation/SetterAccessor.php new file mode 100644 index 0000000000000..4d013c4bd937f --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Annotation/SetterAccessor.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 SetterAccessor +{ + /** + * Associates this method to the setter of this property. + * + * @var string + */ + public $property; +} 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..3e1a37c14a999 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Mapping/ClassMetadata.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Mapping; + +/** + * @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; + } + + /** + * Return metadata for a particular property, or null if it doesn't exist. + * + * @param string $property + * + * @return PropertyMetadata|null + */ + public function getMetadataForProperty($property) + { + return isset($this->propertyMetadataCollection[$property]) ? $this->propertyMetadataCollection[$property] : null; + } + + /** + * Merges a {@link ClassMetadata} into the current one. + * + * @param self $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..282d501c87e90 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Mapping/Factory/LazyLoadingMetadataFactory.php @@ -0,0 +1,123 @@ + + * + * 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\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}. + * + * @author Luis Ramón López + */ +class LazyLoadingMetadataFactory implements MetadataFactoryInterface +{ + /** + * The loader for loading the class metadata. + * + * @var LoaderInterface|null + */ + private $loader; + + /** + * The loaded metadata, indexed by class name. + * + * @var ClassMetadata[] + */ + protected $loadedClasses = array(); + + /** + * Creates a new metadata factory. + * + * @param LoaderInterface|null $loader The loader for configuring new metadata + */ + public function __construct(LoaderInterface $loader = null) + { + $this->loader = $loader; + } + + /** + * {@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 (!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); + } + + 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, '\\'); + + return class_exists($class) || interface_exists($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..e66a1c6869c27 --- /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\ClassMetadata; +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 ClassMetadata 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..7a7dd28279386 --- /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\AdderAccessor; +use Symfony\Component\PropertyAccess\Annotation\GetterAccessor; +use Symfony\Component\PropertyAccess\Annotation\PropertyAccessor; +use Symfony\Component\PropertyAccess\Annotation\RemoverAccessor; +use Symfony\Component\PropertyAccess\Annotation\SetterAccessor; +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 PropertyAccessor) { + $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 GetterAccessor) { + 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 SetterAccessor) { + 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 AdderAccessor) { + 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 RemoverAccessor) { + 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..022c17605c7f7 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Mapping/PropertyMetadata.php @@ -0,0 +1,189 @@ + + * + * 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 self $propertyMetadata + */ + public function merge(PropertyMetadata $propertyMetadata) + { + // Overwrite only if not defined + if (null === $this->getter) { + $this->getter = $propertyMetadata->getter; + } + if (null === $this->setter) { + $this->setter = $propertyMetadata->setter; + } + if (null === $this->adder) { + $this->adder = $propertyMetadata->adder; + } + if (null === $this->remover) { + $this->remover = $propertyMetadata->remover; + } + } + + /** + * 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 2309cc63b4ed7..19ebfea645d0d 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -23,6 +23,8 @@ use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; +use Symfony\Component\PropertyAccess\Mapping\Factory\MetadataFactoryInterface; +use Symfony\Component\PropertyAccess\Mapping\PropertyMetadata; /** * Default implementation of {@link PropertyAccessorInterface}. @@ -30,6 +32,7 @@ * @author Bernhard Schussek * @author Kévin Dunglas * @author Nicolas Grekas + * @author Luis Ramón López */ class PropertyAccessor implements PropertyAccessorInterface { @@ -118,6 +121,11 @@ class PropertyAccessor implements PropertyAccessorInterface */ const CACHE_PREFIX_PROPERTY_PATH = 'p'; + /** + * @internal + */ + const CACHE_PREFIX_METADATA = 'm'; + /** * @var bool */ @@ -129,6 +137,14 @@ class PropertyAccessor implements PropertyAccessorInterface */ private $cacheItemPool; + /** + * @var MetadataFactoryInterface + */ + private $classMetadataFactory; + + /** + * @var array + */ private $readPropertyCache = array(); private $writePropertyCache = array(); private $propertyPathCache = array(); @@ -141,15 +157,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|null + * @param MetadataFactoryInterface $classMetadataFactory|null */ - 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; } /** @@ -520,18 +538,20 @@ private function getReadAccessInfo($class, $property) } if ($this->cacheItemPool) { - $item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_READ.str_replace('\\', '.', $key)); + $item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_READ.$this->encodeCacheKey($key)); if ($item->isHit()) { return $this->readPropertyCache[$key] = $item->get(); } } + $metadata = $this->getPropertyMetadata($class, $property); + $access = array(); $reflClass = new \ReflectionClass($class); $access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property); $camelProp = $this->camelize($property); - $getter = 'get'.$camelProp; + $getter = ($metadata && $metadata->getGetter()) ? $metadata->getGetter() : 'get'.$camelProp; $getsetter = lcfirst($camelProp); // jQuery style, e.g. read: last(), write: last($item) $isser = 'is'.$camelProp; $hasser = 'has'.$camelProp; @@ -699,12 +719,14 @@ private function getWriteAccessInfo($class, $property, $value) } if ($this->cacheItemPool) { - $item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_WRITE.str_replace('\\', '.', $key)); + $item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_WRITE.$this->encodeCacheKey($key)); if ($item->isHit()) { return $this->writePropertyCache[$key] = $item->get(); } } + $metadata = $this->getPropertyMetadata($class, $property); + $access = array(); $reflClass = new \ReflectionClass($class); @@ -713,7 +735,7 @@ private function getWriteAccessInfo($class, $property, $value) $singulars = (array) Inflector::singularize($camelized); if (is_array($value) || $value instanceof \Traversable) { - $methods = $this->findAdderAndRemover($reflClass, $singulars); + $methods = $this->findAdderAndRemover($reflClass, $singulars, $metadata); if (null !== $methods) { $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER; @@ -723,7 +745,8 @@ private function getWriteAccessInfo($class, $property, $value) } if (!isset($access[self::ACCESS_TYPE])) { - $setter = 'set'.$camelized; + $setter = ($metadata && $metadata->getSetter()) ? $metadata->getSetter() : 'set'.$camelized; + $getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item) if ($this->isMethodAccessible($reflClass, $setter, 1)) { @@ -742,7 +765,7 @@ private function getWriteAccessInfo($class, $property, $value) // 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)) { + } elseif (null !== $methods = $this->findAdderAndRemover($reflClass, $singulars, $metadata)) { $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 '. @@ -819,11 +842,17 @@ private function camelize($string) * * @return array|null An array containing the adder and remover when found, null otherwise */ - private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars) + private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars, PropertyMetadata $metadata = null) { + $fixedAdder = ($metadata && $metadata->getAdder()) ? $metadata->getAdder() : null; + $fixedRemover = ($metadata && $metadata->getRemover()) ? $metadata->getRemover() : null; + if ($fixedAdder && $fixedRemover) { + return array($fixedAdder, $fixedRemover); + } + foreach ($singulars as $singular) { - $addMethod = 'add'.$singular; - $removeMethod = 'remove'.$singular; + $addMethod = $fixedAdder ?: 'add'.$singular; + $removeMethod = $fixedRemover ?: 'remove'.$singular; $addMethodFound = $this->isMethodAccessible($reflClass, $addMethod, 1); $removeMethodFound = $this->isMethodAccessible($reflClass, $removeMethod, 1); @@ -832,6 +861,8 @@ private function findAdderAndRemover(\ReflectionClass $reflClass, array $singula return array($addMethod, $removeMethod); } } + + return null; } /** @@ -923,4 +954,42 @@ public static function createCache($namespace, $defaultLifetime, $version, Logge return $apcu; } + + /** + * Returns metadata associated with the property if it exists. + * + * @param $class + * @param $property + * + * @return null|PropertyMetadata + */ + private function getPropertyMetadata($class, $property) + { + if ($this->cacheItemPool) { + $key = (false !== strpos($class, '@') ? rawurlencode($class) : $class).'..'.$property; + $item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_METADATA.$this->encodeCacheKey($key)); + if ($item->isHit()) { + return $item->get(); + } + } + + $metadata = null; + if ($this->classMetadataFactory && $classMetadata = $this->classMetadataFactory->getMetadataFor($class)) { + $metadata = $classMetadata->getMetadataForProperty($property); + } + + return $metadata; + } + + /** + * Escapes the key so it does not contains invalid characters. + * + * @param string $key + * + * @return string + */ + private function encodeCacheKey($key) + { + return str_replace('\\', '.', $key); + } } diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php index 1db6a1dba23ed..83923538b3981 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php @@ -12,6 +12,7 @@ namespace Symfony\Component\PropertyAccess; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\PropertyAccess\Mapping\Factory\MetadataFactoryInterface; /** * A configurable builder to create a PropertyAccessor. @@ -28,6 +29,11 @@ class PropertyAccessorBuilder */ private $cacheItemPool; + /** + * @var MetadataFactoryInterface|null + */ + private $metadataFactory; + /** * Enables the use of "__call" by the PropertyAccessor. * @@ -121,6 +127,30 @@ public function getCacheItemPool() return $this->cacheItemPool; } + /** + * Sets a metadata loader. + * + * @param MetadataFactoryInterface|null $metadataFactory + * + * @return PropertyAccessorBuilder The builder object + */ + public function setMetadataFactory(MetadataFactoryInterface $metadataFactory = null) + { + $this->metadataFactory = $metadataFactory; + + return $this; + } + + /** + * Gets the used metadata loader. + * + * @return MetadataFactoryInterface|null + */ + public function getMetadataFactory() + { + return $this->metadataFactory; + } + /** * Builds and returns a new PropertyAccessor object. * @@ -128,6 +158,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->metadataFactory); } } 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..fe71ccfa21513 --- /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\PropertyAccessor; +use Symfony\Component\PropertyAccess\Annotation\GetterAccessor; + +/** + * Fixtures for testing metadata. + */ +class Dummy extends DummyParent +{ + /** + * @PropertyAccessor(getter="getter1", setter="setter1", adder="adder1", remover="remover1") + */ + protected $foo; + + /** + * @PropertyAccessor(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; + } + + /** + * @GetterAccessor(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..64e2dce9071b6 --- /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\GetterAccessor; + +/** + * Fixtures for testing metadata. + */ +class DummyParent +{ + /** + * @GetterAccessor(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..5d6de1fab52ec 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\PropertyAccessor; +use Symfony\Component\PropertyAccess\Annotation\GetterAccessor; +use Symfony\Component\PropertyAccess\Annotation\SetterAccessor; + 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; + + /** + * @PropertyAccessor(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; + } + + /** + * @GetterAccessor(property="total") + */ + public function getTotal() + { + return $this->quantity * $this->pricePerUnit; + } + + /** + * @SetterAccessor(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..4b59949d68615 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Mapping/ClassMetadataTest.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 PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\Mapping\ClassMetadata; + +/** + * @author Kévin Dunglas + * @author Luis Ramón López + */ +class ClassMetadataTest extends 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->getMockBuilder('Symfony\Component\PropertyAccess\Mapping\PropertyMetadata')->getMock(); + $a1->method('getName')->willReturn('a1'); + + $a2 = $this->getMockBuilder('Symfony\Component\PropertyAccess\Mapping\PropertyMetadata')->getMock(); + $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->getMockBuilder('Symfony\Component\PropertyAccess\Mapping\PropertyMetadata')->getMock(); + $a1->method('getName')->willReturn('b1'); + $a1->method('__sleep')->willReturn(array()); + + $a2 = $this->getMockBuilder('Symfony\Component\PropertyAccess\Mapping\PropertyMetadata')->getMock(); + $a2->method('getName')->willReturn('b2'); + $a2->method('__sleep')->willReturn(array()); + + $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..3cba200de34d4 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Mapping/Factory/BlackHoleMetadataFactoryTest.php @@ -0,0 +1,34 @@ + + * + * 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 PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\Mapping\Factory\BlackHoleMetadataFactory; + +class BlackHoleMetadataFactoryTest extends 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..43520ea65781b --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Mapping/Factory/LazyLoadingMetadataFactoryTest.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\Factory; + +use PHPUnit\Framework\TestCase; +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 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()); + } +} + +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..2a2277ffe0b3f --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Mapping/Loader/AnnotationLoaderTest.php @@ -0,0 +1,58 @@ + + * + * 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 PHPUnit\Framework\TestCase; +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 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..53f7d68f6a786 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -0,0 +1,56 @@ + + * + * 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 PHPUnit\Framework\TestCase; +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 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..ad484afb87302 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Mapping\Loader; + +use PHPUnit\Framework\TestCase; +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 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..35b64f1359dd5 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Mapping/PropertyMetadataTest.php @@ -0,0 +1,96 @@ + + * + * 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 PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\Mapping\PropertyMetadata; + +/** + * @author Kévin Dunglas + */ +class PropertyMetadataTest extends 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 63bd64225039a..83c399ca0680b 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorBuilderTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorBuilderTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\PropertyAccess\Mapping\Factory\BlackHoleMetadataFactory; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyAccess\PropertyAccessorBuilder; @@ -63,4 +64,12 @@ public function testUseCache() $this->assertEquals($cacheItemPool, $this->builder->getCacheItemPool()); $this->assertInstanceOf(PropertyAccessor::class, $this->builder->getPropertyAccessor()); } + + public function testUseMetadataFactory() + { + $metadataFactory = new BlackHoleMetadataFactory(); + $this->builder->setMetadataFactory($metadataFactory); + $this->assertEquals($metadataFactory, $this->builder->getMetadataFactory()); + $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 1f10262305242..542ca1f8e4057 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php @@ -11,13 +11,45 @@ namespace Symfony\Component\PropertyAccess\Tests; +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\Annotations\AnnotationRegistry; +use Symfony\Component\PropertyAccess\Annotation\AdderAccessor; +use Symfony\Component\PropertyAccess\Annotation\GetterAccessor; +use Symfony\Component\PropertyAccess\Annotation\RemoverAccessor; +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\PropertyAccessor(adder="addAxisTest", remover="removeAxisTest") + */ + private $customAxes; + + // This property will only have its adder accessor overriden + /** + * @Symfony\Component\PropertyAccess\Annotation\PropertyAccessor(adder="addAxis2Test") + */ + private $customAxes2; + + // This property will only have its remover accessor overriden + /** + * @Symfony\Component\PropertyAccess\Annotation\PropertyAccessor(remover="removeAxis3Test") + */ + private $customAxes3; + + /** + * @param array|null $axes + */ public function __construct($axes = null) { $this->axes = $axes; + $this->customAxes = $axes; + $this->customAxes2 = $axes; + $this->customAxes3 = $axes; } // In the test, use a name that StringUtil can't uniquely singularify @@ -26,6 +58,34 @@ public function addAxis($axis) $this->axes[] = $axis; } + // In the test, use a name that StringUtil can't uniquely singularify + /** + * @AdderAccessor(property="customVirtualAxes") + * @param $axis + */ + public function addAxisTest($axis) + { + $this->customAxes[] = $axis; + } + + // Only override adder accessor + /** + * @AdderAccessor(property="customVirtualAxes2") + * @param $axis + */ + public function addAxis2Test($axis) + { + $this->customAxes2[] = $axis; + } + + /** + * @param $axis + */ + public function addCustomAxes3($axis) + { + $this->customAxes3[] = $axis; + } + public function removeAxis($axis) { foreach ($this->axes as $key => $value) { @@ -37,10 +97,81 @@ public function removeAxis($axis) } } + /** + * @RemoverAccessor(property="customVirtualAxes") + * @param $axis + */ + public function removeAxisTest($axis) + { + foreach ($this->customAxes as $key => $value) { + if ($value === $axis) { + unset($this->customAxes[$key]); + + return; + } + } + } + + // Default customAxes2 remover + /** + * @param $axis + */ + public function removeCustomAxes2($axis) + { + foreach ($this->customAxes2 as $key => $value) { + if ($value === $axis) { + unset($this->customAxes2[$key]); + + return; + } + } + } + + // Only override remover accessor + /** + * @RemoverAccessor(property="customAxis3") + * @param $axis + */ + public function removeAxis3Test($axis) + { + foreach ($this->customAxes3 as $key => $value) { + if ($value === $axis) { + unset($this->customAxes3[$key]); + + return; + } + } + } + public function getAxes() { return $this->axes; } + + /** + * @GetterAccessor(property="customVirtualAxes") + * @return array|null + */ + public function getCustomAxes() + { + return $this->customAxes; + } + + /** + * @return array|null + */ + public function getCustomAxes2() + { + return $this->customAxes2; + } + + /** + * @return array|null + */ + public function getCustomAxes3() + { + return $this->customAxes3; + } } class PropertyAccessorCollectionTest_CarOnlyAdder @@ -146,6 +277,50 @@ public function testSetValueCallsAdderAndRemoverForNestedCollections() $this->propertyAccessor->setValue($car, 'structure.axes', $axesAfter); } + /** + * @param $propertyPath string Property path to test + */ + private function baseTestAdderAndRemoverPropertyPath($propertyPath, $getMethod) + { + $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, $propertyPath, $axesMerged); + + $this->assertEquals($axesAfter, $car->$getMethod()); + + // The passed collection was not modified + $this->assertEquals($axesMergedCopy, $axesMerged); + } + public function testSetValueCallsCustomAdderAndRemoverForCollections() + { + $this->baseTestAdderAndRemoverPropertyPath('customAxes', 'getCustomAxes'); + } + + public function testSetValueCallsCustomAdderAndRemoverForCollectionsMethodAnnotation() + { + $this->baseTestAdderAndRemoverPropertyPath('customVirtualAxes', 'getCustomAxes'); + } + + public function testSetValueCallsCustomAdderButNotRemoverForCollectionsMethodAnnotation() + { + $this->baseTestAdderAndRemoverPropertyPath('customAxes2', 'getCustomAxes2'); + } + + public function testSetValueCallsCustomRemoverButNotAdderForCollectionsMethodAnnotation() + { + $this->baseTestAdderAndRemoverPropertyPath('customAxes3', 'getCustomAxes3'); + } + /** * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException * @expectedExceptionMessageRegExp /Could not determine access type for property "axes" in class "Mock_PropertyAccessorCollectionTest_CarNoAdderAndRemover_[^"]*"./ diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index b8356500a5881..554347840b7dc 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -11,9 +11,12 @@ namespace Symfony\Component\PropertyAccess\Tests; +use Doctrine\Common\Annotations\AnnotationReader; use PHPUnit\Framework\TestCase; 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\ReturnTyped; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass; @@ -201,6 +204,18 @@ public function testGetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $p $this->propertyAccessor->getValue($objectOrArray, $path); } + public function testGetWithCustomGetter() + { + $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() + { + $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 */ @@ -301,6 +316,28 @@ public function testSetValueThrowsExceptionIfNotObjectOrArray($objectOrArray, $p $this->propertyAccessor->setValue($objectOrArray, $path, 'value'); } + public function testSetValueWithCustomSetter() + { + $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() + { + $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 d6e7afb69c48c..c30722bb14998 100644 --- a/src/Symfony/Component/PropertyAccess/composer.json +++ b/src/Symfony/Component/PropertyAccess/composer.json @@ -18,13 +18,18 @@ "require": { "php": "^5.5.9|>=7.0.8", "symfony/polyfill-php70": "~1.0", - "symfony/inflector": "~3.1|~4.0" + "symfony/inflector": "~3.4|~4.0" }, "require-dev": { - "symfony/cache": "~3.1|~4.0" + "doctrine/annotations": "~1.2", + "symfony/cache": "~3.4", + "symfony/config": "~3.4", + "symfony/yaml": "~3.4" }, "suggest": { - "psr/cache-implementation": "To cache access methods." + "psr/cache-implementation": "To cache access methods.", + "doctrine/annotations": "To define custom accessors using annotations.", + "symfony/yaml": "To define custom accessors using YAML config files." }, "autoload": { "psr-4": { "Symfony\\Component\\PropertyAccess\\": "" }, From b6171496eeeacf0a549e046ea2e936cb1297c190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Ram=C3=B3n=20L=C3=B3pez?= Date: Thu, 5 Oct 2017 10:52:25 +0200 Subject: [PATCH 2/4] Refactored component mapping registering --- .../DependencyInjection/FrameworkExtension.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 2844bbc783e2c..4b4d7916cbf44 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1271,26 +1271,31 @@ private function registerValidatorMapping(ContainerBuilder $container, array $co $fileRecorder('xml', dirname($reflClass->getFileName()).'/Resources/config/validation.xml'); } + $this->registerComponentMapping($container, $fileRecorder, 'validation'); + + $this->registerMappingFilesFromConfig($container, $config, $fileRecorder); + } + + private function registerComponentMapping(ContainerBuilder $container, $fileRecorder, $component) + { foreach ($container->getParameter('kernel.bundles_metadata') as $bundle) { $dirname = $bundle['path']; if ( - $container->fileExists($file = $dirname.'/Resources/config/validation.yaml', false) || - $container->fileExists($file = $dirname.'/Resources/config/validation.yml', false) + $container->fileExists($file = $dirname . '/Resources/config/'.$component.'.yaml', false) || + $container->fileExists($file = $dirname . '/Resources/config/'.$component.'.yml', false) ) { $fileRecorder('yml', $file); } - if ($container->fileExists($file = $dirname.'/Resources/config/validation.xml', false)) { + if ($container->fileExists($file = $dirname . '/Resources/config/'.$component.'.xml', false)) { $fileRecorder('xml', $file); } - if ($container->fileExists($dir = $dirname.'/Resources/config/validation', '/^$/')) { + if ($container->fileExists($dir = $dirname . '/Resources/config/'.$component, '/^$/')) { $this->registerMappingFilesFromDir($dir, $fileRecorder); } } - - $this->registerMappingFilesFromConfig($container, $config, $fileRecorder); } private function registerMappingFilesFromDir($dir, callable $fileRecorder) From 6f252c594484a6699bfae89d4966287253362d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Ram=C3=B3n=20L=C3=B3pez?= Date: Mon, 6 Nov 2017 09:44:45 +0100 Subject: [PATCH 3/4] Make fabbot.io happy :) --- .../DependencyInjection/FrameworkExtension.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 4b4d7916cbf44..b8cd50ca563c5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1282,17 +1282,17 @@ private function registerComponentMapping(ContainerBuilder $container, $fileReco $dirname = $bundle['path']; if ( - $container->fileExists($file = $dirname . '/Resources/config/'.$component.'.yaml', false) || - $container->fileExists($file = $dirname . '/Resources/config/'.$component.'.yml', false) + $container->fileExists($file = $dirname.'/Resources/config/'.$component.'.yaml', false) || + $container->fileExists($file = $dirname.'/Resources/config/'.$component.'.yml', false) ) { $fileRecorder('yml', $file); } - if ($container->fileExists($file = $dirname . '/Resources/config/'.$component.'.xml', false)) { + if ($container->fileExists($file = $dirname.'/Resources/config/'.$component.'.xml', false)) { $fileRecorder('xml', $file); } - if ($container->fileExists($dir = $dirname . '/Resources/config/'.$component, '/^$/')) { + if ($container->fileExists($dir = $dirname.'/Resources/config/'.$component, '/^$/')) { $this->registerMappingFilesFromDir($dir, $fileRecorder); } } From 91d9d74db37787fc7ce566b871497a4b41defbe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Ram=C3=B3n=20L=C3=B3pez?= Date: Fri, 6 Oct 2017 08:51:58 +0200 Subject: [PATCH 4/4] Added MetadataLoader to PropertyAccess configuration registration --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../DependencyInjection/Configuration.php | 11 +++++++- .../FrameworkExtension.php | 28 ++++++++++++++++++- .../Resources/config/property_access.xml | 11 ++++++++ .../DependencyInjection/ConfigurationTest.php | 4 +++ .../DependencyInjection/Fixtures/php/full.php | 5 ++++ .../Fixtures/php/property_accessor.php | 1 + .../Bundle/FrameworkBundle/composer.json | 8 ++++++ 8 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index db3d480b3e6c1..a4b7401a31f98 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -28,6 +28,7 @@ CHANGELOG name as value, using it makes the command lazy * Added `cache:pool:prune` command to allow manual stale cache item pruning of supported PSR-6 and PSR-16 cache pool implementations + * Added custom property accessors support * Deprecated `Symfony\Bundle\FrameworkBundle\Translation\TranslationLoader`, use `Symfony\Component\Translation\Reader\TranslationReader` instead * Deprecated `translation.loader` service, use `translation.reader` instead diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 7c7cc59142173..ceb8467162cd9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -790,7 +790,16 @@ private function addPropertyAccessSection(ArrayNodeDefinition $rootNode) ->children() ->booleanNode('magic_call')->defaultFalse()->end() ->booleanNode('throw_exception_on_invalid_index')->defaultFalse()->end() - ->end() + ->booleanNode('enable_annotations')->defaultFalse()->end() + ->arrayNode('mapping') + ->addDefaultsIfNotSet() + ->fixXmlConfig('path') + ->children() + ->arrayNode('paths') + ->prototype('scalar')->end() + ->end() + ->end() + ->end() ->end() ->end() ; diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index b8cd50ca563c5..ddba05459cb8c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1391,10 +1391,36 @@ private function registerPropertyAccessConfiguration(array $config, ContainerBui $loader->load('property_access.xml'); - $container->getDefinition('property_accessor')->setPrivate(true); + $loaders = array(); + $fileRecorder = function ($extension, $path) use (&$loaders) { + $definition = new Definition(in_array($extension, array('yaml', 'yml')) ? 'Symfony\Component\PropertyAccess\Mapping\Loader\YamlFileLoader' : 'Symfony\Component\PropertyAccess\Mapping\Loader\XmlFileLoader', array($path)); + $definition->setPublic(false); + $loaders[] = $definition; + }; + + if (isset($config['enable_annotations']) && $config['enable_annotations']) { + if (!$this->annotationsConfigEnabled) { + throw new \LogicException('"enable_annotations" on property access cannot be set as Annotations support is disabled.'); + } + $annotationLoader = new Definition( + 'Symfony\Component\PropertyAccess\Mapping\Loader\AnnotationLoader', + array(new Reference('annotation_reader')) + ); + $annotationLoader->setPublic(false); + $loaders[] = $annotationLoader; + } + + $this->registerComponentMapping($container, $fileRecorder, 'property_accessor'); + + $this->registerMappingFilesFromConfig($container, $config, $fileRecorder); + + $chainLoader = $container->getDefinition('property_access.mapping.chain_loader'); + + $chainLoader->replaceArgument(0, $loaders); $container ->getDefinition('property_accessor') + ->setPrivate(true) ->replaceArgument(0, $config['magic_call']) ->replaceArgument(1, $config['throw_exception_on_invalid_index']) ; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml index 91924e5972d8e..b13f058e00717 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml @@ -7,10 +7,21 @@ + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 50122ed194e00..1029dc24d7c8d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -282,6 +282,10 @@ protected static function getBundleDefaultConfig() 'property_access' => array( 'magic_call' => false, 'throw_exception_on_invalid_index' => false, + 'enable_annotations' => false, + 'mapping' => array( + 'paths' => array(), + ), ), '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 2b2b3f45f0a8d..8986fba40015d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -64,6 +64,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..e27d2271d29de 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' => true, ), )); diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 27c77d0da628e..5fc317e9e75cb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -15,6 +15,12 @@ "homepage": "https://symfony.com/contributors" } ], + "repositories": [ + { + "type": "git", + "url": "https://github.com/lrlopez/property-access" + } + ], "require": { "php": "^5.5.9|>=7.0.8", "ext-xml": "*", @@ -35,6 +41,7 @@ "fig/link-util": "^1.0", "symfony/asset": "~3.3|~4.0", "symfony/browser-kit": "~2.8|~3.0|~4.0", + "symfony/cache": "~3.4|~4.0", "symfony/console": "~3.4|~4.0", "symfony/css-selector": "~2.8|~3.0|~4.0", "symfony/dom-crawler": "~2.8|~3.0|~4.0", @@ -43,6 +50,7 @@ "symfony/form": "~3.4|~4.0", "symfony/expression-language": "~2.8|~3.0|~4.0", "symfony/process": "~2.8|~3.0|~4.0", + "symfony/property-access": "dev-master", "symfony/security-core": "~3.2|~4.0", "symfony/security-csrf": "~2.8|~3.0|~4.0", "symfony/serializer": "~3.3|~4.0",