瀏覽代碼

working new item button for nested (> 2nd level)

Basically same functionality as in
https://github.com/sonata-project/SonataAdminBundle/pull/2985 but BC breaks fixed

fixed also to work with underscore model properties
sakarikl 9 年之前
父節點
當前提交
3ae52b1933
共有 2 個文件被更改,包括 222 次插入27 次删除
  1. 163 27
      Admin/AdminHelper.php
  2. 59 0
      Tests/Admin/AdminHelperTest.php

+ 163 - 27
Admin/AdminHelper.php

@@ -18,6 +18,8 @@ use Sonata\AdminBundle\Util\FormBuilderIterator;
 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;
 
 /**
  * Class AdminHelper.
@@ -113,42 +115,67 @@ class AdminHelper
         // get the field element
         $childFormBuilder = $this->getChildFormBuilder($formBuilder, $elementId);
 
-        // retrieve the FieldDescription
-        $fieldDescription = $admin->getFormFieldDescription($childFormBuilder->getName());
+        //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();
+            $entity = $admin->getSubject();
 
-        try {
-            $value = $fieldDescription->getValue($form->getData());
-        } catch (NoValueException $e) {
-            $value = null;
-        }
+            $path = $this->getElementAccessPath($elementId, $entity);
 
-        // retrieve the posted data
-        $data = $admin->getRequest()->get($formBuilder->getName());
+            $collection = $propertyAccessor->getValue($entity, $path);
 
-        if (!isset($data[$childFormBuilder->getName()])) {
-            $data[$childFormBuilder->getName()] = array();
-        }
+            if ($collection instanceof \Doctrine\ORM\PersistentCollection || $collection instanceof \Doctrine\ODM\MongoDB\PersistentCollection) {
+                //since doctrine 2.4
+                $entityClassName = $collection->getTypeClass()->getName();
+            } elseif ($collection instanceof \Doctrine\Common\Collections\Collection) {
+                $entityClassName = $this->getEntityClassName($admin, explode('.', preg_replace('#\[\d*?\]#', '', $path)));
+            } else {
+                throw new \Exception('unknown collection class');
+            }
 
-        $objectCount = count($value);
-        $postCount   = count($data[$childFormBuilder->getName()]);
+            $collection->add(new $entityClassName());
+            $propertyAccessor->setValue($entity, $path, $collection);
 
-        $fields = array_keys($fieldDescription->getAssociationAdmin()->getFormFieldDescriptions());
+            $fieldDescription = null;
+        } else {
+            // retrieve the FieldDescription
+            $fieldDescription = $admin->getFormFieldDescription($childFormBuilder->getName());
 
-        // for now, not sure how to do that
-        $value = array();
-        foreach ($fields as $name) {
-            $value[$name] = '';
-        }
+            try {
+                $value = $fieldDescription->getValue($form->getData());
+            } catch (NoValueException $e) {
+                $value = null;
+            }
+
+            // retrieve the posted data
+            $data = $admin->getRequest()->get($formBuilder->getName());
+
+            if (!isset($data[$childFormBuilder->getName()])) {
+                $data[$childFormBuilder->getName()] = array();
+            }
+
+            $objectCount = count($value);
+            $postCount = count($data[$childFormBuilder->getName()]);
+
+            $fields = array_keys($fieldDescription->getAssociationAdmin()->getFormFieldDescriptions());
+
+            // for now, not sure how to do that
+            $value = array();
+            foreach ($fields as $name) {
+                $value[$name] = '';
+            }
+
+            // add new elements to the subject
+            while ($objectCount < $postCount) {
+                // append a new instance into the object
+                $this->addNewInstance($form->getData(), $fieldDescription);
+                ++$objectCount;
+            }
 
-        // add new elements to the subject
-        while ($objectCount < $postCount) {
-            // append a new instance into the object
             $this->addNewInstance($form->getData(), $fieldDescription);
-            ++$objectCount;
         }
 
-        $this->addNewInstance($form->getData(), $fieldDescription);
-
         $finalForm = $admin->getFormBuilder()->getForm();
         $finalForm->setData($subject);
 
@@ -169,7 +196,7 @@ class AdminHelper
     public function addNewInstance($object, FieldDescriptionInterface $fieldDescription)
     {
         $instance = $fieldDescription->getAssociationAdmin()->getNewInstance();
-        $mapping  = $fieldDescription->getAssociationMapping();
+        $mapping = $fieldDescription->getAssociationMapping();
 
         $method = sprintf('add%s', $this->camelize($mapping['fieldName']));
 
@@ -201,4 +228,113 @@ class AdminHelper
     {
         return BaseFieldDescription::camelize($property);
     }
+
+    /**
+     * Recursively find the class name of the admin responsible for the element at the end of an association chain.
+     *
+     * @param AdminInterface $admin
+     * @param array          $elements
+     *
+     * @return string
+     */
+    protected function getEntityClassName(AdminInterface $admin, $elements)
+    {
+        $element = array_shift($elements);
+        $associationAdmin = $admin->getFormFieldDescription($element)->getAssociationAdmin();
+        if (count($elements) == 0) {
+            return $associationAdmin->getClass();
+        } else {
+            return $this->getEntityClassName($associationAdmin, $elements);
+        }
+    }
+
+    /**
+     * 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
+     *
+     * @return string
+     *
+     * @throws \Exception
+     */
+    public function getElementAccessPath($elementId, $entity)
+    {
+        $propertyAccessor = new PropertyAccessor();
+
+        $idWithoutUniqueIdentifier = implode('_', explode('_', substr($elementId, strpos($elementId, '_') + 1)));
+
+        //array access of id converted to format which PropertyAccessor understands
+        $initialPath = preg_replace('#(_(\d+)_)#', '[$2]', $idWithoutUniqueIdentifier);
+
+        $parts = preg_split('#\[\d+\]#', $initialPath);
+
+        $partReturnValue = $returnValue = '';
+        $currentEntity = $entity;
+
+        foreach ($parts as $key => $value) {
+            $subParts = explode('_', $value);
+            $id = '';
+            $dot = '';
+
+            foreach ($subParts as $subValue) {
+                $id .= ($id) ? '_'.$subValue : $subValue;
+
+                if ($this->pathExists($propertyAccessor, $currentEntity, $partReturnValue.$dot.$id)) {
+                    $partReturnValue .= $dot.$id;
+                    $dot = '.';
+                    $id = '';
+                } else {
+                    $dot = '';
+                }
+            }
+
+            if ($dot !== '.') {
+                throw new \Exception(sprintf('Could not get element id from %s Failing part: %s', $elementId, $subValue));
+            }
+
+            //check if array access was in this location originally
+            preg_match("#$value\[(\d+)#", $initialPath, $matches);
+
+            if (isset($matches[1])) {
+                $partReturnValue .= '['.$matches[1].']';
+            }
+
+            $returnValue .= $returnValue ? '.'.$partReturnValue : $partReturnValue;
+            $partReturnValue = '';
+
+            if (isset($parts[$key + 1])) {
+                $currentEntity = $propertyAccessor->getValue($entity, $returnValue);
+            }
+        }
+
+        return $returnValue;
+    }
+
+    /**
+     * check if given path exists in $entity.
+     *
+     * @param PropertyAccessor $propertyAccessor
+     * @param mixed            $entity
+     * @param string           $path
+     *
+     * @return bool
+     *
+     * @throws \RuntimeException
+     */
+    private function pathExists(PropertyAccessor $propertyAccessor, $entity, $path)
+    {
+        //sf2 <= 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;
+            }
+        }
+    }
 }

+ 59 - 0
Tests/Admin/AdminHelperTest.php

@@ -112,4 +112,63 @@ class AdminHelperTest extends \PHPUnit_Framework_TestCase
 
         $helper->addNewInstance($object, $fieldDescription);
     }
+
+    public function testGetElementAccessPath()
+    {
+        $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
+
+        $pool = new Pool($container, 'title', 'logo.png');
+        $helper = new AdminHelper($pool);
+
+        $object = $this->getMock('stdClass', array('getPathToObject'));
+        $subObject = $this->getMock('stdClass', array('getAnd'));
+        $sub2Object = $this->getMock('stdClass', array('getMore'));
+
+        $object->expects($this->atLeastOnce())->method('getPathToObject')->will($this->returnValue(array($subObject)));
+        $subObject->expects($this->atLeastOnce())->method('getAnd')->will($this->returnValue($sub2Object));
+        $sub2Object->expects($this->atLeastOnce())->method('getMore')->will($this->returnValue('Value'));
+
+        $path = $helper->getElementAccessPath('uniquePartOfId_path_to_object_0_and_more', $object);
+
+        $this->assertSame('path_to_object[0].and.more', $path);
+    }
+
+    /**
+     * tests only so far that actual value/object is retrieved.
+     *
+     * @expectedException        Exception
+     * @expectedExceptionCode    0
+     * @expectedExceptionMessage unknown collection class
+     */
+    public function testAppendFormFieldElementNested()
+    {
+        $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
+        $pool = new Pool($container, 'title', 'logo.png');
+        $helper = new AdminHelper($pool);
+        $admin = $this->getMock('Sonata\AdminBundle\Admin\AdminInterface');
+        $object = $this->getMock('stdClass', array('getSubObject'));
+        $simpleObject = $this->getMock('stdClass', array('getSubObject'));
+        $subObject = $this->getMock('stdClass', array('getAnd'));
+        $sub2Object = $this->getMock('stdClass', array('getMore'));
+        $sub3Object = $this->getMock('stdClass', array('getFinalData'));
+        $dataMapper = $this->getMock('Symfony\Component\Form\DataMapperInterface');
+        $formFactory = $this->getMock('Symfony\Component\Form\FormFactoryInterface');
+        $eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+        $formBuilder = new FormBuilder('test', get_class($simpleObject), $eventDispatcher, $formFactory);
+        $childFormBuilder = new FormBuilder('subObject', get_class($subObject), $eventDispatcher, $formFactory);
+
+        $object->expects($this->atLeastOnce())->method('getSubObject')->will($this->returnValue(array($subObject)));
+        $subObject->expects($this->atLeastOnce())->method('getAnd')->will($this->returnValue($sub2Object));
+        $sub2Object->expects($this->atLeastOnce())->method('getMore')->will($this->returnValue(array($sub3Object)));
+        $sub3Object->expects($this->atLeastOnce())->method('getFinalData')->will($this->returnValue('value'));
+
+        $formBuilder->setCompound(true);
+        $formBuilder->setDataMapper($dataMapper);
+        $formBuilder->add($childFormBuilder);
+
+        $admin->expects($this->once())->method('getFormBuilder')->will($this->returnValue($formBuilder));
+        $admin->expects($this->once())->method('getSubject')->will($this->returnValue($object));
+
+        $helper->appendFormFieldElement($admin, $simpleObject, 'uniquePartOfId_sub_object_0_and_more_0_final_data');
+    }
 }