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
Related
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).
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.
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.
I've got a form built in Symfony and when rendered in the view, the html form may or may not contain all of the fields in the form object (the entity sort of has a couple of different states and not all fields are rendedered in the view).
The problem is that when the form is processed in the submit handler, via handleRequest() method of the form object, it resets any properties in the entity that are not present in the post data to null, blowing away any existing value.
Is there any way to tell Symfony not to be so stupid and only process the fields present in the POST data?
Or do I have to clone the entity before the handleRequest call and then loop over the POST values and copy the related values from the post-handleRequest entity over to the pre-handleRequest clone of the entity, so I preserve the fields that are not in the POST data.
phew! as you can see, its a bit of a daft solution, to a bit of a daft problem, tbh.
I could understand symfony doing this if the entity was in effect a newly created object, but its been loaded from the DB and then handleRequest called - it should be sensible enough to know the object has already been initialised and only set the fields passed in the POST data.
Thanks for any help.
Regards
Steve.
In short, don't use handleRequest.
You should use submit directly instead along with the clearMissing parameter set to false.
Symfony/Component/Form/FormInterface
/**
* Submits data to the form, transforms and validates it.
*
* #param null|string|array $submittedData The submitted data.
* #param bool $clearMissing Whether to set fields to NULL
* when they are missing in the
* submitted data.
*
* #return FormInterface The form instance
*
* #throws Exception\AlreadySubmittedException If the form has already been submitted.
*/
public function submit($submittedData, $clearMissing = true);
When you use handleRequest it works out what data you are wanting the submit and then submits it using $form->submit($data, 'PATCH' !== $method);, meaning that unless you have submitted the form using the PATCH method then it will clear the fields.
To submit the form yourself without clearing your can use...
$form->submit($request->get($form->getName()), false);
.. which get the form data array from the request and submit it directly, but with the clear missing fields parameter set to false.
If your entity has different states, you could reflect this in your form type.
Either create multiple form types (maybe using inheritance) containing the different field setups and instantiate the required one in your controller.
Something like this:
class YourState1FormType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('someField')
;
}
}
class YourState2FormType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('someOtherField')
;
}
}
Or pass a parameter to the single form type upon creation in the controller and adapt the field setup depending on the state. If you don't add the fields that are not present, they don't get processed.
Something like this:
class YourFormType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
if($options['state'] == 'state1') {
$builder
->add('someField')
;
} else if($options['state'] == 'state2') {
$builder
->add('someOtherField')
;
}
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'state' => 'state1'
));
}
}
Update
Another approach you can take to modify your form based on the submitted data is to register event listeners to the form's PRE_SET_DATA and POST_SUBMIT events. These listeners get called at different moments within the form submission process and allow you to modify your form depending on the data object passed to the form type upon form creation (PRE_SET_DATA) or the form data submitted by the user (POST_SUBMIT).
You can find an explanation and examples in the docs.
Assume for a moment that this form utilizes an imaginary Animal document object class from a ZooCollection that has only two properties ("name" and "color") in symfony2.
I'm looking for a working simple stupid solution, to pre-fill the form fields with the given object auto-magically (eg. for updates ?).
Acme/DemoBundle/Controller/CustomController:
public function updateAnimalAction(Request $request)
{
...
// Create the form and handle the request
$form = $this->createForm(AnimalType(), $animal);
// Set the data again << doesn't work ?
$form->setData($form->getData());
$form->handleRequest($request);
...
}
You should load the animal object, which you want to update. createForm() will use the loaded object for filling up the field in your form.
Assuming you are using annotations to define your routes:
/**
* #Route("/animal/{animal}")
* #Method("PUT")
*/
public function updateAnimalAction(Request $request, Animal $animal) {
$form = $this->createForm(AnimalType(), $animal, array(
'method' => 'PUT', // You have to specify the method, if you are using PUT
// method otherwise handleRequest() can't
// process your request.
));
$form->handleRequest($request);
if ($form->isValid()) {
...
}
...
}
I think its always a good idea to learn from the code generated by Symfony and doctrine console commands (doctrine:generate:crud). You can learn the idea and the way you should handle this type of requests.
Creating your form using the object is the best approach (see #dtengeri's answer). But you could also use $form->setData() with an associative array, and that sounds like what you were asking for. This is helpful when not using an ORM, or if you just need to change a subset of the form's data.
http://api.symfony.com/2.8/Symfony/Component/Form/Form.html#method_setData
The massive gotcha is that any default values in your form builder will not be overridden by setData(). This is counter-intuitive, but it's how Symfony works. Discussion:
https://github.com/symfony/symfony/issues/7141