symfony: Validating form field, many-to-many-relationship - forms

I have three entities: User, Work and Journal. There is a many-to-many-relationship between User and Work defined as follows:
In the Work entity:
/**
* #ORM\ManyToMany(targetEntity="User", inversedBy="peerReviewedWorks")
* #ORM\JoinTable(name="test_work_user_peer")
*/
private $peerReviewers
In the User entity:
/**
* #ORM\ManyToMany(targetEntity="Work", mappedBy="peerReviewers")
*/
private $peerReviewedWorks;
There is no relationship between my third entity, Journal, and User but a one-to-many-relationship between Journal and Work:
Journal entity:
/**
* #ORM\OneToMany(targetEntity="Work", mappedBy="journal")
*/
private $works;
Work entity:
/**
* #ORM\ManyToOne(targetEntity="Journal", inversedBy="works")
* #ORM\JoinColumn(name="journal_id", referencedColumnName="id")
*/
private $journal;
From the Work controller I am creating a form which lets the editor choose the peer-reviewers from a list and assign them to a work:
<?php
public function manageAction(Request $request, Work $work)
{
// ...
$form = $this->createForm(ManageWorkType::class, $work);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// ...
}
}
The ManageWorkType class looks as follows:
<?php
class ManageWorkType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// add some stuff
->add('submit', SubmitType::class)
;
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$work = $event->getData();
$form = $event->getForm();
// form field only added when journal is peer-reviewed
if($work->getJournal()->getIsPeerReviewed() == 1) {
// get a list of all active peer-reviewers
$form->add('peerReviewers', EntityType::class, array(
'class' => 'PlatformBundle:User',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('u')
->where('u.peerReviewStatus = :status')
->setParameter('status', 'active')
;
},
'multiple' => true,
));
}
});
// ...
}
Since not every journal requires the same number of peer-reviews, I have a field number_of_peer_reviewers in the Journal entity which holds the number of required peer-reviewers:
Journal entity:
/**
* #var string
*
* #ORM\Column(name="numberOfPeerReviewers", type="string", length=1, nullable=true)
*/
private $numberOfPeerReviewers;
Before assigning the peer-reviewers from the User entity to the Work entity I have to check if the editor who submitted the form selected the required number of peer-reviewers from the list. If this is not the case the form should not be valid.
In order to achieve this I tried to use a callback validator in the Work entity but failed miserably:
/**
* #ORM\ManyToMany(targetEntity="User", inversedBy="peer_reviewed_works")
* #ORM\JoinTable(name="test_work_user_peer")
* #Assert\Valid
*/
private $peerReviewers;
// ...
/**
* #Assert\Callback
*/
public function validate(ExecutionContextInterface $context, $payload)
{
if(count($this->getPeerReviewers()) < $this->getPeerReviewers()->getNumberOfPeerReviewers()) {
$context->buildViolation('Error message')
->atPath('peerReviewers')
->addViolation();
}
}
As you can see I do not know how to access the number_of_peer_reviewers field from the Journal table.
Any guidance is greatly appreciated.

Related

How to get data in a form event within a CollectionType?

I have a problem with Symfony 4 on an issue already identified and described on Github (here: https://github.com/symfony/symfony/issues/5694#issuecomment-110398953) but I can't find a way to apply this answer.
When I try to use a POST_SET_DATA form event in a ChildType form, the function getData() gives me a null value because the "allow_add" option is set on true in the ParentType form which is a CollectionType.
I have 3 collections: Page, Moduling and Module. The Moduling document is used to embed a collection of Module forms. The purpose is to be able to add multiple forms to the Page collection with one request, following this Symfony article: https://symfony.com/doc/current/form/form_collections.html.
I have 2 different embedded documents: Tag and Task. Both of them are embedded in the Module document (EmbedOne). What I want to do is to be able to custom the ModuleType field with a form event listener so that I just need to set the title of the Module in the controller and then Symfony knows it needs to use the TaskType or the TagType within the ModuleType.
So first, here is my controller
class TaskingController extends Controller
{
/**
* The controller from which I set the module title, "task" here
*
* #Route("/{slug}/task/create", name="tasking_create")
*
* #ParamConverter("page", options={"mapping": {"slug": "slug"}})
*
* #return Response
*/
public function createTasking(DocumentManager $dm, $id, Module $module, Moduling $moduling)
{
$page = $dm->find(Page::class, $id);
$module->setTitle('task');
$moduling->addModule($module);
$page->addModuling($moduling);
$form = $this->createForm(ModulingType, $moduling);
$form->handleRequest($request);
if ($form->isValid() && $form->isSubmitted() {
// Form validation then redirection
}
// Render form template}
}
}
Now, here are my three collections: pages, moduling and modules
/**
* My page document
*
* #MongoDB\Document(collection="pages")
*/
class Page
{
/**
* #MongoDB\Id(strategy="AUTO")
*/
protected $id;
/**
* #MongoDB\ReferenceMany(targetDocument="App\Document\Moduling")
*
* #var Moduling
*/
protected $moduling = array();
public function __construct()
{
$this->moduling = new ArrayCollection();
}
/**
* Get the value of id
*/
public function getId()
{
return $this->id;
}
/**
* #return Collection $moduling
*/
public function getModuling()
{
return $this->moduling;
}
/**
* #param Moduling $moduling
*/
public function addModuling(Moduling $moduling)
{
$this->moduling[] = $moduling;
}
/**
* #param Moduling $moduling
*/
public function removeModuling(Moduling $moduling)
{
$this->moduling->removeElement($moduling);
}
}
/**
* #MongoDB\Document(collection="moduling")
*/
class Moduling
{
/**
* #MongoDB\Id(strategy="AUTO")
*/
protected $id;
/**
* #MongoDB\ReferenceOne(targetDocument="App\Document\Page", storeAs="id")
*
* #var Page
*/
protected $parentPage;
/**
* #MongoDB\ReferenceMany(targetDocument="App\Document\Module", mappedBy="moduling")
*/
protected $module = array();
public function __construct()
{
$this->module = new ArrayCollection();
}
/**
* Get the value of id
*/
public function getId()
{
return $this->id;
}
public function getModule()
{
return $this->module;
}
public function addModule(Module $module): self
{
$this->module[] = $module;
}
public function removeModule(Module $module)
{
$this->module->removeElement($module);
}
/**
* Get the value of parentPage
*
* #return Page
*/
public function getParentPage()
{
return $this->parentPage;
}
/**
* Set the value of parentPage
*
* #param Page $parentPage
*
* #return self
*/
public function setParentPage(Page $parentPage)
{
$this->parentPage = $parentPage;
return $this;
}
}
/**
* #MongoDB\Document(collection="modules")
*/
class Module
{
/**
* #MongoDB\Id(strategy="AUTO")
*/
public $id;
/**
* #MongoDB\Field(type="string")
*/
public $title;
/**
* #MongoDB\ReferenceOne(targetDocument="App\Document\Moduling", inversedBy="module", storeAs="id")
*/
public $moduling;
/**
* #MongoDB\EmbedOne(targetDocument="App\Document\Task", strategy="set")
* #Assert\Valid
*/
public $task;
public function getTitle()
{
return $this->title;
}
/**
* #return self
*/
public function setTitle($title)
{
$this->title = $title;
return $this;
}
public function getTask()
{
return $this->task;
}
public function setTask(Task $task = null)
{
$this->task = $task;
}
}
My embedded document Task. The Tag document has the same structure.
/**
* #MongoDB\EmbeddedDocument
*/
class Task
{
/**
* #MongoDB\Id(strategy="AUTO")
*/
protected $id;
public function getId()
{
return $this->id;
}
}
My ModulingType, which is a collection of ModuleType
class ModulingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('module', CollectionType::class, [
'entry_type' => ModuleType::class,
'entry_options' => [
'label' => false,
],
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Moduling::class
]);
}
}
class ModuleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) {
$module = $event->getData();
$form = $event->getForm();
if ('task' == $module->getTitle()) {
$form->add('task', TaskType::class);
}
});
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Module::class
]);
}
}
So I have identified the problem. When I try to make this work, Symfony sends me this error message: "Call to a member function getTitle() on null". It seems the getData() doesn't get anything.
Actually, after reading few posts on Github I've realized that the "allow_add" option set on "true" was the origin of this issue. And indeed, when I set it on "false" I don't have any error message. But the consequence of this is that my JQuery doesn't allow me to duplicate the form if I want to, the "allow_add" option is necessary to do that.
In the Github post I uploaded, they say that the solution is to write this code first in the ModuleType:
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) {
if (null != $event->getData()) {
}
}
It's what I did but it doesn't change anything. I wrote this, followed by the code written in the ModuleType but I still have the same error message... Perhaps I don't know how to insert it correctly in the ModuleType.
I hope someone has a solution. I know I can still add the Tag and Task types directly in the ModulingType but I would have more collections.
Thanks a lot for helping me, I hope I've been clear enough!
Cheers
Did you tried this:
if (!is_null($module) && 'task' == $module->getTitle()) {
$form->add('task', TaskType::class);
}
So actually I found a solution, I was really closed, but I got a new problem...
class ModuleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$module = $event->getData();
$form = $event->getForm();
if (null != $event->getData()) {
if ('task' == $module->getTitle()) {
$form->add('task', TaskType::class);
}
}
});
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Module::class
]);
}
}
This was the solution so as you can see it was not that complicated BUT the fact of using a form event in the ModuleType creates a new issue.
In my ModulingType, I add an option
'allow_add' => true,
This really useful tool allows to automatically add a "data-prototype" in my form so that I can copy/past some jQuery lines available here (https://symfony.com/doc/current/form/form_collections.html) and then be able to duplicate or delete my form. However, when using a form event, the data-prototype doesn't register anything as it is created before my TaskType.
So after spending hours reading discussions on Github and trying to find the solution, I came to the conclusion I had to create a TaskingType and a TagingType, which look like this:
class TaskingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('task', TaskType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Module::class
]);
}
}
So ok, this solution is not perfect and I have some code duplication. But at least it allows me to only have 3 collections: Page, Moduling and Module.
If someone finds an elegant way to manage everything with one form without deleting the content available in data-prototype, please keep me posted :)

Symfony2 Custom Form Type w/Data Transformer - Service Injection

I'm implementing a custom form type with data transformer to fulfill a many-to-many relationship according to http://symfony.com/doc/2.8/form/data_transformers.html
class ClientType extends AbstractType
{
private $manager;
public function __construct(ObjectManager $manager)
{
$this->manager = $manager;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('shortname')
->add('shortinfo')
->add('web')
->add('user', CollectionType::class, array(
'entry_type' => IntegerType::class,
'allow_add' => true,
'allow_delete' => true
));
$builder->get('user')->addModelTransformer(new UserToPrimaryKeyTransformer($this->manager));
}
}
The sub-type UserType is:
class UserType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('loginname')
->add('firstname')
->add('lastname')
/* ... */
->add('email')
->add('id')
;
}
}
The transformer class is pretty much copy-pasted from the docs:
class UserToPrimaryKeyTransformer implements DataTransformerInterface
{
private $manager;
public function __construct(ObjectManager $manager)
{
$this->manager = $manager;
}
/**
* Transform entity to pkid
*
* #param User
* #return integer
*/
public function transform($user)
{
if(null === $user) {
return -1;
}
return $user->getId();
}
/**
* #param integer $pkid
*/
public function reverseTransform($pkid)
{
if (!$pkid) {
return;
}
$user = $this->manager
->getRepository('AcmeBundle:User')
// query for the user with this id
->find($pkid)
;
if (null === $user) {
/* ... */
}
return $user;
}
}
So much for the context. The problem, however seems to lie in the service definition, precisely the injection of the Doctrine Entity Manager:
acme.form.type.client:
class: AcmeBundle\Form\ClientType
arguments: [ #doctrine.orm.entity_manager ]
tags:
- { name: form.type }
Now when I post this form, I get:
Type error: Argument 1 passed to AcmeBundle\Form\ClientType::__construct() must implement interface Doctrine\Common\Persistence\ObjectManager, none given
Any ideas or pointers on what's going on? Does the #doctrine.orm.entity_manager variable have to be instantiated somewhere?
I'm on Symfony 2.8, btw
found the problem. I forgot that I was manually instantiating the form in order to fulfill an API requirement (it's a REST service I'm developing):
// create a form with an empty name in order to avoid needing a JSON ROOT element
$form = $this->get('form.factory')->createNamed('', new \AcmeBundle\Form\ClientType(), $client);
after changing this to
$form = $this->get('form.factory')->createNamed('', new \AcmeBundle\Form\ClientType($em), $client);
it worked.
Thank you all!
Your form type expecting ObjectManager and you are injecting EntityManager. I suggest to change constructor parameter to EntityManager and it'll be working

#Assert\Valid() on Entity, remove the validation on form

I been searching online, couldn't find the answer to my problem.
I want to disable #Assert/Valid() on first field, if second field is selected by the user. right now validation is happening on both fields.
Form type
AppBundle/Form/ParcelType.php
class ParcelType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$user = 1;
$builder
//TODO if address is selected from history, then dont validate this field
->add('pickupAddressNew', new AddressType())
->add('pickupAddressHistory', 'entity', [
'class' => 'AppBundle\Entity\Address',
'property' => 'formatAddress',
'query_builder' => function (EntityRepository $er) use ($user) {
return $er->createQueryBuilder('a')
->where('a.user = :user')
->andWhere('a.type = :type')
->setParameter('user', $user)
->setParameter('type', 'pickup')
->orderBy('a.isDefault', 'DESC')
->addOrderBy('a.id', 'DESC');
}
]););
}
public function getName()
{
return 'parcel';
}
}
AppBundle/Entity/Model/Parcel.php
class Parcel
{
protected $name;
/**
* #Assert\Type(type="AppBundle\Entity\Address")
* #Assert\Valid()
*/
protected $pickupAddressNew;
/**
* #Assert\Type(type="AppBundle\Entity\Address")
* #Assert\Valid()
*/
protected $pickupAddressHistory;
...
}
Address
AppBundle/Entity/Address.php
class Address
{
...
private $id;
..
private $firstName;
/**
* #var string
*
* #Assert\NotBlank(message="field.address.blank")
* #Assert\Length(
* min = 3,
* max = 255,
* minMessage = "field.address.min",
* maxMessage = "field.address.max"
* )
* #ORM\Column(name="format_address", type="string", length=255, nullable=false)
*/
private $address;
}
After long search, I couldn't find any answer, but found another solution which will solve it. Sharing with community, so others can solve it quickly.
Remove #Assert/Valid() from the annotation and add following on the form type
public function buildForm(...) {
...
$form->add('pickupAddressNew', new AddressType(), [
'label' => 'form.label.pickupAddressNew',
'constraints' => new Valid()
])
// also add event listener
$builder->addEventListener(FormEvents::SUBMIT, array($this, 'conditionValid'));
}
now create condition valid method on same formType class.
public function conditionValid (FormEvent $event)
{
$parcel = $event->getData();
$form = $event->getForm();
if ($parcel->getPickupAddressHistory() > 0)
{
$form->add('pickupAddressNew', new AddressType(), [
'label' => 'form.label.pickupAddress'
]);
}
}
On this method, we check if second field has value and its selected, then recreate the first field without the validation rule, this will bypass the group validation.

Symfony2 form collection not calling addxxx and removexxx even if 'by_reference' => false

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 :)

How to set a default value in a Symfony 2 form field?

I've been trying to set up a form with Symfony 2.
So I followed the tutorial and I've created a special class for creating the form and handling the validation process outside the controller (as shown in the documentation)
But now I need to fill in a field automatically, I've heard that I have to do it in the ProductType.php, where the form (for my product) is created.
But I don't know how to do, here is my buildForm function in ProductType.php :
class QuotesType extends AbstractType
{
private $id;
public function __construct($id){
$this->product_id = $id;
}
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('user_name', 'text')
->add('user_lastname', 'text')
->add('user_email', 'email')
->add('user_comments', 'textarea')
->add('user_product_id', 'hidden', array(
'data' => $this->product_id,
));
;
}
and it obviously doesnt work since I got a SQL error saying that my field is null.
How can I put a default value to the user_product_id ? should I do it directly to the object ?
EDIT:
Here is a part of the code of my entity :
namespace QN\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* QN\MainBundle\Entity\Quotes
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="QN\MainBundle\Entity\QuotesRepository")
*/
class Quotes
{
public function __construct($p_id)
{
$this->date = new \Datetime('today');
}
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var integer $user_product_id
*
* #ORM\Column(name="user_product_id", type="integer")
*/
private $user_product_id = "1";
/**
* #var datetime $date
*
* #ORM\Column(name="date", type="datetime")
*/
private $date;
And my controller :
public function requestAction($id)
{
$repository = $this->getDoctrine()
->getEntityManager()
->getRepository('QNMainBundle:Categories');
$categories = $repository->findAll();
$quote = new Quotes($id);
$form = $this->createForm(new QuotesType(), $quote);
$formHandler = new QuotesHandler($form, $this->get('request'), $this->getDoctrine()->getEntityManager());
if( $formHandler->process() )
{
return $this->redirect( $this->generateUrl('QNMain_Product', array('id' => $id)) );
}
return $this->render('QNMainBundle:Main:requestaform.html.twig', array(
'categories' => $categories,
'id' => $id,
'form' => $form->createView(),
));
}
My Handler :
namespace QN\MainBundle\Form;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManager;
use QN\MainBundle\Entity\Quotes;
class QuotesHandler
{
protected $form;
protected $request;
protected $em;
public function __construct(Form $form, Request $request, EntityManager $em)
{
$this->form = $form;
$this->request = $request;
$this->em = $em;
}
public function process()
{
if( $this->request->getMethod() == 'POST' )
{
$this->form->bindRequest($this->request);
if( $this->form->isValid() )
{
$this->onSuccess($this->form->getData());
return true;
}
}
return false;
}
public function onSuccess(Quotes $quote)
{
$this->em->persist($quote);
$this->em->flush();
}
}
I've also put here the Date I try to set up in the entity, I might do something wrong in both case since I can't make it work neither ..Date is not in the buildForm function, I don't know if I should ..
Another way is creating a Form Type Extension:
namespace App\Form\Extension;
// ...
class DefaultValueTypeExtension extends AbstractTypeExtension
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (null !== $default = $options['default']) {
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
static function (FormEvent $event) use ($default) {
if (null === $event->getData()) {
$event->setData($default);
}
}
);
}
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('default', null);
}
public static function getExtendedTypes(): iterable
{
yield FormType::class;
}
}
Now any possible value can be passed as default to any form field:
$form->add('user', null, ['default' => $this->getUser()]);
$form->add('user_product_id', null, ['default' => 1]);
This method is specially useful when you don't have a chance to hook into the initialization process of the bound object.
What you're trying to do here is creating a security hole: anyone would be able to inject any ID in the user_product_id field and dupe you application. Not mentioning that it's useless to render a field and to not show it.
You can set a default value to user_product_id in your entity:
/**
* #ORM\Annotations...
*/
private $user_product_id = 9000;