In a form (ContractType) I need the association to a Customer, which itself has an association to a Company and a BillingAddress. But the data itself of these 3 "nested entities" should not be editable at all in the contract view. That's why the field for the customer ManyToOne-Relation in the contract is hidden.
However for display purposes I need the whole customer-object in the form too (incl. the underlying company and billing address) - but as read-only. This is why I added an additional field customerFullObject to the contract form-type - which is not mapped (mapped => false). As it is an unmapped field, I set the data via a form event.
Now, when I edit the contract data, the company- and billing-address-data of the customer is nullified.
If I remove/comment the whole customerFullObject part, everything is fine.
I don't understand, how/why an unmapped field can change the data of an entity.
How can I prevent Symfony from this? Or is there a better "pattern"/way to achieve what I want? I couldn't find anything in the docs.
Below some code snippets from the model (removed namespaces for briefness):
Contract.php
/**
* #ORM\Entity
*/
class Contract {
/**
* #var Customer
* #ORM\ManyToOne(targetEntity="Customer", inversedBy="contracts")
* #Assert\Type(type="Customer")
* #Assert\NotBlank(message = "customer.notBlank")
**/
private $customer;
// Other fields left out as irrelevant.
}
Customer.php
/**
* #ORM\Entity
*/
class Customer {
/**
* #var Company
* #ORM\OneToOne(targetEntity="Company")
* #Assert\NotBlank(message="company.notBlank")
**/
private $company;
/**
* #var Address
* #ORM\OneToOne(targetEntity="Address")
* #Assert\NotBlank(message="billingAddress.notBlank")
**/
private $billingAddress;
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="Contract", mappedBy="customer", orphanRemoval=true, cascade={"all"})
**/
private $contracts;
// Other fields left out as irrelevant.
}
ContractType.php
class ContractType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add(
'customer',
'customerHiddenType',
array(
'label' => 'contract.customer',
)
);
$builder->add(
'customerFullObject',
'customerType',
array(
'cascade_validation' => false,
'mapped' => false
)
);
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$editMode = $form->getConfig()->getOption('editMode');
if (!empty($editMode) && $editMode === RequestAction::CREATE()) {
$form->remove('customerFullObject');
} else {
$form->get('customerFullObject')->setData($event->getData()->getCustomer());
}
}
);
// Other children left out as irrelevant.
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(
array(
'data_class' => 'Contract',
'cascade_validation' => true
)
);
}
public function getName() {
return 'contractType';
}
}
CustomerType.php
class CustomerType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add(
'company',
'companyType',
array(
'cascade_validation' => false,
'label' => 'customer.company'
)
);
$builder->add(
'billingAddress',
'addressType',
array(
'cascade_validation' => false,
'label' => 'customer.billingAddress'
)
);
// Other children left out as irrelevant.
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(
array(
'data_class' => 'Customer',
'cascade_validation' => true
)
);
}
public function getName() {
return 'customerType';
}
}
Related
I'm using symfony 2.3, so apparently, I can't use the 'allow_extra_fields' option discussed here.
I have a main Form Type, RegistrationStep1UserType :
/**
* Class RegistrationStep1UserType
* #package Evo\DeclarationBundle\Form\Type
*/
class RegistrationStep1UserType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('customer', new RegistrationStep1CustomerType(), [
'label' => false,
'data_class' => 'Evo\UserBundle\Entity\Customer',
'cascade_validation' => true,
])
->add('declaration', 'evo_declaration_bundle_registration_step1_declaration_type', [
'label' => false,
'cascade_validation' => true,
])
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Evo\UserBundle\Entity\User',
'validation_groups' => false,
));
}
/**
* #return string
*/
public function getName()
{
return 'evo_declaration_bundle_registration_step1_user_type';
}
}
This form type includes an embedded Form Type (on "declaration" field), RegistrationStep1DeclarationType, registered as a service :
/**
* Class RegistrationStep1DeclarationType
* #package Evo\DeclarationBundle\Form\Type
*/
class RegistrationStep1DeclarationType extends AbstractType
{
/**
* #var EntityManagerInterface
*/
private $em;
/**
* #var EventSubscriberInterface
*/
private $addBirthCountyFieldSubscriber;
/**
* RegistrationStep1DeclarationType constructor.
* #param EntityManagerInterface $em
* #param EventSubscriberInterface $addBirthCountyFieldSubscriber
*/
public function __construct(EntityManagerInterface $em, EventSubscriberInterface $addBirthCountyFieldSubscriber)
{
$this->em = $em;
$this->addBirthCountyFieldSubscriber = $addBirthCountyFieldSubscriber;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('birthLastname', null, [
'label' => 'Nom de naissance',
'attr' => [
'required' => true,
],
])
->add('nationality', 'entity', [
'label' => 'Nationalité',
'class' => 'Evo\GeoBundle\Entity\Country',
'property' => 'nationalityFr',
'attr' => [
'required' => true,
'class' => 'selectpicker',
],
'preferred_choices' => $this->fillPreferredNationalities(),
])
->add('birthCountry', 'entity', [
'label' => 'Pays de naissance',
'class' => 'Evo\GeoBundle\Entity\Country',
'property' => 'nameFr',
'empty_value' => '',
'empty_data' => null,
'attr' => [
'required' => true,
'class' => 'trigger-form-modification selectpicker',
],
'preferred_choices' => $this->fillPreferredBirthCountries(),
])
->add('birthCity', null, [
'label' => 'Ville de naissance',
'attr' => [
'required' => true,
],
])
;
$builder->get("birthCountry")->addEventSubscriber($this->addBirthCountyFieldSubscriber);
}
/**
* #return array
*/
private function fillPreferredNationalities()
{
$nationalities = $this->em->getRepository("EvoGeoBundle:Country")->findBy(["isDefault" => true]);
return $nationalities;
}
/**
* #return array|\Evo\GeoBundle\Entity\Country[]
*/
private function fillPreferredBirthCountries()
{
$countries = $this->em->getRepository("EvoGeoBundle:Country")->findBy(["isDefault" => true]);
return $countries;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'required' => false,
'data_class' => 'Evo\DeclarationBundle\Entity\Declaration',
'validation_groups' => false,
));
}
/**
* #return string
*/
public function getName()
{
return 'evo_declaration_bundle_registration_step1_declaration_type';
}
}
This embedded Form Type adds a Subscriber (registered as a service too, because it needs injection of EntityManager) on the "birthCountry" field.
The goal is to dynamically add or remove an extra field (called "birthCounty") depending on the value of the birthCountry choice list (note the 2 fields are different here, "birthCountry" and "birthCounty").
Here is the Subscriber :
/**
* Class AddBirthCountyFieldSubscriber
* #package Evo\CalculatorBundle\Form\EventListener
*/
class AddBirthCountyFieldSubscriber implements EventSubscriberInterface
{
/**
* #var EntityManagerInterface
*/
private $em;
/**
* AddBirthCountyFieldSubscriber constructor.
* #param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* #return array
*/
public static function getSubscribedEvents()
{
return array(
FormEvents::POST_SET_DATA => 'postSetData',
FormEvents::POST_SUBMIT => 'postSubmit',
);
}
/**
* #param FormEvent $event
*/
public function postSetData(FormEvent $event)
{
$birthCountry = $event->getData();
$form = $event->getForm()->getParent();
$this->removeConditionalFields($form);
if ($birthCountry instanceof Country && true === $birthCountry->getIsDefault()) {
$this->addBirthCountyField($form);
}
}
/**
* #param FormEvent $event
*/
public function postSubmit(FormEvent $event)
{
$birthCountry = $event->getData();
$form = $event->getForm()->getParent();
if (!empty($birthCountry)) {
$country = $this->em->getRepository("EvoGeoBundle:Country")->find($birthCountry);
$this->removeConditionalFields($form);
if ($country instanceof Country && true === $country->getIsDefault()) {
$this->addBirthCountyField($form);
}
}
}
/**
* #param FormInterface $form
*/
private function addBirthCountyField(FormInterface $form)
{
$form
->add('birthCounty', 'evo_geo_bundle_autocomplete_county_type', [
'label' => 'Département de naissance',
'attr' => [
'required' => true,
],
])
;
}
/**
* #param FormInterface $form
*/
private function removeConditionalFields(FormInterface $form)
{
$form->remove('birthCounty');
}
}
In the view, when the "birthCountry" choice list changes, it trigger an AJAX request to the controller, which handles the request and render the view again (as explained in the documentation about dynamic form submission) :
$form = $this->createForm(new RegistrationStep1UserType(), $user);
if ($request->isXmlHttpRequest()) {
$form->handleRequest($request);
} elseif ("POST" == $request->getMethod()) {
[...]
}
The problem is the following :
When I make a change on the birthCountry choice list and select a Country supposed to hide the "birthCounty" field, the form correctly render without that field, but it shows an error message :
Ce formulaire ne doit pas contenir des champs supplémentaires.
or
this form should not contain extra fields (in english)
I tried many different solutions :
adding a 'validation_groups' option to RegistrationStep1UserType and RegistrationStep1DeclarationType
adding a preSubmit event to AddBirthCountyFieldSubscriber replicating the logic of preSetData and postSubmit methods
even adding 'mapped' => false, to the birthCounty field triggers the error. very surprising
Even $form->getExtraData() is empty if I dump it just after $form->handleRequest($request);
But in vendor\symfony\symfony\src\Symfony\Component\Form\Extension\Validator\Constraints\FormValidator, I can see an extra field
array(1) {
["birthCounty"]=>
string(0) ""
}
here :
// Mark the form with an error if it contains extra fields
if (count($form->getExtraData()) > 0) {
echo '<pre>';
\Doctrine\Common\Util\Debug::dump($form->getExtraData());
echo '</pre>';
die();
$this->context->addViolation(
$config->getOption('extra_fields_message'),
array('{{ extra_fields }}' => implode('", "', array_keys($form->getExtraData()))),
$form->getExtraData()
);
}
Did I miss something about form dynamic extra fields ?
I did not analyzed all the question but, I guess, that you can invert the logic: always add that field and remove it when the condition is not satisfied.
That way you don't need to perform operations in postSubmit (that is where the issue is)
I'm using a Callback to validate an entity
/**
* #ORM\Entity(repositoryClass="AppBundle\Repository\Foo")
* #ORM\Table(name="foo")
* #Constraints\Callback(methods={"validate"})
*/
class Foo
{
...
function validate(ExecutionContextInterface $context)
{
if ($this->foos) {
$context->buildViolation('Foos cannot be emty')
->atPath('foos')
->addViolation();
}
}
A form is using this entity:
class FooFormType extends AbstractType
{
private $foosService;
function __construct(FoosService $foosService) {
$this->foosService = $foosService;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('foos', EntityType::class, array(
'label' => false,
'class' => 'AppBundle:Entity\Foo',
'choices' => $this->foosService->getSomeFoos($builder->getData()),
'expanded' => true,
'multiple' => true,
'required' => true,
))
->getForm();
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Foo',
));
}
}
This form is called within a factory:
public function getFooForm(Foo $foo)
{
return $this->formFactory->create(new FooFormType($this->foosService), $foo);
}
Finally, in the controller:
...
$form = $this->get('my_factory')->getFooForm($foo);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->getDoctrine()->getManager()->flush();
...
}
...
This seems to work correctly, but the validation is being done over the previous entity. I mean, if I submit empty foos I can see the validation error, but if I sumbit some foos, and then I remove those foosand submit again, the validation doesn't throw any exception because it's being done over the last entity, which had some foos. I've checked that the submitted data is correct, and indeed, Foo entity is been persisted with empty foos (only if the previous one had foos).
What can be causing this weird behaviour?
I have the Customer entity and two one-to-many relations CustomerPhone and CustomerAddress.
The Customer entity has addPhone/removePhone and addAddress/removeAddress "adders".
CustomerType collections options has 'by_reference' => false for both collections.
Entity functions addPhone/removePhone and addAddress/removeAddress not called after form submitted, so CustomerPhone and CustomerAddress have no parent id after persist.
Why could addPhone/removePhone and addAddress/removeAddress not called on form submit?
UPD 1.
After #Baig suggestion now I have addPhone/removePhone "adders" called, but addAddress/removeAddress not. Can't get why because they are identical.
# TestCustomerBundle/Entity/Customer.php
/**
* #var string
*
* #ORM\OneToMany(targetEntity="CustomerPhone", mappedBy="customerId", cascade={"persist"}, orphanRemoval=true)
*/
private $phone;
/**
* #var string
*
* #ORM\OneToMany(targetEntity="CustomerAddress", mappedBy="customerId", cascade={"persist"}, orphanRemoval=true)
*/
private $address;
Same file "adders"
# TestCustomerBundle/Entity/Customer.php
/**
* Add customer phone.
*
* #param Phone $phone
*/
public function addPhone(CustomerPhone $phone) {
$phone->setCustomerId($this);
$this->phone->add($phone);
return $this;
}
/**
* Remove customer phone.
*
* #param Phone $phone customer phone
*/
public function removePhone(CustomerPhone $phone) {
$this->phone->remove($phone);
}
/**
* Add customer address.
*
* #param Address $address
*/
public function addAddress(CustomerAddress $address) {
$address->setCustomerId($this);
$this->address->add($address);
return $this;
}
/**
* Remove customer address.
*
* #param Address $address customer address
*/
public function removeAddress(CustomerAddress $address) {
$this->address->remove($address);
}
Relations:
# TestCustomerBundle/Entity/CustomerPhone.php
/**
* #ORM\ManyToOne(targetEntity="Customer", inversedBy="phone")
* #ORM\JoinColumn(name="customer_id", referencedColumnName="id")
**/
private $customerId;
#TestCustomerBundle/Entity/CustomerAddress.php
/**
* #ORM\ManyToOne(targetEntity="Customer", inversedBy="address")
* #ORM\JoinColumn(name="customer_id", referencedColumnName="id")
**/
private $customerId;
CustomerType form:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('phone', 'collection', array(
'type' => new CustomerPhoneType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'options' => array('label' => false)
))
->add('address', 'collection', array(
'type' => new CustomerAddressType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'options' => array('label' => false)
))
->add('submit', 'submit')
;
}
Controller.
# TestCustomerBundle/Controller/DefaultController.php
public function newAction(Request $request)
{
$customer = new Customer();
// Create form.
$form = $this->createForm(new CustomerType(), $customer);
// Handle form to store customer obect with doctrine.
if ($request->getMethod() == 'POST')
{
$form->bind($request);
if ($form->isValid())
{
/*$em = $this->get('doctrine')->getEntityManager();
$em->persist($customer);
$em->flush();*/
$request->getSession()->getFlashBag()->add('success', 'New customer added');
}
}
// Display form.
return $this->render('DeliveryCrmBundle:Default:customer_form.html.twig', array(
'form' => $form->createView()
));
}
UPD 2.
Test if addAddress called.
/**
* Add customer address.
*
* #param Address $address
*/
public function addAddress(Address $address) {
jkkh; // Test for error if method called. Nothing throws.
$address->setCustomerId($this);
$this->address->add($address);
}
UPD 3.
CustomerAddressType.php
<?php
namespace Delivery\CrmBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class CustomerAddressType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('street')
->add('house')
->add('building', 'text', ['required' => false])
->add('flat', 'text', ['required' => false])
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Delivery\CrmBundle\Entity\CustomerAddress'
));
}
/**
* #return string
*/
public function getName()
{
return 'delivery_crmbundle_customeraddress';
}
}
CustomerPhoneType.php
<?php
namespace Delivery\CrmBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class CustomerPhoneType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('number')
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Delivery\CrmBundle\Entity\CustomerPhone'
));
}
/**
* #return string
*/
public function getName()
{
return 'phone';
}
}
For me this was eventually solved by adding getXXX, which returns the collection to the PropertyAccessor. Without that you keep on wondering why addXXX or removeXXX aren't called.
So make sure that:
The option by_reference is set to false at the field,
You have both the adder and remover method on the owning side of the relationship,
The getter is accessible for the PropertyAccessor to check if by_reference can be used,
If you want to use the prototype to handle adding/removing via Javascript, make sure allow_add is set to true.
This answer corresponds to Symfony 3, but I am sure this applies to Symfony 2 as well. Also this answer is more as a reference than addressing OP's issue in particular (which I am not to clear)
On ..Symfony/Component/PropertyAccess/PropertyAccessor.php the method writeProperty is responsible for calling either setXXXXs or addXXX & removeXXXX methods.
So here is order on which it looks for the method:
If the entity is array or instance of Traversable (which ArrayCollection is) then pair of
addEntityNameSingular()
removeEntityNameSingular()
Source for reference:
if (is_array($value) || $value instanceof \Traversable) {
$methods = $this->findAdderAndRemover($reflClass, $singulars);
if (null !== $methods) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER;
$access[self::ACCESS_ADDER] = $methods[0];
$access[self::ACCESS_REMOVER] = $methods[1];
}
}
If not then:
setEntityName()
entityName()
__set()
$entity_name (Should be public)
__call()
Source for reference:
if (!isset($access[self::ACCESS_TYPE])) {
$setter = 'set'.$camelized;
$getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item)
if ($this->isMethodAccessible($reflClass, $setter, 1)) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $setter;
} elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD;
$access[self::ACCESS_NAME] = $getsetter;
} elseif ($this->isMethodAccessible($reflClass, '__set', 2)) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
$access[self::ACCESS_NAME] = $property;
} elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY;
$access[self::ACCESS_NAME] = $property;
} elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) {
// we call the getter and hope the __call do the job
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC;
$access[self::ACCESS_NAME] = $setter;
} else {
$access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND;
$access[self::ACCESS_NAME] = sprintf(
'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '.
'"__set()" or "__call()" exist and have public access in class "%s".',
$property,
implode('', array_map(function ($singular) {
return '"add'.$singular.'()"/"remove'.$singular.'()", ';
}, $singulars)),
$setter,
$getsetter,
$reflClass->name
);
}
}
To answer OP's issue, based on the above mentioned information, the PropertyAccessor class of symfony is not able to read your addXX and removeXX method properly. The potential reason might be that is not identified as array or ArrayCollection which has to be done from the constructor of the entity
public function __construct() {
$this->address = new ArrayCollection();
// ....
}
I had the same problem, but I'm not sure that it's the same cause.
I your entity's attribute that has OneToMany relation ship must have an 's' at the end. So that in "handleRequest" (leave it a black box, I didn't look up inside), symfony would find your "addxxx" without "s".
In the exemple 'Task - Tag', he declared "tags" but getTag.
In your case I would think you change your $phone to $phones and the method follow:
public function setPhones($phones){}
public function addPhone(Phone $phone){}
To the name of method your form searching for, just delete temporarily setter in your entity and submit your form, symfony will told you.
Just hope this would help you out :)
I have a problem when I want to handle form with collection
I have two entities FeatureCategory and Feature
User chooses one feature in every featureCategory (collection of featureCategories).
Feature category entity:
class FeatureCategory
{
/**
* #var \Doctrine\Common\Collections\Collection
* #ORM\OneToMany(targetEntity="Site\BackendBundle\Entity\Feature", mappedBy="featureCategory", cascade={"persist"})
*/
private $features;
/**
* Constructor
*/
public function __construct()
{
$this->features = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add feature
*
* #param \Site\BackendBundle\Entity\Feature $feature
* #return FeatureCategory
*/
public function addFeature(\Site\BackendBundle\Entity\Feature $feature)
{
$feature->setFeatureCategory($this);
$this->features[] = $feature;
return $this;
}
/**
* Remove feature
*
* #param \Site\BackendBundle\Entity\Feature $feature
*/
public function removeFeature(\Site\BackendBundle\Entity\Feature $feature)
{
$this->features->removeElement($feature);
}
/**
* Get features
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getFeatures()
{
return $this->features;
}
}
And feature entity:
class Feature
{
/**
* #var \Site\BackendBundle\Entity\FeatureCategory
*
* #ORM\ManyToOne(targetEntity="Site\BackendBundle\Entity\FeatureCategory", inversedBy="features")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="feature_category_id", referencedColumnName="id")
* })
*/
private $featureCategory;
/**
* Set featureCategory
*
* #param \Site\BackendBundle\Entity\FeatureCategory $featureCategory
* #return Feature
*/
public function setFeatureCategory(\Site\BackendBundle\Entity\FeatureCategory $featureCategory = null)
{
$this->featureCategory = $featureCategory;
return $this;
}
/**
* Get featureCategory
*
* #return \Site\BackendBundle\Entity\FeatureCategory
*/
public function getFeatureCategory()
{
return $this->featureCategory;
}
}
Feature categories form:
class FeatureCategoriesFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('featureCategories', 'collection', array(
'type' => 'site_frontend_feature_category',
'by_reference' => false
))
->add('quantity', 'text', array(
'data' => 1
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => array('feature_categories')
));
}
public function getName()
{
return 'site_frontend_feature_categories';
}
}
And feature category form:
class FeatureCategoryFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($builder)
{
$form = $event->getForm();
$data = $event->getData();
/* Check we're looking at the right data/form */
if ($data instanceof FeatureCategory)
{
$choices = $data->getFeatures();
$form
->add('features', 'entity', array(
'multiple' => false,
'expanded' => true,
'class' => 'SiteBackendBundle:Feature',
'property' => 'value',
'choices' => $choices,
'data' => null
))
->add('name', 'hidden', array(
'read_only' => true,
'label' => $data->getName()
))
;
}
});
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Site\BackendBundle\Entity\FeatureCategory',
'validation_groups' => array('feature_category')
));
}
public function getName()
{
return 'site_frontend_feature_category';
}
}
And finally my controller:
class OrderController extends Controller
{
public function addProductAction(Request $request, $slug)
{
$em = $this->get('doctrine.orm.default_entity_manager');
$item = $em->getRepository('SiteBackendBundle:Product')
->getItem($slug);
if (!$item) {
throw new NotFoundHttpException();
}
$featureCategories = $em->getRepository('SiteBackendBundle:FeatureCategory')
->getListByProduct($item);
$featureCategoriesForm = $this->createForm('site_frontend_feature_categories', array('featureCategories' => $featureCategories),
array(
'method' => 'POST',
'action' => $this->generateUrl('user_order_add_product', array(
'slug' => $item->getSlug(),
))
)
);
if ($request->isXmlHttpRequest()) {
if ($request->isMethod('POST')) {
$featureCategoriesForm->handleRequest($request);
if ($featureCategoriesForm->isValid()) {
//order add product logic
}
}
}
}
}
After $featureCategoriesForm->handleRequest($request); line, I got an error
Neither the property "features" nor one of the methods
"addFeatur()"/"removeFeatur()", "addFeature()"/"removeFeature()",
"setFeatures()", "features()", "__set()" or "__call()" exist and have
public access in class "Site\BackendBundle\Entity\FeatureCategory".
Sorry for this long post
Could anyone help?
Eventually, I fixed issue using entities as arrays with choice type form, without data_class and so on
I've got a form that is mapped to an entity ('data_class' => ...). I've got validators set up (via annotations) on the entity's properties.
The entity has a property (nameTranslations) of doctrine's type array. I created a custom field type composed of multiple fields that is assigned to this field in the form. Each of the subform's fields (of type text) has validators setup (NotBlank) via validation_constraint option.
I tried various validation annotations on the nameTranslations property, including Valid(). I tried settings error_bubbling on almost anything. The subform (field nameTranslations) doesn't get validated at all.
The subform:
class TranslatableTextType extends AbstractType
{
private $langs;
/**
* {#inheritDoc}
*/
public function __construct($multilang)
{
$this->langs = $multilang->getLangs();
}
/**
* {#inheritDoc}
*/
public function buildForm(FormBuilder $builder, array $options)
{
foreach($this->langs as $locale => $lang)
{
$builder->add($locale, 'text', array(
'label' => sprintf("%s [%s]", $options['label'], $lang),
'error_bubbling' => true,
));
}
}
/**
* {#inheritDoc}
*/
public function getDefaultOptions(array $options)
{
$constraints = [
'fields' => [],
'allowExtraFields' => true,
'allowMissingFields' => true,
];
foreach($this->langs as $locale => $lang)
{
$constraints['fields'][$locale] = new NotBlank();
}
$collectionConstraint = new Collection($constraints);
return [
'validation_constraint' => $collectionConstraint,
'label' => '',
'error_bubbling' => true
];
}
/**
* {#inheritDoc}
*/
public function getParent(array $options)
{
return 'form';
}
/**
* {#inheritDoc}
*/
public function getName()
{
return 'translatable_text';
}
}
In the main form:
$builder->add('nameTranslations', 'translatable_text', [
'label' => 'Name'
]);
In the entity:
/**
* #var array
*
* #Assert\Valid
* #ORM\Column(type="array")
*/
protected $nameTranslations;
I think you should use collection type for that instead of custom one or your custom type should have collection type defined as parent.
You can use All validator like:
/**
* #Assert\All({
* #Assert\NotBlank
* })
* #ORM\Column(type="array")
*/
protected $nameTranslations;
it will check if each of array value is not blank.