Преглед на файлове

Merge pull request #3488 from Koc/property-access-reuse

Property access reuse
Oskar Stark преди 9 години
родител
ревизия
9681619bdf

+ 1 - 4
Admin/Admin.php

@@ -39,7 +39,6 @@ use Sonata\CoreBundle\Validator\ErrorElement;
 use Symfony\Component\Form\Form;
 use Symfony\Component\Form\FormBuilderInterface;
 use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\PropertyAccess\PropertyAccess;
 use Symfony\Component\PropertyAccess\PropertyPath;
 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 use Symfony\Component\Security\Acl\Model\DomainObjectInterface;
@@ -974,7 +973,7 @@ abstract class Admin implements AdminInterface, DomainObjectInterface
         if ($this->isChild() && $this->getParentAssociationMapping()) {
             $parent = $this->getParent()->getObject($this->request->get($this->getParent()->getIdParameter()));
 
-            $propertyAccessor = PropertyAccess::createPropertyAccessor();
+            $propertyAccessor = $this->getConfigurationPool()->getPropertyAccessor();
             $propertyPath = new PropertyPath($this->getParentAssociationMapping());
 
             $object = $this->getSubject();
@@ -1367,8 +1366,6 @@ abstract class Admin implements AdminInterface, DomainObjectInterface
         if (isset($this->templates[$name])) {
             return $this->templates[$name];
         }
-
-        return;
     }
 
     /**

+ 17 - 17
Admin/AdminHelper.php

@@ -19,7 +19,7 @@ use Sonata\AdminBundle\Util\FormViewIterator;
 use Symfony\Component\Form\FormBuilderInterface;
 use Symfony\Component\Form\FormView;
 use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
-use Symfony\Component\PropertyAccess\PropertyAccessor;
+use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
 
 /**
  * Class AdminHelper.
@@ -118,7 +118,7 @@ class AdminHelper
         //Child form not found (probably nested one)
         //if childFormBuilder was not found resulted in fatal error getName() method call on non object
         if (!$childFormBuilder) {
-            $propertyAccessor = new PropertyAccessor();
+            $propertyAccessor = $this->pool->getPropertyAccessor();
             $entity = $admin->getSubject();
 
             $path = $this->getElementAccessPath($elementId, $entity);
@@ -249,7 +249,7 @@ class AdminHelper
     }
 
     /**
-     * get access path to element which works with PropertyAccessor.
+     * Get access path to element which works with PropertyAccessor.
      *
      * @param string $elementId expects string in format used in form id field. (uniqueIdentifier_model_sub_model or uniqueIdentifier_model_1_sub_model etc.)
      * @param mixed  $entity
@@ -260,7 +260,7 @@ class AdminHelper
      */
     public function getElementAccessPath($elementId, $entity)
     {
-        $propertyAccessor = new PropertyAccessor();
+        $propertyAccessor = $this->pool->getPropertyAccessor();
 
         $idWithoutUniqueIdentifier = implode('_', explode('_', substr($elementId, strpos($elementId, '_') + 1)));
 
@@ -312,29 +312,29 @@ class AdminHelper
     }
 
     /**
-     * check if given path exists in $entity.
+     * Check if given path exists in $entity.
      *
-     * @param PropertyAccessor $propertyAccessor
-     * @param mixed            $entity
-     * @param string           $path
+     * @param PropertyAccessorInterface $propertyAccessor
+     * @param mixed                     $entity
+     * @param string                    $path
      *
      * @return bool
      *
      * @throws \RuntimeException
      */
-    private function pathExists(PropertyAccessor $propertyAccessor, $entity, $path)
+    private function pathExists(PropertyAccessorInterface $propertyAccessor, $entity, $path)
     {
-        //sf2 <= 2.3 did not have isReadable method for PropertyAccessor
+        // Symfony <= 2.3 did not have isReadable method for PropertyAccessor
         if (method_exists($propertyAccessor, 'isReadable')) {
             return $propertyAccessor->isReadable($entity, $path);
-        } else {
-            try {
-                $propertyAccessor->getValue($entity, $path);
+        }
 
-                return true;
-            } catch (NoSuchPropertyException $e) {
-                return false;
-            }
+        try {
+            $propertyAccessor->getValue($entity, $path);
+
+            return true;
+        } catch (NoSuchPropertyException $e) {
+            return false;
         }
     }
 }

+ 20 - 5
Admin/Pool.php

@@ -12,6 +12,8 @@
 namespace Sonata\AdminBundle\Admin;
 
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\PropertyAccess\PropertyAccess;
+use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
 
 /**
  * Class Pool.
@@ -21,9 +23,9 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 class Pool
 {
     /**
-     * @var ContainerInterface|null
+     * @var ContainerInterface
      */
-    protected $container = null;
+    protected $container;
 
     /**
      * @var string[]
@@ -65,18 +67,24 @@ class Pool
      */
     protected $options;
 
+    /**
+     * @var PropertyAccessorInterface
+     */
+    protected $propertyAccessor;
+
     /**
      * @param ContainerInterface $container
      * @param string             $title
      * @param string             $logoTitle
      * @param array              $options
      */
-    public function __construct(ContainerInterface $container, $title, $logoTitle, $options = array())
+    public function __construct(ContainerInterface $container, $title, $logoTitle, $options = array(), PropertyAccessorInterface $propertyAccessor = null)
     {
         $this->container = $container;
         $this->title     = $title;
         $this->titleLogo = $logoTitle;
         $this->options   = $options;
+        $this->propertyAccessor = $propertyAccessor;
     }
 
     /**
@@ -325,8 +333,6 @@ class Pool
         if (isset($this->templates[$name])) {
             return $this->templates[$name];
         }
-
-        return;
     }
 
     /**
@@ -359,4 +365,13 @@ class Pool
 
         return $default;
     }
+
+    public function getPropertyAccessor()
+    {
+        if (null === $this->propertyAccessor) {
+            $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
+        }
+
+        return $this->propertyAccessor;
+    }
 }

+ 3 - 5
Controller/HelperController.php

@@ -20,7 +20,6 @@ use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
-use Symfony\Component\PropertyAccess\PropertyAccess;
 use Symfony\Component\PropertyAccess\PropertyPath;
 use Symfony\Component\Security\Core\Exception\AccessDeniedException;
 use Symfony\Component\Validator\Validator\ValidatorInterface;
@@ -269,12 +268,11 @@ class HelperController
             return new JsonResponse(array('status' => 'KO', 'message' => 'The field cannot be edit, editable option must be set to true'));
         }
 
-        $propertyAccessor = PropertyAccess::createPropertyAccessor();
-        $propertyPath     = new PropertyPath($field);
+        $propertyPath = new PropertyPath($field);
 
         // If property path has more than 1 element, take the last object in order to validate it
         if ($propertyPath->getLength() > 1) {
-            $object = $propertyAccessor->getValue($object, $propertyPath->getParent());
+            $object = $this->pool->getPropertyAccessor()->getValue($object, $propertyPath->getParent());
 
             $elements     = $propertyPath->getElements();
             $field        = end($elements);
@@ -286,7 +284,7 @@ class HelperController
             $value = new \DateTime($value);
         }
 
-        $propertyAccessor->setValue($object, $propertyPath, '' !== $value ? $value : null);
+        $this->pool->getPropertyAccessor()->setValue($object, $propertyPath, '' !== $value ? $value : null);
 
         $violations = $this->validator->validate($object);
 

+ 15 - 0
DependencyInjection/SonataAdminExtension.php

@@ -203,6 +203,8 @@ BOOM
         $container->setParameter('sonata.admin.configuration.filters.persist', $config['persist_filters']);
 
         $this->configureClassesToCompile();
+
+        $this->replacePropertyAccessor($container);
     }
 
     public function configureClassesToCompile()
@@ -297,4 +299,17 @@ BOOM
     {
         return 'https://sonata-project.org/schema/dic/admin';
     }
+
+    private function replacePropertyAccessor(ContainerBuilder $container)
+    {
+        if (!$container->has('form.property_accessor')) {
+            return;
+        }
+
+        $pool = $container->getDefinition('sonata.admin.pool');
+        $pool->replaceArgument(4, new Reference('form.property_accessor'));
+
+        $modelChoice = $container->getDefinition('sonata.admin.form.type.model_choice');
+        $modelChoice->replaceArgument(0, new Reference('form.property_accessor'));
+    }
 }

+ 11 - 4
Form/ChoiceList/ModelChoiceList.php

@@ -17,6 +17,7 @@ use Symfony\Component\Form\Exception\InvalidArgumentException;
 use Symfony\Component\Form\Exception\RuntimeException;
 use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
 use Symfony\Component\PropertyAccess\PropertyAccess;
+use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
 use Symfony\Component\PropertyAccess\PropertyPath;
 
 /**
@@ -82,6 +83,11 @@ class ModelChoiceList extends SimpleChoiceList
      */
     private $propertyPath;
 
+    /**
+     * @var PropertyAccessorInterface
+     */
+    private $propertyAccessor;
+
     /**
      * @param ModelManagerInterface $modelManager
      * @param string                $class
@@ -89,7 +95,7 @@ class ModelChoiceList extends SimpleChoiceList
      * @param null                  $query
      * @param array                 $choices
      */
-    public function __construct(ModelManagerInterface $modelManager, $class, $property = null, $query = null, $choices = array())
+    public function __construct(ModelManagerInterface $modelManager, $class, $property = null, $query = null, $choices = array(), PropertyAccessorInterface $propertyAccessor = null)
     {
         $this->modelManager   = $modelManager;
         $this->class          = $class;
@@ -100,6 +106,7 @@ class ModelChoiceList extends SimpleChoiceList
         // displaying entities as strings
         if ($property) {
             $this->propertyPath = new PropertyPath($property);
+            $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
         }
 
         parent::__construct($this->load($choices));
@@ -146,14 +153,14 @@ class ModelChoiceList extends SimpleChoiceList
         foreach ($entities as $key => $entity) {
             if ($this->propertyPath) {
                 // If the property option was given, use it
-                $propertyAccessor = PropertyAccess::createPropertyAccessor();
-                $value = $propertyAccessor->getValue($entity, $this->propertyPath);
+                $value = $this->propertyAccessor->getValue($entity, $this->propertyPath);
             } else {
                 // Otherwise expect a __toString() method in the entity
                 try {
                     $value = (string) $entity;
                 } catch (\Exception $e) {
-                    throw new RuntimeException(sprintf("Unable to convert the entity %s to String, entity must have a '__toString()' method defined", ClassUtils::getClass($entity)), 0, $e);
+                    throw new RuntimeException(sprintf('Unable to convert the entity "%s" to string, provide '
+                        .'"property" option or implement "__toString()" method in your entity.', ClassUtils::getClass($entity)), 0, $e);
                 }
             }
 

+ 20 - 11
Form/ChoiceList/ModelChoiceLoader.php

@@ -17,6 +17,7 @@ use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
 use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
 use Symfony\Component\Form\Exception\RuntimeException;
 use Symfony\Component\PropertyAccess\PropertyAccess;
+use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
 use Symfony\Component\PropertyAccess\PropertyPath;
 
 /**
@@ -42,18 +43,27 @@ class ModelChoiceLoader implements ChoiceLoaderInterface
 
     private $choices;
 
+    /**
+     * @var PropertyPath
+     */
     private $propertyPath;
 
+    /**
+     * @var PropertyAccessorInterface
+     */
+    private $propertyAccessor;
+
     private $choiceList;
 
     /**
-     * @param ModelManagerInterface $modelManager
-     * @param string                $class
-     * @param null                  $property
-     * @param null                  $query
-     * @param array                 $choices
+     * @param ModelManagerInterface          $modelManager
+     * @param string                         $class
+     * @param null                           $property
+     * @param null                           $query
+     * @param array                          $choices
+     * @param PropertyAccessorInterface|null $propertyAccessor
      */
-    public function __construct(ModelManagerInterface $modelManager, $class, $property = null, $query = null, $choices = array())
+    public function __construct(ModelManagerInterface $modelManager, $class, $property = null, $query = null, $choices = array(), PropertyAccessorInterface $propertyAccessor = null)
     {
         $this->modelManager = $modelManager;
         $this->class = $class;
@@ -61,13 +71,13 @@ class ModelChoiceLoader implements ChoiceLoaderInterface
         $this->query = $query;
         $this->choices = $choices;
 
-        $this->identifier     = $this->modelManager->getIdentifierFieldNames($this->class);
+        $this->identifier = $this->modelManager->getIdentifierFieldNames($this->class);
 
         // The property option defines, which property (path) is used for
         // displaying entities as strings
         if ($property) {
             $this->propertyPath = new PropertyPath($property);
-            $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
+            $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
         }
     }
 
@@ -89,14 +99,13 @@ class ModelChoiceLoader implements ChoiceLoaderInterface
             foreach ($entities as $key => $entity) {
                 if ($this->propertyPath) {
                     // If the property option was given, use it
-                    $propertyAccessor = PropertyAccess::createPropertyAccessor();
-                    $valueObject = $propertyAccessor->getValue($entity, $this->propertyPath);
+                    $valueObject = $this->propertyAccessor->getValue($entity, $this->propertyPath);
                 } else {
                     // Otherwise expect a __toString() method in the entity
                     try {
                         $valueObject = (string) $entity;
                     } catch (\Exception $e) {
-                        throw new RuntimeException(sprintf("Unable to convert the entity %s to String, entity must have a '__toString()' method defined", ClassUtils::getClass($entity)), 0, $e);
+                        throw new RuntimeException(sprintf('Unable to convert the entity "%s" to string, provide "property" option or implement "__toString()" method in your entity.', ClassUtils::getClass($entity)), 0, $e);
                     }
                 }
 

+ 18 - 4
Form/Type/ModelType.php

@@ -24,6 +24,7 @@ use Symfony\Component\Form\FormView;
 use Symfony\Component\OptionsResolver\Options;
 use Symfony\Component\OptionsResolver\OptionsResolver;
 use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
 
 /**
  * Class ModelType
@@ -33,6 +34,16 @@ use Symfony\Component\OptionsResolver\OptionsResolverInterface;
  */
 class ModelType extends AbstractType
 {
+    /**
+     * @var PropertyAccessorInterface
+     */
+    protected $propertyAccessor;
+
+    public function __construct(PropertyAccessorInterface $propertyAccessor)
+    {
+        $this->propertyAccessor = $propertyAccessor;
+    }
+
     /**
      * {@inheritdoc}
      */
@@ -82,8 +93,9 @@ class ModelType extends AbstractType
     public function configureOptions(OptionsResolver $resolver)
     {
         $options = array();
+        $propertyAccessor = $this->propertyAccessor;
         if (interface_exists('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface')) { // SF2.7+
-            $options['choice_loader'] = function (Options $options, $previousValue) {
+            $options['choice_loader'] = function (Options $options, $previousValue) use ($propertyAccessor) {
                 if ($previousValue && count($choices = $previousValue->getChoices())) {
                     return $choices;
                 }
@@ -93,12 +105,13 @@ class ModelType extends AbstractType
                     $options['class'],
                     $options['property'],
                     $options['query'],
-                    $options['choices']
+                    $options['choices'],
+                    $propertyAccessor
                 );
 
             };
         } else {
-            $options['choice_list'] = function (Options $options, $previousValue) {
+            $options['choice_list'] = function (Options $options, $previousValue) use ($propertyAccessor) {
                 if ($previousValue && count($choices = $previousValue->getChoices())) {
                     return $choices;
                 }
@@ -108,7 +121,8 @@ class ModelType extends AbstractType
                     $options['class'],
                     $options['property'],
                     $options['query'],
-                    $options['choices']
+                    $options['choices'],
+                    $propertyAccessor
                 );
             };
         }

+ 1 - 1
Resources/config/core.xml

@@ -10,7 +10,7 @@
             <argument />
             <argument />
             <argument type="collection" />
-            <argument type="collection" />
+            <argument type="service" id="property_accessor" />
 
             <call method="setTemplates">
                 <argument>%sonata.admin.configuration.templates%</argument>

+ 1 - 0
Resources/config/form_types.xml

@@ -12,6 +12,7 @@
         </service>
 
         <service id="sonata.admin.form.type.model_choice" class="Sonata\AdminBundle\Form\Type\ModelType">
+            <argument type="service" id="property_accessor" />
             <tag name="form.type" alias="sonata_type_model" />
         </service>
 

+ 9 - 0
Tests/Admin/AdminTest.php

@@ -26,6 +26,7 @@ use Sonata\AdminBundle\Tests\Fixtures\Bundle\Entity\Tag;
 use Sonata\AdminBundle\Tests\Fixtures\Entity\FooToString;
 use Sonata\AdminBundle\Tests\Fixtures\Entity\FooToStringNull;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\PropertyAccess\PropertyAccess;
 
 class AdminTest extends \PHPUnit_Framework_TestCase
 {
@@ -1477,6 +1478,14 @@ class AdminTest extends \PHPUnit_Framework_TestCase
         $request = $this->getMock('Symfony\Component\HttpFoundation\Request');
         $tagAdmin->setRequest($request);
 
+        $configurationPool = $this->getMockBuilder('Sonata\AdminBundle\Admin\Pool')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $configurationPool->expects($this->any())->method('getPropertyAccessor')->will($this->returnValue(PropertyAccess::createPropertyAccessor()));
+
+        $tagAdmin->setConfigurationPool($configurationPool);
+
         return $tagAdmin;
     }
 

+ 3 - 0
Tests/DependencyInjection/Compiler/AddDependencyCallsCompilerPassTest.php

@@ -365,6 +365,9 @@ class AddDependencyCallsCompilerPassTest extends \PHPUnit_Framework_TestCase
         $container
             ->register('router')
             ->setClass('Symfony\Component\Routing\RouterInterface');
+        $container
+            ->register('property_accessor')
+            ->setClass('Symfony\Component\PropertyAccess\PropertyAccessor');
         $container
             ->register('form.factory')
             ->setClass('Symfony\Component\Form\FormFactoryInterface');

+ 3 - 0
Tests/DependencyInjection/Compiler/ExtensionCompilerPassTest.php

@@ -349,6 +349,9 @@ class ExtensionCompilerPassTest extends \PHPUnit_Framework_TestCase
         $container
             ->register('router')
             ->setClass('Symfony\Component\Routing\RouterInterface');
+        $container
+            ->register('property_accessor')
+            ->setClass('Symfony\Component\PropertyAccess\PropertyAccessor');
         $container
             ->register('form.factory')
             ->setClass('Symfony\Component\Form\FormFactoryInterface');

+ 14 - 6
Tests/Form/Type/ModelTypeTest.php

@@ -14,20 +14,29 @@ namespace Sonata\AdminBundle\Tests\Form\Type;
 use Sonata\AdminBundle\Form\Type\ModelType;
 use Symfony\Component\Form\Test\TypeTestCase;
 use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\PropertyAccess\PropertyAccess;
 
 class ModelTypeTest extends TypeTestCase
 {
+    protected $type;
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        $this->type = new ModelType(PropertyAccess::createPropertyAccessor());
+    }
+
     public function testGetDefaultOptions()
     {
-        $type = new ModelType();
         $modelManager = $this->getMock('Sonata\AdminBundle\Model\ModelManagerInterface');
 
         $optionResolver = new OptionsResolver();
 
         if (!method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')) {
-            $type->setDefaultOptions($optionResolver);
+            $this->type->setDefaultOptions($optionResolver);
         } else {
-            $type->configureOptions($optionResolver);
+            $this->type->configureOptions($optionResolver);
         }
 
         $options = $optionResolver->resolve(array('model_manager' => $modelManager, 'choices' => array()));
@@ -58,14 +67,13 @@ class ModelTypeTest extends TypeTestCase
      */
     public function testCompoundOption($expectedCompound, $multiple, $expanded)
     {
-        $type = new ModelType();
         $modelManager = $this->getMock('Sonata\AdminBundle\Model\ModelManagerInterface');
         $optionResolver = new OptionsResolver();
 
         if (!method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')) {
-            $type->setDefaultOptions($optionResolver);
+            $this->type->setDefaultOptions($optionResolver);
         } else {
-            $type->configureOptions($optionResolver);
+            $this->type->configureOptions($optionResolver);
         }
 
         $options = $optionResolver->resolve(array('model_manager' => $modelManager, 'choices' => array(), 'multiple' => $multiple, 'expanded' => $expanded));

+ 8 - 3
Twig/Extension/SonataAdminExtension.php

@@ -17,7 +17,6 @@ use Sonata\AdminBundle\Admin\AdminInterface;
 use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
 use Sonata\AdminBundle\Admin\Pool;
 use Sonata\AdminBundle\Exception\NoValueException;
-use Symfony\Component\PropertyAccess\PropertyAccess;
 
 /**
  * Class SonataAdminExtension.
@@ -275,7 +274,13 @@ class SonataAdminExtension extends \Twig_Extension implements \Twig_Extension_In
 
         if (null === $propertyPath) {
             // For BC kept associated_tostring option behavior
-            $method = $fieldDescription->getOption('associated_tostring', '__toString');
+            $method = $fieldDescription->getOption('associated_tostring');
+
+            if ($method) {
+                @trigger_error('Option "associated_tostring" is deprecated since version 2.3. Use "associated_property" instead.', E_USER_DEPRECATED);
+            } else {
+                $method = '__toString';
+            }
 
             if (!method_exists($element, $method)) {
                 throw new \RuntimeException(sprintf(
@@ -293,7 +298,7 @@ class SonataAdminExtension extends \Twig_Extension implements \Twig_Extension_In
             return $propertyPath($element);
         }
 
-        return PropertyAccess::createPropertyAccessor()->getValue($element, $propertyPath);
+        return $this->pool->getPropertyAccessor()->getValue($element, $propertyPath);
     }
 
     /**