Can't get a way to read the property "user" in class "App\Entity\User" - forms

I made a form so that when a user is selected, his role changes from ["ROLE_USER"] to ["ROLE_ADMIN"]. When form is submitted, I have the following error : Can't get a way to read the property "user" in class "App\Entity\User".
I understand it must come the fact there is no such field named user in the User class, but I don't know with which field I can replace user. I already tried name or roles, but it doesn't work either with them.
How can I select a user and simply change his role ?
AdminType.php
<?php
namespace App\Form\Admin;
use App\Entity\User;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
class AdminType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('user', EntityType::class, [
'class' => User::class,
'choice_label' => 'name',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('u')
->andWhere('u.roles LIKE :role')
->setParameter('role', '["ROLE_USER"]')
->orderBy('u.firstname', 'ASC');
},
'placeholder' => 'J\'ajoute un administrateur',
])
->add('save', SubmitType::class, [
'attr' => ['class' => 'save'],
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
'translation_domain' => 'forms'
]);
}
}
AdminController.php
<?php
namespace App\Controller\Admin;
use App\Form\Admin\AdminType;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class AdminController extends AbstractController
{
#[Route('/admin/list', name: 'admin')]
public function admin(
Request $request,
UserRepository $userRepository,
EntityManagerInterface $entityManagerInterface
){
$admins = $userRepository->admin();
$form = $this->createForm(AdminType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->get('user')->getData();
$user->setRoles(["ROLE_ADMIN"]);
$entityManagerInterface->persist($user);
$entityManagerInterface->flush();
return $this->redirectToRoute('admin');
}
return $this->renderForm('admin/user/admin.html.twig', [
'admins' => $admins,
'form' => $form
]);
}

Short answer
remove 'data_type' => User::class from configureOptions in your AdminType form.
Explanation
Setting the data_type of the form to the User entity will cause Symfony to try to create a User object and set its user Property to the value in the form's user field (which you get via $form->get('user')). That's why the error message tells you, that it can't find a way to read (to then overwrite) the property user on the User class.
Removing the data_type will then mean, that the form's data type is just an array (the default), you could also explicitly set null.
If the form's data type is an array, it'll just set the user key in that array.
Since your form's user field is of type EntityType, with a given class, it already ensures that the form's user field's value must be of that class (the User entity). And since you only want to select a user, to then add a role, I assume that the form doesn't need the User data_type.

Related

Symfony Forms: Adding a CallbackTransformer to a field that is added in an EventListener

I have a somewhat complex form and am struggling with adding a ModelTransformer to a dynamically added field.
First I have a basic form with some fields and one CollectionType field that includes a custom Type:
class FilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// adding some other fields here ...
$builder->add('conditions', Type\CollectionType::class, [
'entry_type' => FilterRowType::class,
'allow_add' => true,
'prototype' => true,
'allow_delete' => true,
'entry_options' => ['label' => false],
]);
}
}
The FilterRowType consists of several fields that are depending on each other.
First the user has to select an option from a dropdown and then another field is added whose type and options depend on the selected value of the first field.
The second field could be TextType or NumberType or even ChoiceType with its choices again depending on the first field.
Finally I need to add a CallbackTransformer to this second field.
So here is what I currently have (widely stripped of stuff I think is not important for this question):
class FilterRowType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('attribute', Type\ChoiceType::class, [
'choices' => $this->getAttributeChoices(),
]);
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($builder) {
$this->addDynamicInputs($event, $builder);
});
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($builder) {
$this->addDynamicInputs($event, $builder);
});
}
public function addDynamicInputs(FormEvent $event, FormBuilderInterface $builder)
{
$form = $event->getForm();
$data = $event->getData();
// adding some other fields ...
$valueConfig = $this->getValueConfig($data['attribute']);
$form->add('value', $valueConfig['type'], $valueConfig['options']);
$valueConfig['options']['auto_initialize'] = false;
$form->add(
$builder->create('value', $valueConfig['type'], $valueConfig['options'])
->addModelTransformer($this->getCallbackTransformer ())
->getForm()
);
}
}
And this is actually working ! :)
BUT:
As you might already have spotted I am actually adding the 'value' field twice here.
This happened by accident as I added the CallbackTransformer later and forgot to delete the original line.
The problem is that if I now remove the original line $form->add('value', $valueConfig['type'], $valueConfig['options']); I run into an exception:
Neither the property "value" nor one of the methods "value()", "getvalue()"/"isvalue()"/"hasvalue()" or "__call()" exist and have public access in class "Symfony\Component\Form\FormView".
Probably because I set $valueConfig['options']['auto_initialize'] = false; for the new creation of the field?
But if I remove that line I run into a different error:
Automatic initialization is only supported on root forms. You should set the "auto_initialize" option to false on the field "value"
Of course I could leave everything as it is with adding the 'value' field twice.
But that seems a very fishy solution to me and I am afraid that it might have some unforeseen consequences even if currently everything seems to work fine.
So can maybe someone with more insight into symfony forms enlighten me?
Are there possible problems with my 'solution' ?
Is there a better/proper way of doing what I am trying to do?
I had the same case today. Here is what I did :
Create an extension :
namespace App\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ModelTransformerExtension extends AbstractTypeExtension {
public static function getExtendedTypes(): iterable {
return [FormType::class];
}
public function buildForm(FormBuilderInterface $builder, array $options) {
parent::buildForm($builder, $options);
if (isset($options['model_transformer'])) {
$builder->addModelTransformer($options['model_transformer']);
}
}
public function configureOptions(OptionsResolver $resolver) {
parent::configureOptions($resolver);
$resolver->setDefaults(array('model_transformer' => null));
}
}
Use it in your form field options :
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$form->add('fieldName', $TextType::class, [
'model_transformer' => // Your transformer here
]);
});

Array-type property and Form CollectionType with data transformer

I have an entity with a property that is set as an array
/**
* #ORM\Column(type="array")
*/
private $labels = [];
this array of data stores translations of a label like
[
'en' => 'foo-in-English',
'de' => 'foo-in-German',
'ru' => 'foo-in-Russian'
]
I have a Form with the type set for the labels like:
$builder
->add('labels', CollectionType::class);
Note that the entry_type defaults (properly) to TextType here. Left as is, the template would be displayed with text fields, like:
Labels: en: _____FOO IN ENGLISH____
de: _____FOO IN GERMAN_____
ru: _____FOO IN RUSSIAN____
But, I would like the fields to be displayed with the actual language name and not the two-letter code as the label, so something like:
Labels: English: _____FOO IN ENGLISH____
German: _____FOO IN GERMAN_____
Russian: _____FOO IN RUSSIAN____
I also want to make sure that all my selected/supported languages are displayed - even if they currently have no value.
So, this seems like the proper place for a DataTransformer, but try as I might I could not get this concept to work within the Form class. It seems that attempting to transform the data of a collection type is more difficult (or impossible?) than a simpler type like text.
I've overcome this as a workaround by transforming the data within the controller before submitting it to the form and after processing the form before persistence. e.g.
$this->transformTranslations($fooEntity);
$form = $this->createForm(FooType::class, $fooEntity);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$fooEntity = $form->getData();
$this->reverseTransformTranslations($fooEntity);
$this->getDoctrine()->getManager()->persist($fooEntity);
$this->getDoctrine()->getManager()->flush();
...
I'm wondering if anyone has a better method (like how to use normal data or model transformers). I can't seem to find much online about using data transformers with collection types. TIA!
I have not personally used a doctrine array value before, however
you can define a 'default' form class for each of your translation options like so:
AppBundle\Form\LanguageStringEditorType.php
class LanguageStringEditorType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('en', TextareaType::class, ['label' => 'English'])
->add('de', TextareaType::class, ['label' => 'German'])
->add('ru', TextareaType::class, ['label' => 'Russian'])
;
}
}
If you keep the naming ('en', 'de' and 'ru') the same as your data array key names for example having an (doctrine) entity like this:
AppBundle\Entity\LanguageString.php
class LanguageString {
private $identifier;
private $translations; // this is the doctrine array type
// however I didn't feel like setting up a database for this
// test so I'm manually filling it see the next bit
... Getter and setter things ...
And create a type for that as well:
AppBundle\Form\LanguageStringType.php
class LanguageStringType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('identifier')
->add('translations', LanguageStringEditorType::class, ['label' => 'Translations'])
;
}
}
We can use this in the controller
$data = new LanguageString();
// fill some dummy content (not using database)..
$data->setIdentifier('hello_world');
$data->setTranslations([
'en' => 'Hello world!',
'de' => 'Hallo Welt!',
'ru' => 'Привет мир'
]);
$form = $this->createForm(LanguageStringType::class, $data);
return $this->render('default/index.html.twig', [
'form' => $form->createView()
]);
And the rest is done by magic, no transformers required. The data is placed in the form fields. And set to the entity when using the handleRequest. Just remember that the data key values are the same as the form builder names.
And as a bonus you have defined all your default language fields in the LanguageStringEditorType class, filled in or not.
So, I learned I needed to separate my two needs into different solutions. First I created a new form type to use instead of the text type I was using by default:
$builder
])
->add('labels', CollectionType::class, [
'entry_type' => TranslationType::class
])
This class is very simple and is only an extension of a regular TextType:
class TranslationType extends AbstractType
{
/**
* #var LocaleApiInterface
*/
private $localeApi;
/**
* TranslationType constructor.
* #param LocaleApiInterface $localeApi
*/
public function __construct(LocaleApiInterface $localeApi)
{
$this->localeApi = $localeApi;
}
/**
* {#inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['label'] = array_search($view->vars['name'], $this->localeApi->getSupportedLocaleNames());
}
public function getParent()
{
return TextType::class;
}
}
This satisfied the labelling issue. Second, to ensure I had all the supported locales in my data, I used a FormEventListener:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$supportedLocales = $this->localeApi->getSupportedLocales();
$data = $event->getData();
$labels = $data['labels'];
foreach ($supportedLocales as $locale) {
if (!array_key_exists($locale, $labels)) {
$labels[$locale] = $labels['en'];
}
}
$data['labels'] = $labels;
$event->setData($data);
});
This adds the required keys to the data if they are not already present.

Translating validation error messages in a custom FormType

I have the current situation
<?php
namespace MyBundle\Form\Type;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints as Assert;
class MyFormType extends AbstractType implements ContainerAwareInterface
{
use \Symfony\Component\DependencyInjection\ContainerAwareTrait;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$translator = $this->container->get('translator');
$builder->add('my-field', 'text', [
'constraints' => [
new Assert\NotBlank([
'message' => $translator->trans('%field% should not be blank.', ['%field%' => $translator->trans('MyFieldName')]),
]),
],
]);
}
public function getName()
{
return 'my_form';
}
}
This example already works, I am trying to refactor it so I don't have to include the container (or the translator) in it.
The challenge lies in keeping
'%field% should not be blank.' and
'MyFieldName'
as the only two translatable strings, 'cause it's likely that MyFieldName is going to be translated already (like for labels) leaving '%field% should not be blank.' as a generic message valid for any field in the site.
Past into constraints
'attr' => array(
'placeholder' => 'Message',
)
If you have activated the translator in symfony framework config by uncommenting this line :
#translator: { fallbacks: [%locale%] }
in config.yml, all error messages of violations set by the validator are translated by default with values from validators domain.
You should us either app/Resources/translations/validators.(_format) or
src/(Acme/)*Bundle/Resources/translations/validators.(_format) to define your custom messages and once done clear your cache.
example :
# app/Resources/translations/validators.yml
my_form:
errors:
my_field:
not_blank: My field should not be blank
and
// FormType
$builder->add('my-field', 'text', [
'constraints' => [
new Assert\NotBlank([
'message' => 'my_form.errors.my_field.not_blank',
]),
],
]);
Error messages from validator will be automatically translated.
See http://symfony.com/doc/current/book/translation.html#translating-constraint-messages

Symfony2 : Sort / Order a translated entity form field?

I am trying to order an entity form field witch is translated.
I am using the symfony translation tool, so i can't order values with a SQL statement.
Is there a way to sort values after there are loaded and translated ?
Maybe using a form event ?
$builder
->add('country', 'entity',
array(
'class' => 'MyBundle:Country',
'translation_domain' => 'countries',
'property' => 'name',
'empty_value' => '---',
)
)
I found the solution to sort my field values in my Form Type.
We have to use the finishView() method which is called when the form view is created :
<?php
namespace My\Namespace\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Bundle\FrameworkBundle\Translation\Translator;
class MyFormType extends AbstractType
{
protected $translator;
public function __construct(Translator $translator)
{
$this->translator = $translator;
}
public function finishView(FormView $view, FormInterface $form, array $options)
{
// Order translated countries
$collator = new \Collator($this->translator->getLocale());
usort(
$view->children['country']->vars['choices'],
function ($a, $b) use ($collator) {
return $collator->compare(
$this->translator->trans($a->label, array(), 'countries'),
$this->translator->trans($b->label, array(), 'countries')
);
}
);
}
// ...
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('country', 'entity',
array(
'class' => 'MyBundle:Country',
'translation_domain' => 'countries',
'property' => 'name',
'empty_value' => '---',
)
)
;
}
}
OLD ANSWER
I found a solution for my problem, I can sort them in my controller after creating the view :
$fview = $form->createView();
usort(
$fview->children['country']->vars['choices'],
function($a, $b) use ($translator){
return strcoll($translator->trans($a->label, array(), 'countries'), $translator->trans($b->label, array(), 'countries'));
}
);
Maybe I can do that in a better way ?
Originally I wished to do directly in my form builder instead of adding extra code in controllers where I use this form.
I think it's impossible. You need to use PHP sorting, but if you use Symfony Form Type, I would advise to sort it with JavaScript after page is loaded.
If your countries are in an array, just use the sort() function, with the SORT_STRING flag. You will do some gymnastic to have it in my opinion.
Check this doc : http://php.net/manual/fr/function.sort.php

Form validation - Require only one field to be filled in

I have the following form class:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('existingfolder', 'entity', array(
'class' => 'ImageBundle:Folder',
'required' => false,
))
->add('folder', 'text', array('required' => false))
->add('file', 'file');
}
How can I set up the validation so that either the existingfolder or folder field must be filled (but not both of them)?
Any advice appreciated.
Thanks.
Use the True or Callback validation assert, here an example to check if the user must give at least one of the folders:
<?php
namespace Acme\BlogBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class Image
{
// ...properties, functions, etc...
/**
* #Assert\True(message = "You must give at least an existing folder or a new folder")
*/
public function isThereOneFieldFilled()
{
return ($this->existingfolder || $this->folder); // If false, display an error !
}
}
Here another example if the user must give ONLY one field but not both:
/**
* #Assert\True(message = "You must give an existing folder or a new folder, not both")
*/
public function isThereOnlyOneFieldFilled()
{
return (!$this->existingfolder && $this->folder || $this->existingfolder && !$this->folder);
}
EDIT:
Callback validation inside a form (I found an example here):
// use ...
use Symfony\Component\Form\CallbackValidator;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
// Inside the form:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('existingfolder', 'entity', array(
'class' => 'ImageBundle:Folder',
'required' => false,
))
->add('folder', 'text', array('required' => false))
->add('file', 'file');
// Use the CallbackValidator like a TrueValidator behavior
$builder->addValidator(new CallbackValidator(function(FormInterface $form) {
if (!$form["existingfolder"]->getData() && !$form["folder"]->getData()) {
$form->addError(new FormError('You must give at least an existing folder or a new folder'));
}
}));
}