From c7e8fc9642300975d8ced1d1975a32f2c3a72d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Wed, 21 Aug 2019 00:46:45 +0200 Subject: [PATCH] Fixed exception about non-persisted entities, when using the cached StructuralEntityType. --- .../AdminPages/BaseAdminController.php | 3 + src/Form/Part/PartLotType.php | 6 +- src/Form/Type/StructuralEntityType.php | 110 +++++++++++++++++- 3 files changed, 115 insertions(+), 4 deletions(-) diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index 2b48c0a6..5e524f4d 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -72,6 +72,9 @@ abstract class BaseAdminController extends AbstractController $em->flush(); } + //Rebuild form, so it is based on the updated data. Important for the parent field! + $form = $this->createForm($this->form_class, $entity); + return $this->render($this->twig_template, [ 'entity' => $entity, 'form' => $form->createView() diff --git a/src/Form/Part/PartLotType.php b/src/Form/Part/PartLotType.php index d012f7f0..a4c1ccc0 100644 --- a/src/Form/Part/PartLotType.php +++ b/src/Form/Part/PartLotType.php @@ -37,12 +37,15 @@ use App\Entity\Parts\PartLot; use App\Entity\Parts\Storelocation; use App\Form\Type\StructuralEntityType; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\DataMapperInterface; +use Symfony\Component\Form\Exception; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use function GuzzleHttp\Promise\queue; @@ -55,7 +58,8 @@ class PartLotType extends AbstractType $builder->add('storage_location', StructuralEntityType::class, ['class' => Storelocation::class, 'label' => 'part_lot.edit.location', - 'disable_not_selectable' => true, 'attr' => ['class' => 'form-control-sm']]); + 'disable_not_selectable' => true, + 'attr' => ['class' => 'selectpicker form-control-sm', 'data-live-search' => true]]); $builder->add('amount',NumberType::class, [ 'html5' => true, 'label' => 'part_lot.edit.amount', diff --git a/src/Form/Type/StructuralEntityType.php b/src/Form/Type/StructuralEntityType.php index c2b26517..451b5f0d 100644 --- a/src/Form/Type/StructuralEntityType.php +++ b/src/Form/Type/StructuralEntityType.php @@ -38,13 +38,19 @@ use App\Repository\StructuralDBElementRepository; use App\Services\TreeBuilder; 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\DataMapperInterface; +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception; +use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; +use function foo\func; /** * This class provides a choice form type similar to EntityType, with the difference, that the tree structure @@ -64,10 +70,19 @@ class StructuralEntityType extends AbstractType $this->builder = $builder; } + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addModelTransformer(new CallbackTransformer( + function ($value) use ($options){ + return $this->transform($value, $options); + }, function ($value) use ($options) { + return $this->reverseTransform($value, $options); + })); + } + public function configureOptions(OptionsResolver $resolver) { $resolver->setRequired(['class']); - $resolver->setDefaults(['attr' => ['class' => 'selectpicker', 'data-live-search' => true], '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 @@ -128,7 +143,6 @@ class StructuralEntityType extends AbstractType $choices = $this->builder->typeToNodesList($options['class'], null); - /** @var StructuralDBElementRepository $repo */ /*$repo = $this->em->getRepository($options['class']); $choices = $repo->toNodesList(null); */ @@ -142,11 +156,101 @@ class StructuralEntityType extends AbstractType //See extendedBootstrap4_layout.html.twig for that... $view->vars['use_html_in_labels'] = true; - parent::buildView($view, $form, $options); // TODO: Change the autogenerated stub + parent::buildView($view, $form, $options); } public function getParent() { return ChoiceType::class; } + + /** + * Transforms a value from the original representation to a transformed representation. + * + * This method is called when the form field is initialized with its default data, on + * two occasions for two types of transformers: + * + * 1. Model transformers which normalize the model data. + * This is mainly useful when the same form type (the same configuration) + * has to handle different kind of underlying data, e.g The DateType can + * deal with strings or \DateTime objects as input. + * + * 2. View transformers which adapt the normalized data to the view format. + * a/ When the form is simple, the value returned by convention is used + * directly in the view and thus can only be a string or an array. In + * this case the data class should be null. + * + * b/ When the form is compound the returned value should be an array or + * an object to be mapped to the children. Each property of the compound + * data will be used as model data by each child and will be transformed + * too. In this case data class should be the class of the object, or null + * when it is an array. + * + * All transformers are called in a configured order from model data to view value. + * At the end of this chain the view data will be validated against the data class + * setting. + * + * This method must be able to deal with empty values. Usually this will + * be NULL, but depending on your implementation other empty values are + * possible as well (such as empty strings). The reasoning behind this is + * that data transformers must be chainable. If the transform() method + * of the first data transformer outputs NULL, the second must be able to + * process that value. + * + * @param mixed $value The value in the original representation + * + * @return mixed The value in the transformed representation + * + * @throws TransformationFailedException when the transformation fails + */ + public function transform($value, $options) + { + return $value; + } + + /** + * Transforms a value from the transformed representation to its original + * representation. + * + * This method is called when {@link Form::submit()} is called to transform the requests tainted data + * into an acceptable format. + * + * The same transformers are called in the reverse order so the responsibility is to + * return one of the types that would be expected as input of transform(). + * + * This method must be able to deal with empty values. Usually this will + * be an empty string, but depending on your implementation other empty + * values are possible as well (such as NULL). The reasoning behind + * this is that value transformers must be chainable. If the + * reverseTransform() method of the first value transformer outputs an + * empty string, the second value transformer must be able to process that + * value. + * + * By convention, reverseTransform() should return NULL if an empty string + * is passed. + * + * @param mixed $value The value in the transformed representation + * + * @return mixed The value in the original representation + * + * @throws TransformationFailedException when the transformation fails + */ + public function reverseTransform($value, $options) + { + /* This step is important in combination with the caching! + The elements deserialized from cache, are not known to Doctrinte ORM any more, so doctrine thinks, + that the entity has changed (and so throws an exception about non-persited entities). + This function just retrieves a fresh copy of the entity from database, so doctrine detect correctly that no + change happened. + The performance impact of this should be very small in comparison of the boost, caused by the caching. + */ + + if ($value === null) { + return null; + } + + $entity = $this->em->find($options['class'], $value->getID()); + + return $entity; + } } \ No newline at end of file