Create a custom resource form on Sylius (Symfony3) : "Expected scalar, but got array" - forms

I try to create a custom form for my Sylius Resource "article" using the Sylius doc. Without creating custom form, everything works well, but if I want to make a custom form, I have this error "Invalid type for path "sylius_resource.resources.blog.article.classes.form". Expected scalar, but got array."
Here is my ArticleType class :
<?php
namespace BlogAdminBundle\Form\Type;
use Symfony\Component\Form\FormBuilderInterface;
use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType;
class ArticleType extends AbstractResourceType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Build your custom form!
$builder->add('id', HiddenType::class)
->add('titre', TextType::class)
->add('date', DateType::class, array('html5' => true))
->add('contenu', CKEditorType::class)
->add('tags', TextType::class)
->add('resume', TextareaType::class)
->add('save', SubmitType::class, array('label' => 'Enregistrer l\'article'));
}
public function getName()
{
return 'admin_article';
}
}
And the declaration of my resource :
sylius_resource:
resources:
blog.article:
driver: doctrine/orm
classes:
model: BlogBundle\Entity\Article
form:
default: BlogAdminBundle\Form\Type\ArticleType
Does anyone know what is the problem ?
Thanks everyone !

you have to register your form as form.type service. And you have to send argument of your form class. You should do something like this:
services:
app.form.type.article:
class: BlogAdminBundle\Form\Type\ArticleType
arguments: [BlogBundle\Entity\Article]
tags:
- { name: form.type }
You can check what classes are used for your Article by using this command:
php bin/console debug:container | grep article

Related

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

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.

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

How to add label into form builder (not in twig)?

I have this code, but it doesn't work:
$builder->add('name','text',array(
'label' => 'Due Date',
));
the problem i have in fosuserbundle, i have overring form
<?php
namespace Acme\UserBundle\Form\Type;
use Symfony\Component\Form\FormBuilder;
use FOS\UserBundle\Form\Type\RegistrationFormType as BaseType;
class RegistrationFormType extends BaseType
{
public function buildForm(FormBuilder $builder, array $options)
{
// add your custom field
$builder->add('name','text',array(
'label' => 'Due Date',
));
parent::buildForm($builder, $options);
}
public function getName()
{
return 'acme_user_registration';
}
}
but not work, not give me any error and set the label "fos_user_registration_form_name"
You see label as fos_user_registration_form_name, because FOSUserBundle uses translations files to translate all texts in it.
You have to add your translations to file called like Resources/translations/FOSUserBundle.nb.yml (example for norwegian) or you can modify translations file coming with the bundle (copying it to Acme\UserBundle is a better way).