Allow to create nested entitiy structures directly from the part edit page.

However there is still a bug, that the newly created entites are not shown as selected (even though they are). Fixes issue #203
This commit is contained in:
Jan Böhmer 2023-01-30 00:37:12 +01:00
parent 1654010ea3
commit e0c380d81a
3 changed files with 133 additions and 32 deletions

View file

@ -22,6 +22,7 @@ namespace App\Form\Type\Helper;
use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Base\AbstractStructuralDBElement;
use App\Repository\StructuralDBElementRepository;
use App\Services\Trees\NodesListBuilder; use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\ChoiceList\Loader\AbstractChoiceLoader; use Symfony\Component\Form\ChoiceList\Loader\AbstractChoiceLoader;
@ -33,6 +34,8 @@ class StructuralEntityChoiceLoader extends AbstractChoiceLoader
private NodesListBuilder $builder; private NodesListBuilder $builder;
private EntityManagerInterface $entityManager; private EntityManagerInterface $entityManager;
private ?string $additional_element = null;
public function __construct(Options $options, NodesListBuilder $builder, EntityManagerInterface $entityManager) public function __construct(Options $options, NodesListBuilder $builder, EntityManagerInterface $entityManager)
{ {
$this->options = $options; $this->options = $options;
@ -42,10 +45,16 @@ class StructuralEntityChoiceLoader extends AbstractChoiceLoader
protected function loadChoices(): iterable protected function loadChoices(): iterable
{ {
return $this->builder->typeToNodesList($this->options['class'], null); $tmp = [];
if ($this->additional_element) {
$tmp = $this->createNewEntitiesFromValue($this->additional_element);
$this->additional_element = null;
} }
public function loadChoicesForValues(array $values, callable $value = null) return array_merge($tmp, $this->builder->typeToNodesList($this->options['class'], null));
}
/*public function loadChoicesForValues(array $values, callable $value = null)
{ {
$tmp = parent::loadChoicesForValues($values, $value); $tmp = parent::loadChoicesForValues($values, $value);
@ -59,29 +68,13 @@ class StructuralEntityChoiceLoader extends AbstractChoiceLoader
return $tmp; return $tmp;
} }
return [$this->createNewEntityFromValue((string)$values[0])]; return [$this->createNewEntitiesFromValue($values[0])[0]];
}
return $tmp;
}
/*public function loadValuesForChoices(array $choices, callable $value = null)
{
$tmp = parent::loadValuesForChoices($choices, $value);
if ($this->options['allow_add'] && count($tmp) === 1) {
if ($tmp[0] instanceof AbstractDBElement && $tmp[0]->getID() === null) {
return [$tmp[0]->getName()];
}
return [(string)$choices[0]->getID()];
} }
return $tmp; return $tmp;
}*/ }*/
public function createNewEntitiesFromValue(string $value): array
public function createNewEntityFromValue(string $value): AbstractStructuralDBElement
{ {
if (!$this->options['allow_add']) { if (!$this->options['allow_add']) {
throw new \RuntimeException('Cannot create new entity, because allow_add is not enabled!'); throw new \RuntimeException('Cannot create new entity, because allow_add is not enabled!');
@ -92,13 +85,32 @@ class StructuralEntityChoiceLoader extends AbstractChoiceLoader
} }
$class = $this->options['class']; $class = $this->options['class'];
/** @var AbstractStructuralDBElement $entity */ /** @var StructuralDBElementRepository $repo */
$entity = new $class(); $repo = $this->entityManager->getRepository($class);
$entity->setName($value);
//Persist element to database $entities = $repo->getNewEntityFromPath($value, '->');
$results = [];
foreach($entities as $entity) {
//If the entity is newly created (ID null), add it as result and persist it.
if ($entity->getID() === null) {
$this->entityManager->persist($entity); $this->entityManager->persist($entity);
$results[] = $entity;
}
}
return $entity; return $results;
} }
public function setAdditionalElement(?string $element): void
{
$this->additional_element = $element;
}
public function getAdditionalElement(): ?string
{
return $this->additional_element;
}
} }

View file

@ -31,9 +31,12 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader; use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
use Symfony\Component\Form\Event\PostSubmitEvent;
use Symfony\Component\Form\Event\PreSubmitEvent;
use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView; use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\Options;
@ -65,6 +68,38 @@ class StructuralEntityType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event) {
//When the data contains non-digit characters, we assume that the user entered a new element.
//In that case we add the new element to our choice_loader
$data = $event->getData();
if (null === $data || !is_string($data) || $data === "" || ctype_digit($data)) {
return;
}
$form = $event->getForm();
$options = $form->getConfig()->getOptions();
$choice_loader = $options['choice_loader'];
if ($choice_loader instanceof StructuralEntityChoiceLoader) {
$choice_loader->setAdditionalElement($data);
}
});
/* $builder->addEventListener(FormEvents::POST_SUBMIT, function (PostSubmitEvent $event) {
$name = $event->getForm()->getConfig()->getName();
$data = $event->getForm()->getData();
if ($event->getForm()->getParent() === null) {
return;
}
$event->getForm()->getParent()->add($name, static::class, $event->getForm()->getConfig()->getOptions());
$new_form = $event->getForm()->getParent()->get($name);
$new_form->setData($data);
});*/
$builder->addModelTransformer(new CallbackTransformer( $builder->addModelTransformer(new CallbackTransformer(
function ($value) use ($options) { function ($value) use ($options) {
return $this->modelTransform($value, $options); return $this->modelTransform($value, $options);
@ -81,7 +116,18 @@ class StructuralEntityType extends AbstractType
'show_fullpath_in_subtext' => true, //When this is enabled, the full path will be shown in subtext 'show_fullpath_in_subtext' => true, //When this is enabled, the full path will be shown in subtext
'subentities_of' => null, //Only show entities with the given parent class 'subentities_of' => null, //Only show entities with the given parent class
'disable_not_selectable' => false, //Disable entries with not selectable property 'disable_not_selectable' => false, //Disable entries with not selectable property
'choice_value' => 'id', //Use the element id as option value and for comparing items 'choice_value' => function (?AbstractStructuralDBElement $element) {
if ($element === null) {
return null;
}
if ($element->getID() === null) {
//Must be the same as the separator in the choice_loader, otherwise this will not work!
return $element->getFullPath('->');
}
return $element->getID();
}, //Use the element id as option value and for comparing items
'choice_loader' => function (Options $options) { 'choice_loader' => function (Options $options) {
return new StructuralEntityChoiceLoader($options, $this->builder, $this->em); return new StructuralEntityChoiceLoader($options, $this->builder, $this->em);
}, },
@ -95,6 +141,15 @@ class StructuralEntityType extends AbstractType
return $this->generateChoiceAttr($choice, $key, $value, $options); return $this->generateChoiceAttr($choice, $key, $value, $options);
}; };
}, },
'group_by' => function (AbstractStructuralDBElement $element)
{
//Show entities that are not added to DB yet separately from other entities
if ($element->getID() === null) {
return 'New (not added to DB yet)';
}
return null;
},
'choice_translation_domain' => false, //Don't translate the entity names 'choice_translation_domain' => false, //Don't translate the entity names
]); ]);

View file

@ -89,4 +89,38 @@ class StructuralDBElementRepository extends NamedDBElementRepository
return $result; return $result;
} }
/**
* Creates a structure of AbsstractStructuralDBElements from a path separated by $separator, which splits the various levels.
* This function will try to use existing elements, if they are already in the database. If not, they will be created.
* An array of the created elements will be returned, with the last element being the deepest element.
* @param string $path
* @param string $separator
* @return AbstractStructuralDBElement[]
*/
public function getNewEntityFromPath(string $path, string $separator = '->'): array
{
$parent = null;
$result = [];
foreach (explode($separator, $path) as $name) {
$name = trim($name);
if ('' === $name) {
continue;
}
//See if we already have an element with this name and parent
$entity = $this->findOneBy(['name' => $name, 'parent' => $parent]);
if (null === $entity) {
/** @var AbstractStructuralDBElement $entity */
$entity = new ($this->getClassName());
$entity->setName($name);
$entity->setParent($parent);
}
$result[] = $entity;
$parent = $entity;
}
return $result;
}
} }