Fixed exception about non-persisted entities, when using the cached StructuralEntityType.

This commit is contained in:
Jan Böhmer 2019-08-21 00:46:45 +02:00
parent 7608d5dbda
commit c7e8fc9642
3 changed files with 115 additions and 4 deletions

View file

@ -72,6 +72,9 @@ abstract class BaseAdminController extends AbstractController
$em->flush(); $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, [ return $this->render($this->twig_template, [
'entity' => $entity, 'entity' => $entity,
'form' => $form->createView() 'form' => $form->createView()

View file

@ -37,12 +37,15 @@ use App\Entity\Parts\PartLot;
use App\Entity\Parts\Storelocation; use App\Entity\Parts\Storelocation;
use App\Form\Type\StructuralEntityType; use App\Form\Type\StructuralEntityType;
use Symfony\Component\Form\AbstractType; 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\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use function GuzzleHttp\Promise\queue; use function GuzzleHttp\Promise\queue;
@ -55,7 +58,8 @@ class PartLotType extends AbstractType
$builder->add('storage_location', StructuralEntityType::class, ['class' => Storelocation::class, $builder->add('storage_location', StructuralEntityType::class, ['class' => Storelocation::class,
'label' => 'part_lot.edit.location', '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, $builder->add('amount',NumberType::class, [ 'html5' => true,
'label' => 'part_lot.edit.amount', 'label' => 'part_lot.edit.amount',

View file

@ -38,13 +38,19 @@ use App\Repository\StructuralDBElementRepository;
use App\Services\TreeBuilder; use App\Services\TreeBuilder;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader; 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\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
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;
use Symfony\Component\OptionsResolver\OptionsResolver; 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 * 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; $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) public function configureOptions(OptionsResolver $resolver)
{ {
$resolver->setRequired(['class']); $resolver->setRequired(['class']);
$resolver->setDefaults(['attr' => ['class' => 'selectpicker', 'data-live-search' => true], $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 '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
@ -128,7 +143,6 @@ class StructuralEntityType extends AbstractType
$choices = $this->builder->typeToNodesList($options['class'], null); $choices = $this->builder->typeToNodesList($options['class'], null);
/** @var StructuralDBElementRepository $repo */ /** @var StructuralDBElementRepository $repo */
/*$repo = $this->em->getRepository($options['class']); /*$repo = $this->em->getRepository($options['class']);
$choices = $repo->toNodesList(null); */ $choices = $repo->toNodesList(null); */
@ -142,11 +156,101 @@ class StructuralEntityType extends AbstractType
//See extendedBootstrap4_layout.html.twig for that... //See extendedBootstrap4_layout.html.twig for that...
$view->vars['use_html_in_labels'] = true; $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() public function getParent()
{ {
return ChoiceType::class; 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;
}
} }