Symfony Embedded Form Validation - php

I have several Entities, that are used in a single form, that extends from an abstract class.
I have created a form type for each entity and then they were embedded in a parent form.
I would like to perform validation based on groups, so the EmailType must check the "elemento" property only on Assert\NotBlank (Default group) and Assert\Email (email group), and Telefono must check Assert\NotBlank (Default group) and Assert\Regex (phone group).
With my configuration both checks (constraints) are performed, so email are checked on Email constraint AND Regex, and so the phone filed is... Where am i wrong?
Staff Entity on Collection emails and phones have configured the Assert\Valid() contraint
This is the example
Parent Form
<?php
namespace App\Form\Staff;
class StaffType extends AbstractType {
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(['data_class' => \Cowbell\Entity\Staff\Staff::class,
'validation_groups' => ['Default', 'email', 'phone']]);
}
/**
*
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
// ... Other field of Staff Entity
->add('emails', CollectionType::class, ['label' => 'admin.emails',
'entry_type' => \App\Form\Contatto\CBEmailType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'empty_data' => null,
'translation_domain' => 'admin',
'validation_groups' => ['email']])
->add('telefoni', CollectionType::class, ['label' => 'admin.phones',
'entry_type' => \App\Form\Contatto\CBTelefonoType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'empty_data' => null,
'translation_domain' => 'admin',
'validation_groups' => ['phone']]);
}
}
Then CBEmailType
<?php
namespace App\Form\Contatto;
class CBEmailType extends AbstractType{
/**
*
* #param OptionsResolver $resolver
*/
public function configureOptions( OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => \App\Entity\Contatto\Email::class,
'validation_groups' => ['Default', 'email']]);;
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('elemento', EmailType::class, ['label' => 'admin.email',
'translation_domain' => 'admin'])
}
}
CBTelefonoType
<?php
namespace App\Form\Contatto;
class CBTelefonoType extends AbstractType{
/**
*
* #param OptionsResolver $resolver
*/
public function configureOptions( OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => \Cowbell\Entity\Contatto\Telefono::class,
'validation_groups' => ['Default', 'phone']]);
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('elemento', TextType::class, ['label' => 'admin.phone',
'translation_domain' => 'admin'])
}
}
Both, Email and Telefono extend
<?php
namespace App\Entity\Contact;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
abstract class AbstractElementoContact {
/**
*
* #var string
*
* #ORM\Column(name="elemento", type="string", length=100, nullable=false)
* #Assert\NotBlank()
* #Assert\Email(strict=true, checkHost=true, checkMX=true, groups={"email"})
* #Assert\Regex("/[0-9]{6,50}/", groups={"phone"})
*/
protected $elemento;

AFAIK you can't set validation_groups on CollectionType form field (resp. you can set it, but it has no effect), so the whole form, including the set of sub-forms in the collection is allways validated with validation_groups set on the whole parent form.
The purpose of validation_groups is to allow constraints modification of object's properties for different purposes (e.g. creating new vs. editing existing), but not for what you described above.
Think about, if it would be possible, to use your current Email and Telephono as a property directly in the Staff object (or StaffType respectivelly) with using validation_groups to solve the $elemento should be Email elemento once and Telephono elemento once...
The solution for your case is to define Email and Telephono as different classes (not inherited from AbstractElementoContact) with specific contraint for each of them.

Related

Symfony: Expected argument of type "?Doctrine\Common\Collections\Collection", "array" given at property path

i have a entity called DynamicForm, which looks like this:
<?php
namespace App\Entity\Product\DynamicForm;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Entity;
/**
* Class DynamicForm
* #package App\Entity\Product
*
* #Entity
* #ORM\Table(name="product_dynamic_form")
*/
class DynamicForm
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="id", type="integer", unique=true, nullable=false)
*/
private ?int $id = null;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Product\DynamicForm\Component\Text", mappedBy="dynamicForm")
* #ORM\JoinColumn(name="component_text_id", referencedColumnName="id")
*/
private ?Collection $textComponents;
/**
* #return Collection|null
*/
public function getTextComponents(): ?Collection
{
return $this->textComponents;
}
/**
* #param Collection|null $textComponents
*
* #return DynamicForm
*/
public function setTextComponents(?Collection $textComponents): DynamicForm
{
$this->textComponents = $textComponents;
return $this;
}
}
Also i created a related type for it - DynamicFormType:
<?php
namespace App\Type\Product\DynamicForm;
use App\Entity\Product\DynamicForm\DynamicForm;
use App\Type\Product\DynamicForm\Component\TextType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DynamicFormType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('textComponents', CollectionType::class, [
'entry_type' => TextType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'label' => ' '
])
->add('submit', SubmitType::class, [
'label' => 'form.basic.save'
]);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', DynamicForm::class);
}
}
The TextType entry type class from the namespace App\Type\Product\DynamicForm\Component\TextType looks like this:
<?php
namespace App\Type\Product\DynamicForm\Component;
use App\Entity\Product\DynamicForm\Component\Text;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type as FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class TextType extends AbstractType
{
private TranslatorInterface $translator;
/**
* TextType constructor.
*
* #param TranslatorInterface $translator
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('label', FormType\TextType::class, [
'required' => true,
'translation_domain' => 'wedding',
'label' => 'setting.form.dynamic_by_user.query_to_guest'
])
->add('type', FormType\ChoiceType::class, [
'required' => true,
'translation_domain' => 'wedding',
'label' => 'setting.form.dynamic_by_user.select_type',
'attr' => [
'class' => 'enriched',
'data-search-placeholder' => $this->translator->trans('select.search'),
'data-search-no-results-text' => $this->translator->trans('select.search_no_results_found')
],
'choice_translation_domain' => 'wedding',
'choices' => [
'setting.form.dynamic_by_user.type_text' => Text::TYPE_TEXT_FIELD,
'setting.form.dynamic_by_user.type_textarea' => Text::TYPE_TEXT_AREA,
'setting.form.dynamic_by_user.type_email' => Text::TYPE_EMAIL_FIELD,
'setting.form.dynamic_by_user.type_number' => Text::TYPE_NUMBER_FIELD,
]
]);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', Text::class);
}
}
While i try to transmit the form, the request in the controller contains the form data as you can see it, in the following screenshot:
My problem is now that i always get the following error (while handling the request for the form via ->handleRequest($request) on created form in controller):
Expected argument of type "?Doctrine\Common\Collections\Collection", "array" given at property path "textComponents".
I have such collection settings also in other classes, but without problems - I don't know any further, can anyone please assist me or see the error?
(I am using Symfony version 5.2.9, if u need any further info just ask for it - I will give it to you as soon as possible)
Trying to add a constructor in your entity
public function __construct()
{
$this->textComponents = new ArrayCollection();
}
Add addTextComponent and removeTextComponent methods intead of setTextComponents
public function addTextComponent(Text $textComponent): self
{
$textComponent->setDynamicForm($this);
$this->textComponents->add($textComponent);
return $this;
}
public function removeTextComponent(Text $textComponent): self
{
$this->textComponents->removeElement($textComponent);
return $this;
}
Add 'by_reference' => false in the textComponents form params
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('textComponents', CollectionType::class, [
'entry_type' => TextType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'label' => ' ',
'by_reference' => false,
])
->add('submit', SubmitType::class, [
'label' => 'form.basic.save'
]);
}
I was able to fix this by doing this, that allows us to pass a Collection or an empty array or an array of your entity which I think is Text:
/**
* #param Collection|Text[] $textComponents
*
* #return DynamicForm
*/
public function setTextComponents($textComponents): DynamicForm
{
$this->textComponents = $textComponents;
return $this;
}

Symfony - Filter a form with a CollectionType of Entity

I have two entities, client and order.
I have an admin interface where I show all the orders of a client, where I can modify or delete every order.
To do that I use a Collection Type:
My controller:
$form = $this->createForm(ClientConfigType::class, $client);
This is my ClientConfigType :
<?php
namespace App\Form;
use App\Entity\Client;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ClientConfigType extends AbstractMainType {
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add("orders",
CollectionType::class,
[
'entry_type' => OrderConfigType::class,
'allow_add' => true,
'label' => false
]);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => Client::class,
'allow_extra_fields' => true,
));
}
}
And my OrderConfigType is a classic formType.
Everything is working perfectly without any filtering.
But I want to be able to filter and display my collectionType of Order.
For Exemple I would like to display the order of a specific date or the orders > 100$, etc
I tried to use query builder but it's only working for EntityType and not CollectionType
I tried to pass a variable from my Controller to my Form then to my Entity "get" function like that:
$minimumPrice = $request->query->get('minimumPrice');
$form = $this->createForm(ClientConfigType::class, $client, ['minimumPrice' => $minimumPrice ]);
Then in my ConfigType I can retrieve my variable in the configureOptions function but Then, I can't do anything to use that value to filter my collection Type.
How can I filter my collectionType ?
Instead of passing minutePrice you can query like you want your orders, and pass order's collection to the form.
Example here:
class ClientConfigType extends AbstractMainType {
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add("orders",
CollectionType::class,
[
'entry_type' => OrderConfigType::class,
'allow_add' => true,
'label' => false,
'data' => $options['orderCollection']
]);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => Client::class,
'orderCollection' => null,
));
}
}
$orderCollection = $em->getRepository(Order::class)->findAll(); //something like this or custom query it s an example
$form = $this->createForm(ClientConfigType::class, $client, ['orderCollection' => $orderCollection ]);

Symfony for propert_path for child form type

I understand if I put
->add('visitor.name', TextType::class, [
'property_path' => 'vistorName'
])
This will be identical to <input name="vistorName"
How can I do similar for child form, to have all the child field as a parent field name without putting in as a child array.
$builder->add('customer', CustomerType::class);
CustomerType
$builder
->add('name', TextType::class)
->add('email', EmailType::class)
->add('phone', PhoneNumberType::class, [
'default_region' => 'GB', // TODO GLOBAL release
'format' => PhoneNumberFormat::NATIONAL
]);
The above form should generate <input name="name"... <input name="email"... and not as following <input name="customer[name]"...
how can I do that?
Following is complete code example
// Cart model
class Cart {
protected $productName;
/** #var Customer */
protected $customer;
}
// Customer model
Class Customer {
protected $name;
protected $email;
protected $phone;
}
//CustomerType form
$builder
->add('name', TextType::class)
->add('email', EmailType::class)
->add('phone', PhoneNumberType::class, [
'default_region' => 'GB', // TODO GLOBAL release
'format' => PhoneNumberFormat::NATIONAL
]);
// main parent form
class MainFormType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('productName', TextType::class)
->add('customer', CustomerType::class); // need property_path for this, so all the inner fields can be used as parent
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Cart::class,
'required' => false
]);
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'cart';
}
}
It works the other way around:
Scenario:
VisitorEntity:
TypeOfVisitorEntity
CustomerEntity
Now you want to update a VisitorEntity customer.name:
class MainFormType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('productName', TextType::class)
->add('customerName', TextType::class, ['property_path'=>'customer.name'])
->add('customerEmail', TextType::class, ['property_path'=>'customer.email'])
->add('customerPhone', TextType::class, ['property_path'=>'customer.phone'])
;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Cart::class,
'required' => false
]);
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'cart';
}
}
property_path
https://symfony.com/doc/3.4/reference/forms/types/form.html#property-path
type: any default: the field's name
Fields display a property value of the form's domain object by default. When the form is submitted, the submitted value is written back into the object.
If you want to override the property that a field reads from and writes to, you can set the property_path option. Its default value is the field's name.
If you wish the field to be ignored when reading or writing to the object you can set the property_path option to false, but using property_path for this purpose is deprecated, you should use the mapped option.

Validating Symfony3 form with Command not Entity

I have prepared a simple application with symfony3.2 and use of DDD (at least I tried). But I have problems validating ProductType form, which uses NewProductCommand instad of Product object.
My form is:
<?php
namespace AdminBundle\Form;
use Shop\Domain\Command\NewProductCommand;
use AppBundle\Form\PriceType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* Product add form
*
* #package AdminBundle\Form
*/
class ProductType extends AbstractType
{
/**
* Building form
*
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'name',
TextType::class,
array(
'required' => true,
'label' => 'Nazwa',
// 'constraints' => [new NotBlank(), new Length(['max' => 255])],
)
)
->add(
'description',
TextareaType::class,
array(
'required' => true,
'label' => 'Opis',
'attr' => [
'rows' => 10
],
// 'constraints' => [new Length(['min' => 100, 'max' => 65000])],
)
)
->add(
'price',
PriceType::class,
array(
'required' => true,
'label' => 'Cena'
)
);
}
/**
* Options configuration
*
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
array(
'data_class' => NewProductCommand::class
)
);
}
/**
* Return form name
*
* #return string
*/
public function getName()
{
return 'product';
}
}
And in controller I have:
$command = new NewProductCommand();
$form = $this->createForm(ProductType::class, $command);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
The validation works when I remove commented constraints, but what I want to achieve is that the form uses my Product class validators.
I think that the problem is that, I pass here NewProductCommand::class, and not the Product::class, so the form doesn't know anything about Product class (which is defined in doctrine orm product mapping: https://github.com/wojto/washop/blob/master/src/Shop/Infrastructure/Repository/Doctrine/config/mapping/Product.orm.yml) and validation, which is in yml too (https://github.com/wojto/washop/blob/master/src/AdminBundle/Resources/config/validation.yml).
Where is the place, I should connect NewProductCommand with Product class so the form can use validation for Product class?
I hope, I make it clear ;) full app code is here: https://github.com/wojto/washop/
Maybe I am doing everything wrong, so I appreciate any help :)

Symfony 2 : preselect multiple values in a form by loading manualy attribute data from an ArrayCollection

I need to get preselected some values of an entity attribute that I get in the PRE_SET_DATA event, not from data base.
I have a Form working, all datas from my Entity AccessGroup is loaded but my problem is to get selected the ArrayCollection attribute named accessGroups from entity User which is not stored in database.
To make it clear, attribute accessGroups is loaded by User's roles.
Here is the FormType Class
namespace Pkg\ExtranetBundle\Form;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Doctrine\ORM\EntityManager;
class RoleType extends AbstractType
{
/**
* #var EntityManager
*/
protected $em;
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$this->em = $options['em'];
$builder->addEventListener(FormEvents::PRE_SUBMIT, array($this, 'onPreSubmit'));
}
/**
* Listener before normalizing data form
*
* #param FormEvent $event
*/
public function onPreSetData(FormEvent $event)
{
$user = $event->getData();
$accessGroups = $this->em->getRepository('PkgExtranetBundle:AccessGroup')->getSelected($user->getRoles());
$user->setAccessGroups(new ArrayCollection($accessGroups));
$event->setData($user);
$form = $event->getForm();
$form->add('accessGroups', EntityType::class, array(
'class' => 'PkgExtranetBundle:AccessGroup',
'choice_label' => 'name',
'choice_value' => 'role',
'multiple' => true,
'expanded' => false
))
->add('save', SubmitType::class, array('label' => 'registration.submit', 'translation_domain' => 'FOSUserBundle'));
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Pkg\ExtranetBundle\Entity\User',
'em' => null
));
}
}
Okey, the class I made has its own problem with the choice_value parameter :
$form->add('accessGroups', EntityType::class, array(
'class' => 'PkgExtranetBundle:AccessGroup',
'choice_label' => 'name',
'choice_value' => 'role', // If choice_value is not the entity index, then preselection will not be applied as the index could not be retrieved.
'multiple' => true,
'expanded' => false
))

Categories