diff --git a/src/Form/Type/Helper/StructuralEntityChoiceLoader.php b/src/Form/Type/Helper/StructuralEntityChoiceLoader.php index 13c351e7..409b8837 100644 --- a/src/Form/Type/Helper/StructuralEntityChoiceLoader.php +++ b/src/Form/Type/Helper/StructuralEntityChoiceLoader.php @@ -22,6 +22,7 @@ namespace App\Form\Type\Helper; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractStructuralDBElement; +use App\Repository\StructuralDBElementRepository; use App\Services\Trees\NodesListBuilder; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Form\ChoiceList\Loader\AbstractChoiceLoader; @@ -33,6 +34,8 @@ class StructuralEntityChoiceLoader extends AbstractChoiceLoader private NodesListBuilder $builder; private EntityManagerInterface $entityManager; + private ?string $additional_element = null; + public function __construct(Options $options, NodesListBuilder $builder, EntityManagerInterface $entityManager) { $this->options = $options; @@ -42,10 +45,16 @@ class StructuralEntityChoiceLoader extends AbstractChoiceLoader 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; + } + + return array_merge($tmp, $this->builder->typeToNodesList($this->options['class'], null)); } - public function loadChoicesForValues(array $values, callable $value = null) + /*public function loadChoicesForValues(array $values, callable $value = null) { $tmp = parent::loadChoicesForValues($values, $value); @@ -59,29 +68,13 @@ class StructuralEntityChoiceLoader extends AbstractChoiceLoader return $tmp; } - return [$this->createNewEntityFromValue((string)$values[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 [$this->createNewEntitiesFromValue($values[0])[0]]; } return $tmp; }*/ - - public function createNewEntityFromValue(string $value): AbstractStructuralDBElement + public function createNewEntitiesFromValue(string $value): array { if (!$this->options['allow_add']) { 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']; - /** @var AbstractStructuralDBElement $entity */ - $entity = new $class(); - $entity->setName($value); + /** @var StructuralDBElementRepository $repo */ + $repo = $this->entityManager->getRepository($class); - //Persist element to database - $this->entityManager->persist($entity); + $entities = $repo->getNewEntityFromPath($value, '->'); - return $entity; + $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); + $results[] = $entity; + } + } + + return $results; } + + public function setAdditionalElement(?string $element): void + { + $this->additional_element = $element; + } + + public function getAdditionalElement(): ?string + { + return $this->additional_element; + } + } \ No newline at end of file diff --git a/src/Form/Type/StructuralEntityType.php b/src/Form/Type/StructuralEntityType.php index f03a3af3..1b46ab14 100644 --- a/src/Form/Type/StructuralEntityType.php +++ b/src/Form/Type/StructuralEntityType.php @@ -31,9 +31,12 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\CallbackTransformer; 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\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\Options; @@ -65,12 +68,44 @@ class StructuralEntityType extends AbstractType 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( - function ($value) use ($options) { - return $this->modelTransform($value, $options); - }, function ($value) use ($options) { - return $this->modelReverseTransform($value, $options); - })); + function ($value) use ($options) { + return $this->modelTransform($value, $options); + }, function ($value) use ($options) { + return $this->modelReverseTransform($value, $options); + })); } public function configureOptions(OptionsResolver $resolver): void @@ -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 'subentities_of' => null, //Only show entities with the given parent class '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) { return new StructuralEntityChoiceLoader($options, $this->builder, $this->em); }, @@ -95,6 +141,15 @@ class StructuralEntityType extends AbstractType 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 ]); diff --git a/src/Repository/StructuralDBElementRepository.php b/src/Repository/StructuralDBElementRepository.php index 52ab4591..2f8a656a 100644 --- a/src/Repository/StructuralDBElementRepository.php +++ b/src/Repository/StructuralDBElementRepository.php @@ -89,4 +89,38 @@ class StructuralDBElementRepository extends NamedDBElementRepository 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; + } }