Skip to content

Commit

Permalink
working new item button for nested (> 2nd level)
Browse files Browse the repository at this point in the history
Basically same functionality as in
sonata-project#2985 but BC breaks fixed

fixed also to work with underscore model properties
  • Loading branch information
sakarikl committed Feb 22, 2016
1 parent ee5098f commit 3ae52b1
Show file tree
Hide file tree
Showing 2 changed files with 222 additions and 27 deletions.
190 changes: 163 additions & 27 deletions Admin/AdminHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
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.
Expand Down Expand Up @@ -113,42 +115,67 @@ public function appendFormFieldElement(AdminInterface $admin, $subject, $element
// 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);

Expand All @@ -169,7 +196,7 @@ public function appendFormFieldElement(AdminInterface $admin, $subject, $element
public function addNewInstance($object, FieldDescriptionInterface $fieldDescription)
{
$instance = $fieldDescription->getAssociationAdmin()->getNewInstance();
$mapping = $fieldDescription->getAssociationMapping();
$mapping = $fieldDescription->getAssociationMapping();

$method = sprintf('add%s', $this->camelize($mapping['fieldName']));

Expand Down Expand Up @@ -201,4 +228,113 @@ public function camelize($property)
{
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 changes: 59 additions & 0 deletions Tests/Admin/AdminHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,63 @@ public function testAddNewInstanceInflector()

$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');
}
}

0 comments on commit 3ae52b1

Please sign in to comment.