Goal
Custom Formelement written as an Extension of the (Doctrine) Entity Field. The Element is rendered as a Select2 Element with Tagging. This allows to choose multiple items from a list or add a new value. If a new value is added, a new entity with the value as the property shall be created and associated to the underlying model of the formType.
What works
I can successfully render the select2 Element with the available entities.
Problem
The submitted value is a string containing the (unique) selected properties which currently causes a validation error (invalid value). It doesn't deliever the ID of the selected entities as the original field does, so I wrote a generic DataTransformer that should be able to deal with it. However I am unable to attach the transformer to the field as intended from the context of the TypeExtension. I would have to add the transformer inside my form. As another option I considered is to suppress the validation Listener enitrely, but this would not only disable the validation of the enitre form, it wouldn't be helpful in correctly mapping the selected values to the enitities.
What's the best way to implement this functionality? As the entity FieldType already offers most of the functionality, I would rather try to avoid to write a completly new fieldType.
Update: Why I can't attach the transformer
One option would of course be to do it just the way the cookbook entry suggests, that is to attach the transformer whenever I use the field:
class MyFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ...
$entityManager = $options['em'];
$transformer = new ObjectListToStringTransformer($this->em,
[
'class' => $options['class'],
'property' => $options['property'],
'delimiter' => ','
]
);
$builder->add(
$builder->create('entityType', 'entity',
array(
'select2' => true, /*Triggers the select2 template to be used*/
'multiple' => true,
'class' => 'Bundle:Entity',
'property' => 'name'
)
)->addModelTransformer($transformer)
);
}
// ....
}
That could work. But this is not verbose and not typesafe. If I set select2 => true the Transformer must be added otherwise it will not validate. So I'd really like to have that done within my Extension. My first approach to achieve this was simply wrong, just the way the cookbook entry mentioned "how NOT to do it":
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['select2']) {
$builder->addModelTransformer(new ObjectListToStringTransformer($this->em,
[
'class' => $options['class'],
'property' => $options['property'],
'delimiter' => ','
]
)
);
}
parent::buildForm($builder, $options);
}
This will add a transformer to the entire form, not only this field. AFAIK I can't do create(...)->addModelTransformer($transformer) as I probably could if it was a sub class (vertical inheritance) but not inside the extension (horizontal inheritance). Is there a way to add it after the field was already added? Or is there a way to achieve this by creating a subclass of the choice field?
Apparently the entity field itself has a registered ModelTransformer (or a ViewTransformer) which get called before my ModelTransformer. Since this always fails, my Transformer is never reached. To get around this, I changed addModelTransformer to addViewTransformer. While the code of my transformer wasn't working yet, the interaction with it does.
Related
I am having an issue with the EntityType class in Symfony 5 where I cannot find a way to make the form row return a string instead of an entity (MyEntity). This is what I basically have as part of the form builder:
$builder->add('myEntity', EntityType::class, [
'class' => MyEntity::class,
'placeholder' => "(Please select an option.)",
'choice_label' => 'name'
])
My aim is to have this field return the name for the MyEntity selected, not the entire entity (so, the exact same value as the 'choice_label'). I've already tried adding a 'choice_value' => 'name' attribute (with a 'mapped' => false attribute, which is apparently necessary in that case), but to no avail. More precisely, the form row starts returning null rather than a string for any entity.
How would I be able to achieve my goal?
It turns out that defining the __toString() function in MyEntity as returning the name field (the exact same way you would define getName()) fixed it, and I now get no errors plus a successful submission to the DB.
public function __toString() {
return $this->name;
}
(In case you're wondering why just this addition and nothing else fixes it, it's because __toString is already being used by default in some pre-existing entity-related code to express an entity in a simpler and custom way, if I'm not mistaken. Now that I made this function available, it's going to go to it and safely assume that when my entity might need to be expressed in a simpler [string] format, __toString should be called to "transform" MyEntity into a meaningful string, such as its name. I guess all that is to say that for instance, you won't refer to a car by all of its internal parts, but rather by just its model.)
I'm glad you found the right solution, but it didn't work for me.
I use the EntityType in its own Type which is called in a CollectionType (yes, something special).
To solve the problem for me, I used the DataTransformer, as follows:
class ProductTextTransformer implements DataTransformerInterface
{
public function reverseTransform($text)
{
if (null === $text) {
return '';
}
return $text->getName();
}
public function transform($name)
{
// NOTE: Should not Called!
}
}
I have two kind of entities : "Affaire" and "Pointage". My Affaire Entity has a OneToMany relation with my Pointage Entity :
//App\Entity\Affaire.php
/**
* #ORM\OneToMany(targetEntity="App\Entity\Pointage", mappedBy="numAffaire")
*/
private $pointages;
//App\Entity\Pointage.php
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Affaires", inversedBy="pointages")
* #ORM\JoinColumn(nullable=false)
*/
private $numAffaire;
I created a form for my Affaire Entity which get "Pointages" related to a "Affaire".
//App\Form\AffaireType.php
class AffaireType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('numAffaire');
$builder->add('descAffaire');
$builder->add('pointages', CollectionType::class, [
'entry_type' => PointageType::class,
'entry_options' => ['label' => false, 'pointages' => $options['pointages']],
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired(['pointages']);
$resolver->setDefaults([
'data_class' => Affaires::class,
]);
}
}
//App\Form\PointageType.php
class PointageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('heurePointage');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired(['pointages']);
$resolver->setDefaults([
'data_class' => Pointage::class,
]);
}
}
My question is : how can I filter the "Pointages" in my AffaireType class ? For example, I want to get all the pointages associated to my Affaire where the id of my pointages is less than 100. Is it possible to make a query somewhere ?
!!! I would strongly advise against what you're doing there !!!
Reason:
Your form type's data class is set to an entity (as far as I can tell). And the form is designed to modify the object it is given (your entity). Now, if you filter a field for that form type, and the form then tries to set the "new" values, all Pointage objects that are not in the form submission (specifically all those that were filtered out) might be removed. You will lose data, if the form calls setPointages - I'm not quite certain, if this is always called, even despite allow_add/allow_remove set to false. If you ever forget, this can make your life miserable. And I believe this is always called, for the standard CollectionType.
update after some code reading: regarding the calling of setPointages: if addPointage/removePointage exist, they will be called. However, the previous value is read by the PropertyAccessor (property-access component) from the target object(!), and the difference will be calculated and add/remove called accordingly -> removing any entities not in the new collection
Possible workarounds
Make heavy use of data mapper / data transformers / form events to somehow hide the fact that there are more Pointage entities. this might work very well, but it complicates stuff a lot but still might become a clean solution.
the general idea is:
form render: get from entity(affaire, all pointages) -> your transformer/mapper/form event handler (affaire, all -> filtered pointages) -> form (affaire, filtered pointages)
form submit: form (affaire, filtered pointages) -> your transformer/mapper/form event handler (affaire, filtered -> "new all" pointages) -> set on entity (affaire, all pointages)
however, you probably have to spend a lot of time understanding the internals of the form component to do this correctly and safely ... (one pointer would be the MergeCollectionListener, which you might be able to adapt to your needs, you might also take a look at the ResizeFormListener, which adds and remove sub forms depending on the given data. remember if you filter your data, you should probably create your own collection type and add a new form listener that handles everything gracefully)
instead of integrating this into the AffaireType, add another form type AffairePointageType with two fields: affaire (AffaireType without pointages) and pointages (CollectionType) and call it like
$filteredPointages = someFilterFunction($affaire->getPointages());
$this->createForm(AffairePointageType::class, [
'affaire' => $affaire,
'pointages' => $filteredPointages,
], [
'pointages' => ... //the option you provide for the CollectionType
]);
where you obviously can provide any pointages, filtered by any filter you like and will only edit those you provide, leaving anything else associated with $affaire untouched. Note, however, that you have to take care of added or removed pointages yourself. (this might break separation of concerns though)
essentially, this is an easier to understand version of the previous workaround, where all filter logic is external, but since the form doesn't communicate "You can edit pointages of the affaire", but instead "you can simultaneously edit an affaire and a set of pointages, which might or might not be related", it's semantically clear and doesn't surprise future users (you included).
However, I believe you're approach might be flawed... but since it's unclear what you're actually trying to achieve, it's hard to propose a proper solution.
If it's just "there are too many (sub) forms displayed" - then it's more of a display issue, that can and probably should be fixed via javascript or css (display:none) or both. (which are imho nicer approaches, that don't mess with form logic and/mechanics).
So i'm making a multi-language site using symfony 2.8 but i had this problem when translating forms, i managed to change labels using translation_domain option like in this example:
->add('save','submit',
array('label'=>'btn.send',
'translation_domain' => 'FrontBundle',
'attr'=>array(
'class'=>'btn btn-blue',
)))
but i had a problem when translating entity type cause the names comes from the database so i added fields for other language
like this:
name_fr ,name_en , name_es ,...
the problem was how to use them with the form, after hours of googling i found this solution even though I didn't like it.
using the documentation i passed the _local from request to my form like this:
contactController:
public function contactAction(Request $request)
{
$contact = new contact();
$contact->setSendTime(new \DateTime('now'));
$form = $this->createForm(new contactType(), $contact,array('lang'=>$request->getLocale()));
//...
}
contactType:
class TaskType extends AbstractType
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
// ...
$resolver->setRequired('lang');
}
//...
public function buildForm(FormBuilderInterface $builder, array $options)
{
$local = $options['lang'];
// ...
$builder
->add('civility', 'entity', array(
'class'=>'BackBundle\Entity\civility',
//use this
'property' => $local == 'fr'?'name_fr':'name_en',
//or this or dont use them both
//'choice_label' => 'name',
'label'=>'Civilité:',
'expanded'=>true,
))
/...
;
}
}
i wonder if there is a less messier and better solution to translate the entity in forms
you can use Symfony Form Events to dynamically manage form data. Check this out
https://symfony.com/doc/2.8/form/dynamic_form_modification.html
It appears to be exactly what you need. You can pass the locale from your Controller to your Form (as an option) and within the Form class add an Event listener listening for an event (pick one that best suits your needs, my best guess is that you need the PRE_SET_DATA event - the one that manipulates the data from the database) and according to the passed locale option you can modify the form fields you need.
So i read Translatable from the DoctrineExtensions that #dbrumann give me and i use it side to side with Sonata Translation Bundle in the admin side and it works just fine.
if any one have any question about configuration put a comment
I have made a Data Transformer for my User class on a form field. It is for users to enter another username to which they want to send a private message to. It also has a title and content, but that doesn't matter, for now.
Here is the form builder:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('content', 'textarea')
->add(
'receiver',
'text',
array(
// validation message if the data transformer fails
'invalid_message' => 'That is not a valid user',
)
);
$builder->get('receiver')->addModelTransformer(new UserTransformer($this->manager));
}
What I want to do is get the invalid_message error and put it into a translation file. The problem is that I have to write it here:
->add('receiver',
'text',
array('invalid_message' => 'user.invalid')
Which will be taken from my translations.
Is there a way to have this message in my Entity class along with all the other validators, instead of having it in the form type class? I don't want to spread my validation messages everywhere in my files.
To answer your question, if you really want to store all the message translation keys in your entity, you could store those in a constant array in your entity class.
For example, your entity class may look like :
//..
class Foo
{
const MESSAGES = [
'invalidUser' => 'user.invalid'
];
public static function getMessages()
{
return self::MESSAGES;
}
//..
}
and in your data transformer :
->add('receiver',
'text', [
'invalid_message' => Foo::getMessages()['invalidUser']
]
Still I am failing to fully understand the motivation behind this. You will need at some point to identify the message you want to display in your data transformer. So what is the point to not provide directly the translation key associated to this message, and instead retrieve it through the entity ?
The place where your messages should be gathered is only the translation file. The validators in your entity class, as well as your data transformer, are only there to provide the good translation keys.
I would really like an pointers, answers from experience on how to reuse Form Types, Entities(object) and Validation constrains between backend API and some other frontend / client code.
Specificaly:
I have an managed entity lets say Product.php.
A ProductType.php with form definitions and validation rules for the entity in validation.yml.
I take these into separate repository, and require them with composer inside my Api and Client code.
The response from Api will be the serialized Product object. On the client side I would deserialize from json to the same Product, but this time without access to any db layer, eg. an unmanaged plain object.
So the question is did anyone manage to reuse these components in similar setting, or point to some resources how to manage this?
The usecase would be: centralized place for object code. Faster client implementations etc.
So far the main problem I stumbled upon is how to manage relations, when defined in the form, with the entity/choice type, as the entity type requires managed object by doctrine. An the choice type doesn't know how to use array of objects as choices.
Is this even worth the trouble, or is better to have almost identical code on both api and client side?
Example objects follows:
`
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name', 'text')
->add('price', 'text')
// use PRE_SET_DATA EVENT to determine if entity or choice here
// this should be used on the server side, with access to db
->add('tags', 'entity', [
'class' => 'Acme\MyBundle\Tag',
'multiple' => true
]);
// This should be used in the client, without access to db
//->add('tags', 'choice', [
// 'data_class' => 'Acme\MyBundle\Tag',
// 'choices' => $arrayOfTagObjects,
// 'multiple' => true
// ]);
}
}
`