I am developing my first complex form with Symfony 2 and I have gotten stuck.
This image represents my situation
So, I have a very simple entity called TShirt with the following attributes:
name -> mapped as a string column in the database
tags_data -> mapped as a string column in the database (that will contain a JSON object)
Then I have a form with more fields than the entity has:
class TShirtType extends AbstractType {
public function buildForm( FormBuilderInterface $builder, array $options ) {
$builder
->add('name', 'text', array(
'required' => true,
))
->add( 'colours', 'choice', array(
'choices' => $availableColours,
'multiple' => true,
'expanded' => true,
'mapped' => false,
))
->add( 'sizes', 'choice', array(
'choices' => $availableSizes,
'multiple' => true,
'expanded' => true,
'mapped' => false,
))
->add( 'materials', 'choice', array(
'choices' => $availableMaterials,
'multiple' => true,
'expanded' => true,
'mapped' => false,
));
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions( OptionsResolverInterface $resolver ) {
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\TShirt',
));
}
public function getName() {
return 'app_bundle_tshirt_form';
}
}
I have read about Data Transformers but as far as I know they cannot help me in this issue because they work with mapped fields.
Giving this scenario, I want to add something like the Data Transformers that takes the colours, sizes and materials form fields and builds the JSON object to store it in the Entity.
So my question is: Is there a way for custom handling the non-mapped fields in the form class?
Any help would be very appreciated! Thanks!!
Maybe you could use an EventListener in your FormType like below :
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$formFactory = $builder->getFormFactory();
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (\Symfony\Component\Form\FormEvent $event) use ($formFactory, $builder) {
$data = $event->getData();
$form->add(
'customFieldName',
'text',
array('some_options' => 'someOptionValue')
);
}
Be careful about FormEvents (here : PRE_SET_DATA).
Also, $data = $event->getData(); allows you to get the related object. You could loop on it and parse JSON (or create as much method as your JSON array contains and call them over and over) in order to display as much $form->add(.. as you have properties ion you JSON array.
it's not good solution to store it in tagsData !
solution for SYMFONY 2.7
Controller.php:
$colors = [
'white' => 'white',
'black' => 'black',
'green' => 'green'
];
$sizes = [
'M' => 'M',
'L' => 'L',
'XL' => 'XL'
];
$materials = [
'Cotton' => 'Cotton',
'Wool' => 'Wool',
'Silk' => 'Silk'
];
$object = new TShirt();
$form = $this->createForm(new TShirtType(), $object, ['colours' => $colors, 'sizes' => $sizes, 'materials' => $materials]);
$form->handleRequest($request);
TShirtType.php
class TShirtType extends AbstractResourceType
{
public function buildForm( FormBuilderInterface $builder, array $options ) {
$builder
->add('name', 'text', array(
'required' => true,
))
->add( 'colours', 'choice', array(
'choices' => $options['colours'],
'multiple' => true,
'expanded' => true,
'mapped' => false
))
->add( 'sizes', 'choice', array(
'choices' => $options['sizes'],
'multiple' => true,
'expanded' => true,
'mapped' => false
))
->add( 'materials', 'choice', array(
'choices' => $options['materials'],
'multiple' => true,
'expanded' => true,
'mapped' => false
));
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event)
{
$object = $event->getData();
$form = $event->getForm();
$tagsData = [
'colours' => $form->get('colours')->getData(),
'sizes' => $form->get('sizes')->getData(),
'materials' => $form->get('materials')->getData(),
];
$object->setTagsData($tagsData);
$event->setData($object);
});
}
public function finishView(FormView $view, FormInterface $form, array $options)
{
$tagsData = $view->vars['data']->getTagsData();
if($tagsData) {
$types = ['colours', 'sizes', 'materials'];
foreach($types as $type) {
foreach($view->offsetGet($type) as $checkbox) {
$checkbox->vars['checked'] = isset($tagsData[$type]) && in_array($checkbox->vars['value'], $tagsData[$type]);
}
}
}
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver ) {
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\TShirShirt',
));
$resolver->setRequired(['colours', 'sizes', 'materials']);
}
public function getName() {
return 'app_bundle_tshirt_form';
}
}
I finally solved the problem by creating a custom DataMapper as this tutorial explains
https://webmozart.io/blog/2015/09/09/value-objects-in-symfony-forms/
Related
I'm working on a Symfony project and I'm currently encountering a problem with a form that I want to pre-fill from a collection of objects.
The form field in question is of type EntityType::class. I would like to pre-select elements of this field from a collection that contains objects of the same type(Classe).
One of the solutions I found is to add a 'data' => $defaultClass property in the buildForm, which would contain the data to be inserted, and to pass this object in the parameters($options) on the formBuilder.
Unfortunately, the two objects of type Classe do not appear in the field once the form is generated even though the $options variable contains the objects.
Thanks in advance for your help. Here are the codes concerned :
SearchCourseData
<?php
namespace App\Data;
use App\Entity\Classe;
use App\Entity\Teacher;
use App\Entity\Location;
class SearchCourseData
{
/**
* #var integer
*/
public $page = 1;
/**
* #var Classe[]
*/
public $classe = [];
// Missing lines
/**
* #var String
*/
public $status;
}
Code SearchCourseForm
class SearchCourseForm extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$defaultClass = new Classe();
if (!empty($options['data']->classe)) {
$defaultClass = $options["data"]->classe;
}
$builder
->add('classe', EntityType::class, [
'class' => Classe::class,
'label' => false,
'required' => false,
'expanded' => false,
'multiple' => true,
'query_builder' => function (ClasseRepository $qb) {
return $qb->createQueryBuilder('a')->orderBy('a.title', 'ASC');
},
'choice_label' => function (Classe $atelier) {
return($atelier->getTitle());
},
'attr' => [
'placeholder' => 'Atelier',
'class' => 'select-classes'
],
'data' => $defaultClass,
])
>add('status', ChoiceType::class, [
'required' => true,
'choices' => [
'Disponible' => "Disponible",
'Brouillon' => "Brouillon",
'Archivé' => "Archivé"
],
'label' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class' => SearchCourseData::class,
'method' => 'GET',
'csrf_protection' => false
]);
}
public function getBlockPrefix() {
return '';
}
}
Code CourseController
/**
* #Route("/course")
*/
class CourseController extends AbstractController {
/**
* #Route("/",name="course")
*/
public function courseList(CourseRepository $courseRepository, Request $request) {
$data = new SearchCourseData();
$defaultClassB = $this->getDoctrine()->getRepository(Classe::class)->find(49);
$defaultClassA = $this->getDoctrine()->getRepository(Classe::class)->find(1);
$defaultClass[] = new ArrayCollection();
$defaultClass[0] = $defaultClassA;
$defaultClass[1] = $defaultClassB;
$data->classe = $defaultClass;
$form = $this->createForm(SearchCourseForm::class, $data);
}
}
#V-light is right you are setting a collection in an index of an array - then overwrite it...
$defaultClass[] = new ArrayCollection();
// === $defaultClass[0] = new ArrayCollection();
the correct and easiest way would be:
/**
* #Route("/course")
*/
class CourseController extends AbstractController {
/**
* #Route("/",name="course")
*/
public function courseList(CourseRepository $courseRepository, Request $request) {
$data = new SearchCourseData();
$data->classe = $this->getDoctrine()->getRepository(Classe::class)->findById([49, 1]);
$form = $this->createForm(SearchCourseForm::class, $data);
}
}
EDIT - the corrected form:
'data' of classe gets set by the ModelTransformer automaticly.
class SearchCourseForm extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('classe', EntityType::class, [
'class' => Classe::class,
'label' => false,
'required' => false,
'expanded' => false,
'multiple' => true,
'query_builder' => function (ClasseRepository $qb) {
return $qb->createQueryBuilder('a')->orderBy('a.title', 'ASC');
},
'choice_label' => function (Classe $atelier) {
return($atelier->getTitle());
},
'attr' => [
'placeholder' => 'Atelier',
'class' => 'select-classes'
],
])
>add('status', ChoiceType::class, [
'required' => true,
'choices' => [
'Disponible' => "Disponible",
'Brouillon' => "Brouillon",
'Archivé' => "Archivé"
],
'label' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class' => SearchCourseData::class,
'method' => 'GET',
'csrf_protection' => false
]);
}
}
Quick explanation of the subject:
A "Project" can have several "Activities"
I had to create simultaneously as many "Activities" as needed (You click on a button "Add an activity" and you got a new line which means a new activity). I managed to do that by doing Collections on my fields.
Here's the code of the formBuilder :
namespace CoreBundle\Form\Type;
use CoreBundle\Entity\Activities;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CreateNewActivitiesType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'aeAmount',
CollectionType::class,
array(
'entry_type' => TextType::class,
'allow_add' => true,
'allow_delete' => true,
'label' => 'Montant de l\'AE',
'label_attr' => array(
'class' => 'aeAmount visuallyhidden'
),
'entry_options' => array(
'attr' => array(
'placeholder' => '80 000 €'
),
),
'required' => true
)
)
->add(
'amountSpent',
CollectionType::class,
array(
'entry_type' => TextType::class,
'allow_add' => true,
'allow_delete' => true,
'label' => 'RDP : ',
'label_attr' => array(
'class' => 'amountSpent visuallyhidden'
),
'entry_options' => array(
'attr' => array(
'placeholder' => '35 000 €'
)
),
'required' => true,
)
)
->add(
'afName',
CollectionType::class,
array(
'entry_type' => TextType::class,
'allow_add' => true,
'allow_delete' => true,
'required' => true,
'label' => 'AF : ',
'label_attr' => array(
'class' => 'afName visuallyhidden',
),
'entry_options' => array(
'attr' => array(
'placeholder' => 'AERT-496'
)
)
)
)
->add(
'year',
CollectionType::class,
array(
'entry_type' => TextType::class,
'allow_delete' => true,
'allow_add' => true,
'required' => true,
'entry_options' => array(
'attr' => array(
'readonly' => true
)
),
'label' => 'Année : ',
'label_attr' => array(
'class' => 'year visuallyhidden'
)
)
)
;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'CoreBundle\Entity\Activities'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'corebundle_collection_activities';
}
}
(I know i can do this much shorter, but refactorisation will be later.)
So, this is currently working, with his associated controller to add my activities, here it is :
/**
* #param integer $projectId
* #param string $projectType
* #param integer $rdId
*
* #return \Symfony\Component\HttpFoundation\Response
*/
public function createNewActivityAction($projectId, $projectType, $rdId)
{
$activity = new Activities();
$request = $this->get('request_stack')->getCurrentRequest();
$form = $this->createForm(CreateNewActivitiesType::class, $activity);
$form->handleRequest($request);
$em = $this->getDoctrine()->getManager();
if ($form->isSubmitted() && $form->isValid()) {
$rdAffiliated = $em->getRepository('CoreBundle:DecisionStatement')->findOneBy(['id' => $rdId]);
$formData = $form->getData();
$yearList = $formData->getYear();
$aeAmountList = $formData->getAeAmount();
$afNameList = $formData->getAfName();
$amountSpentList = $formData->getAmountSpent();
for ($i = 0; $i < count($yearList); $i++) {
$yearDatetime = new DateTime($yearList[$i] . '-01-01 00:00:00');
$existingActivity = $em->getRepository('CoreBundle:Activities')->getExistingActivityWithoutIdentifier(
$yearDatetime,
$rdAffiliated
);
if ($existingActivity) {
/**
* #var Activities $currentActivity
*/
$currentActivity = $existingActivity[0];
$currentActivity->setAeAmount(
$currentActivity->getAeAmount() + $aeAmountList[$i]
);
$currentActivity->setAmountSpent(
$currentActivity->getAmountSpent() + $amountSpentList[$i]
);
$em->persist($currentActivity);
} else {
$newActivity = new Activities();
$newActivity->setYear($yearDatetime)
->setAeAmount($aeAmountList[$i])
->setAfName($afNameList[$i])
->setAmountSpent($amountSpentList[$i])
->setAfReception(false)
->setDecisionStatement($rdAffiliated)
->setPeopleByMonth(0);
$em->persist($newActivity);
}
}
$em->flush();
return $this->redirectToRoute('rd_show_activities', array(
'rdId' => $rdId,
'projectType' => $projectType,
'projectId' => $projectId
));
}
return $this->render('#Core/html/13-edit-activite.html.twig', array(
'page' => 'activities_creation',
'createActivitiesForm' => $form->createView(),
'projectParentId' => $projectId,
'projectParentType' => $projectType,
'rdId' => $rdId
));
}
Here is also a screenshot from the var_dump when activities form is submitted :
But where's my problem begins, it is when I want to edit because my form is based on the entity "Activities". But I want to edit all the existing "Activities" for a given project, I'll have an array containing my "Activities" objects (found by the findBy method), so I can't pass my array into my form, which results in an error.
How to transform this array of many "Activities" objects into only one "Activities" object?
I don't understand why you have only one object "Activities" that contains several collection.
You should create an object activities that contains several activities.
Each activity has only one attribute "year", "amountSpent"...
When you edit your object "ActivitieS" with the appropriate form you will be able to edit each activity linked with this object.
Embed a collection of forms
To correctly manage this kind of problem you need to build a Collection of forms in Symfony.
A "Project" can have several "Activities"
I had to create simultaneously as many "Activities" as needed [...]
At first you need to build a Project entity that will contain all the Activities as a Doctrine ArrayCollection as you can see below:
src/AppBundle/Entity/Project.php
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
class Project
{
protected $activities;
public function __construct()
{
$this->activities = new ArrayCollection();
}
public function getTags()
{
return $this->tags;
}
}
src/AppBundle/Form/Type/ProjectType.php
namespace AppBundle\Form\Type;
use AppBundle\Entity\Project;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
class ProjectType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('tags', CollectionType::class, array(
'entry_type' => ActivitiesType::class,
'allow_add' => true,
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Project::class,
));
}
}
Then you need to enable the user to dynamically add an Activity to a Project you need to make advantage of the prototype variable, using Twig and JavaScript.
I've taken into consideration all the comments that you made and decided to review the whole thing.
DecisionStatement.php :
Added in the __construct() method a new ArrayCollection for the Activities affiliated for a given DecisionStatement.
The method to return the Activities affiliated was already here.
documents = new ArrayCollection();
$this->activitiesAffiliated = new ArrayCollection();
}
/**
* Get activitiesAffiliated
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getActivitiesAffiliated()
{
return $this->activitiesAffiliated;
}
I've created a new FormType (DecisionStatementType.php), to have my Collection of forms :
use CoreBundle\Entity\DecisionStatement;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DecisionStatementType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'activitiesAffiliated',
CollectionType::class,
array(
'entry_type' => NewActivityType::class,
'allow_add' => true,
'allow_delete' => true
)
)
;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => DecisionStatement::class
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'corebundle_collection_decisionstatement';
}
}
And here is the "subForm", newActivityType.php :
use CoreBundle\Entity\Activities;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class NewActivityType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'aeAmount',
TextType::class,
array(
'label' => 'Montant de l\'AE',
'label_attr' => array(
'class' => 'aeAmount visuallyhidden'
),
'attr' => array(
'placeholder' => '80 000 €'
),
'required' => true
)
)
->add(
'amountSpent',
TextType::class,
array(
'label' => 'RDP : ',
'label_attr' => array(
'class' => 'amountSpent visuallyhidden'
),
'attr' => array(
'placeholder' => '35 000 €'
),
'required' => true,
)
)
->add(
'afName',
TextType::class,
array(
'required' => true,
'label' => 'AF : ',
'label_attr' => array(
'class' => 'afName visuallyhidden',
),
'attr' => array(
'placeholder' => 'AERT-496'
)
)
)
;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'CoreBundle\Entity\Activities'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'corebundle_collection_activities';
}
}
In my view's DOM, i have now this :
FormCollection Data-Prototype in DOM
So, is this a bit cleaner ?
I have a form which looks roughly like this: first there is the ItemType:
class ItemType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class, array())
->add('item_user_role', CollectionType::class, array(
'entry_type' => 'MyBundle\Form\ItemUserRoleType',
'allow_add' => true,
'mapped' => false,
'constraints' => array(
new ItemUserRole()
)
))
;
}
...
}
Then there is the ItemUserRoleType:
class ItemUserRoleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'id',
TextType::class,
array(
'mapped' => false,
'constraints' => array(),
)
)
->add('roleid', EntityType::class, array(
'class' => 'MyBundle\Entity\Role',
'placeholder' => 'Select Role',
'required' => false,
'choice_label' => 'name',
'multiple' => false,
'expanded' => false,
'constraints' => array(
new NotNull(),
new NotBlank()
)
))
->add('userid', EntityType::class, array(
'class' => 'MyBundle\Entity\User',
'required' => false,
'choice_label' => 'LastName',
'multiple' => false,
'expanded' => false,
'constraints' => array(
new NotNull(),
new NotBlank()
)
))
;
}
}
What I want to achieve is that a submitted form can either contain only userid and roleid (which is used to create a new ItemUserRole) or contain an id as well (which is used to edit an existing ItemUserRole).
My Problem is now that in my custom Validator "ItemUserRole" I need to access the ID to make sure if the ID is set, then the user is allowed to modify it. But since it is not mapped, I haven't yet found out how to do that.
What I tried was this:
class ItemUserRoleValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
$tmp = $this->context->getRoot()->get('item_user_role')->getData()[0];
var_dump($tmp->getId());
return;
}
}
But that returns NULL. Any hints? Thanks in advance.
Use the $value variable.
class ItemUserRoleValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
$tmp = $value[0];
var_dump($tmp->getId());
return;
}
}
$this->context, doesn't contain any form data, but the collection of all the constraints violations discovered at this time.
I'm currently having trouble with shutting down some validation constraints if a certain option is selected in the form. This option is not related to the model and is not set using the data_class option.
My form contains two embedded address forms which are exactly the same as in one invoice address and one shipment address. If the alternative option is selected, I want to make the shipment address form required through validation. If no alternative option is selected the shipment address form requires no validation and needs to be left alone.
CustomerCheckoutForm
class CustomerCheckoutForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id', 'hidden')
->add('firstName', 'text')
->add('nameAdditions', 'text')
->add('lastName', 'text')
->add('gender', 'choice', array('expanded' => true, 'choices' => array('m' => 'M', 'f' => 'F')))
->add('birthDate', 'date', array(
'required' => true,
'input' => 'string',
'widget' => 'choice',
'years' => range(DATE_CURRENT_YEAR - 80, DATE_CURRENT_YEAR - 18),
'empty_value' => array('year' => 'Year', 'month' => 'Month', 'day' => 'Day')
))
->add('invoiceAddress', new AddressForm($options['countryMapper']), array(
'label' => false,
'required' => true,
))
->add('alternative_shipment_address', 'choice', array(
'expanded' => true,
'choices' => array(0 => 'Delivery on the current address', 1 => 'Alternative shipping address'),
'mapped' => false,
'required' => true,
'label' => 'Delivery address',
'data' => 0,
))
->add('shipmentAddress', new AddressForm($options['countryMapper']), array(
'label' => false,
'required' => false,
))
->add('Continue', 'submit');
$this->registerListeners($builder);
}
private function registerListeners(FormBuilderInterface $builder)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) {
$customer = $event->getData();
$form = $event->getForm();
if (!$customer || $customer->getId() === null) {
$form->add('password', 'password');
}
});
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'EHV\SharedBundle\Model\Customer',
'countryMapper' => null,
'validation_groups' => array(
'Default',
'checkout',
),
));
}
public function getName()
{
return 'customerCheckout';
}
}
AddressForm
class AddressForm extends AbstractType
{
private $countryMapper;
public function __construct(CountryMapper $countryMapper)
{
$this->countryMapper = $countryMapper;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new CountryToIdTransformer($this->countryMapper);
$countries = $this->countryMapper->getAll();
$data[''] = 'Make a choice...';
/** #var Country $country */
foreach ($countries as $country) {
$data[$country->getId()] = $country->getName();
}
$builder
->add('id', 'hidden')
->add('streetName', 'text')
->add('streetNumber', 'text')
->add('streetNumberAddition', 'text')
->add('postalCode', 'text')
->add('city', 'text')
->add(
$builder->create('country', 'choice', array('choices' => $data))
->addModelTransformer($transformer)
);
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'EHV\SharedBundle\Model\Address',
));
}
public function getName()
{
return 'address';
}
}
Both models have their own constraints set through the function:
public static function loadValidatorMetadata(ClassMetaData $metadata) {}
Checkout Callback constraints http://symfony.com/doc/current/reference/constraints/Callback.html
You can read the value of your addresses and rise error if needed.
I managed to use a Form event listener with the Constraint Callback answered by Hpatoio, details below.
CustomerCheckoutForm
$builder->addEventListener(FormEvents::SUBMIT, function(FormEvent $event) {
$form = $event->getForm();
$customer = $event->getData();
if ($form->get('alternative_shipment_address')->getData() === 0) {
$customer->setShipmentAddress(null);
}
});
CustomerConstraint
$callback = function($customer) use ($metadata) {
$metadata->addPropertyConstraint('invoiceAddress', new Valid());
/** #var Customer $customer */
if ($customer->getShipmentAddress() !== null) {
$metadata->addPropertyConstraint('shipmentAddress', new Valid());
}
};
$metadata->addConstraint(new Callback($callback));
Credits to Hpatoio for pointing out the right direction.
I've this Form:
OrdersType
class OrdersType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Others $builder fields goes here
if ($this->register_type[0] == "natural")
{
$builder->add('person', new NaturalPersonType(), array('label' => FALSE));
}
elseif ($this->register_type[0] == "legal")
{
$builder->add('person', new LegalPersonType(), array('label' => FALSE));
}
}
}
PersonType
class PersonType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('description', 'text', array(
'required' => TRUE,
'label' => FALSE
))
->add('contact_person', 'text', array(
'required' => FALSE,
'label' => 'Persona de Contacto'
));
}
}
This is what I'm doing in the controller:
public function editAction($id = null)
{
$em = $this->getDoctrine()->getManager();
$order = $em->getRepository('FrontendBundle:Orders')->find($id);
$type = $order->getPerson()->getPersonType() === 1 ? "natural" : "legal";
$orderForm = $this->createForm(new OrdersType(array($type)), $order, array(
'action' => $this->generateUrl('update-order', array('id' => $id)),
'method' => 'POST',
));
return array(
'entity' => $order,
"form" => $orderForm->createView(),
'id' => $id
);
}
Then in my view I'm rendering fields as follow:
{{ form_widget(form.person.person.description) }}
The code renders the field right but without values and yes, it has values, where is the error? This are the others Forms I'm using:
LegalPersonType
class LegalPersonType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('rif', 'text', array(
'required' => true,
'label' => false,
'attr' => array(
'maxlength' => 10,
))
)
->add('identification_type', 'choice', array(
'label' => FALSE,
'choices' => RifType::getChoices(),
'attr' => array(
'class' => 'toSelect2'
)
))
->add('person', new PersonType(), array('label' => FALSE));
}
}
Working on mapping
PersonType
class PersonType extends AbstractType {
....
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\Person',
'inherit_data' => true
));
}
public function getName()
{
return 'person';
}
}
NaturalPersonType
class NaturalPersonType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('identification_type', 'choice', array(
'label' => 'Número de Cédula',
'choices' => CIType::getChoices()
))
->add('ci', 'number', array(
'required' => true,
'label' => false,
'attr' => array(
'maxlength' => 8,
))
)
->add('person', new PersonType(), array(
'label' => FALSE,
'data_class' => 'Tanane\FrontendBundle\Entity\NaturalPerson'
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\NaturalPerson'
));
}
public function getName()
{
return 'natural_person';
}
}
If I remove the setDefaultOptions method from NaturalPersonType I get this error:
The form's view data is expected to be of type scalar, array or an
instance of \ArrayAccess, but is an instance of class
Tanane\FrontendBundle\Entity\NaturalPerson. You can avoid this error
by setting the "data_class" option to
"Tanane\FrontendBundle\Entity\NaturalPerson" or by adding a view
transformer that transforms an instance of class
Tanane\FrontendBundle\Entity\NaturalPerson to scalar, array or an
instance of \ArrayAccess.
If I leave as it's I get this other:
Method "description" for object "Symfony\Component\Form\FormView" does
not exist in
/var/www/html/tanane/src/Tanane/BackendBundle/Resources/views/Order/edit.html.twig
at line 134
It looks like PersonType is not being mapped correctly to your entity.
Since it's being used both by LegalPersonType and NaturalPersonType to handle some of their properties, I would define it as parent of those two. This can be achieved by using the inherit_data option (documentation).
Your code would look something like this:
PersonType
class PersonType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('description', 'text', array(
'required' => TRUE,
'label' => FALSE
))
->add('contact_person', 'text', array(
'required' => FALSE,
'label' => 'Persona de Contacto'
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'inherit_data' => true,
));
}
}
Note that you should remove the data_class option from setDefaultOptions().
LegalPersonType
class LegalPersonType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('rif', 'text', array(
'required' => true,
'label' => false,
'attr' => array(
'maxlength' => 10,
))
)
->add('identification_type', 'choice', array(
'label' => FALSE,
'choices' => RifType::getChoices(),
'attr' => array(
'class' => 'toSelect2'
)
))
->add('data', new PersonType(), array(
'label' => FALSE,
'data_class' => 'YourBundle\Entity\LegalPerson',
));
}
}
Your NaturalPersonType would look similar to LegalPersonType.
Now, you can do this in your view:
{{ form_widget(form.person.data.description) }}