I have an Entity containing Self-Referenced mapping.
class Category
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=100)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="Category", mappedBy="parent")
*/
private $children;
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="children")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id")
*/
private $parent;
}
In my CategoryType I have this :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$plan = $this->plan;
$builder->add('name');
$builder->add('parent', 'entity', array(
'class' => 'xxxBundle:Category',
'property' => 'name',
'empty_value' => 'Choose a parent category',
'required' => false,
'query_builder' => function(EntityRepository $er) use ($plan) {
return $er->createQueryBuilder('u')
->where('u.plan = :plan')
->setParameter('plan', $plan)
->orderBy('u.id', 'ASC');
},
));
}
Actually, when I render the form field Category this is something like
Cat1
Cat2
Cat3
Subcat1
Subcat2
Cat4
I would like to know if it's possible and how to display something more like, a kind of a simple tree representation :
Cat1
Cat2
Cat3
-- Subcat1
-- Subcat2
Cat4
Regards.
I came up with something which seems correct from what you and jperovic wrote. You will need two new attributes for your Category class :
$level will contain ID's of its parents like "idA-idB", etc. this attribute will be use to sort your results when querying your database so you can be certain SubCatOf3 won't come before Cat3 !
$treeName will contain what jperovic already wrote and will be printed in the form.
I also used Doctrine Events [doc] so when you update/persist them, you don't have to worry about the value of these attributes.
This is your brand new Category.php file :
/**
* #ORM\HasLifeCycleCallbacks()
*/
class Category
{
private $level;
private $treeName;
/**
* Renders something like : "---- Subcategory A"
* #ORM\PreUpdate()
* #ORM\PrePersist()
**/
public function updateTreeName()
{
$itemDepth = 0;
$parent = $this->parent;
while ($parent != null) {
$itemDepth++;
$parent = $parent->getParent();
}
$this->treeName = str_repeat('--', $itemDepth) . ' ' . $this->name
}
/** renders something like : "idParent-idChild1-idChild2"
* #ORM\PreUpdate()
* #ORM\PrePersist()
**/
public function updateLevelName()
{
$this->level = '';
$parent = $this->parent;
while ($parent != null) {
$parent = $parent->getParent();
$this->level .= '-' . $p->getId();
}
}
public function getTreeName()
{
return $this->treeName;
}
public function getLevel()
{
return $this->level;
}
// ...
}
Then, put your query_builder in your CategoryRepository.php like this :
namespace Foo\BarBundle\Entity;
use Doctrine\ORM\EntityRepository;
class CategoryRepository extends EntityRepository
{
public function getHierarchicalCategoryList($plan)
{
$qb = $this->createQueryBuilder('u')
->where('u.plan = :plan')
->setParameter('plan', $plan)
->orderBy('u.level', 'ASC');
$return $qb;
}
}
And in your CategoryType.php :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$plan = $this->plan;
$builder->add('name');
$builder->add('parent', 'entity', array(
'class' => 'xxxBundle:Category',
'property' => 'treeName',
'empty_value' => 'Choose a parent category',
'required' => false,
'query_builder' => function(EntityRepository $er) use ($plan) {
return $er->getHierarchicalCategoryList($plan)
},
));
}
Note : this is quick&dirty work so you might need to correct typos, annotations, etc. Yet, you have the idea ! Hope it helps.
This is a really long shot at best, but I think it could be achieved pretty easily.
Within your query_builder you specified the 'property' => 'name'. You would need to change it to 'treeName'. Doctrine will try to find and invoke property's getter method - that's where all the printing logic comes in:
class Category
{
....
Everything else
....
public function getTreeName(){
$itemDepth = 0;
$p = $this->parent;
while ( $p != null ){
$itemDepth++;
$p = $p->getParent();
}
return str_repeat('--', $itemDepth) . ' ' . $this->name
}
}
This could pose a serious performance hit due to need to iterate for each item the depth times.
What do you think? What is the average depth of items?
Just to be clear, name property and its getter and setter are to remain intact.
Related
First, I'm sorry for my bad english. I need to create form to adding new tags for Article but when I submit form then Request data is not handled in my form because new added tags are not in entity array collection. Is possible to add custom choices to form field with many to one association?
Here is my code:
public function buildForm(FormBuilderInterface $builder, array $options)
{
dump($builder->getFormConfig()); die;
/** #var Domain $domain */
$domain = $this->currentDomainService->getCurrentDomain();
$builder
->add('articleTitle', TextType::class, [])
->addEventSubscriber(new TagsChoicesSubscriber())
;
}
class TagshoicesSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SET_DATA => ['preSetData', -50],
FormEvents::PRE_SUBMIT => ['preSetData', -50],
);
}
public function preSetData(FormEvent $event, $childName)
{
$choices = array();
/** #var Article $article */
$article = $event->getData();
if ($article instanceof Article) {
foreach ($article->getTags() as $tag) {
$tags[] = $tag->getTagName();
}
$event->getForm()->add(
'tags',
ChoiceType::class,
[
'multiple' => true,
'mapped' => false,
'choices' => $choices,
'data' => $tags,
'required' => true,
'constraints' => [
new NotBlank(),
],
]
);
}
}
}
/**
* Article
* #ORM\Entity()
*/
class Article
{
/**
* #ORM\OneToMany(targetEntity="App\Entity\Tags", mappedBy="article")
*/
private $tags;
}
/**
* Tag
*
* #ORM\Entity()
*/
class Tag
{
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Article", inversedBy="tags")
* #ORM\JoinColumn(name="article_id", referencedColumnName="id")
*/
private $article;
}
$form = $this->createForm('App\Form\ArticleType', $article);
$form->handleRequest($request);
The thing you need to implement is either collection type field, or choice type with multiple set to true, here is Symfony collection type and here is Symfony choice type, you will also need a toString function in Tag class.
I want to create form, that serve for adding, editing (and removing when url field is empty) menu items. Problem is that the count of rows/items are variable. (As you can see on the first picture)
Questions:
1)How to write a form that has variable number of fields.
2)How to parse data into the fields at this form.
class GalleryType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add(
//...some textType, fileType fields,...etc
);
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
//...
//some data/data_class option that parse data into the field
]);
}
Extra Information:
I am working on own simple content management system with Symfony 3 framework. I want to allow user to add menu item with information like: URL, Title and for instance FA icon, background image,..etc .
-There is always one empty row for adding item and the rest of fields are fulfilled with existing data (menu item/s). When you confirm the form, this row is added into the form (and empty row as well).
-There are few different kind of menu: main menu, slider, side menu, that has diferent type of fields. (you can see it on the second picture)
-Main menu has: title, url and some item can have children items (as sub menu)
-Slider has: title, url, color of title, background image
-Side menu has: title, url and Font Awesome Icon
I have already done form for navigation menu (footer) where is just 2 fields(title and link), but I feel this is not propertly way how to programming it... for illustrative purposes here is how I've done navigation
Controller:
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\SMBundle\Navigation;
use AppBundle\Entity\Sett;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Form\Extension\Core\Type\TextType;
class SettingsController extends Controller {
//....
/**
* #Route("/admin/menu/navigation", name="navigation")
*/
public function navigationAction(Request $request) {
$set = $this->getDoctrine()->getRepository('AppBundle:Sett')->findOneByName('navigation');
$navigation = $this->deserializeFromStringToObject('navigation');
if (!$navigation) {
$set = new Sett();
$navigation = new Navigation();
}
$form = $this->createFormFromArray($navigation->getLinksArray());
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$set->setEditedAt(new \DateTime());
$set->setName('navigation');
$this->brutalHack($navigation, $form);
$set->setContent($this->serializeFromObjectToString($navigation));
// Save
$this->save($set);
return $this->redirect($this->generateUrl('navigation'));
}
return $this->render("viewSM/menu/navigation.html.twig", array('form' => $form->createView()));
}
private function deserializeFromStringToObject($name) {
$object = $this->getDoctrine()->getRepository('AppBundle:Sett')->findOneByName($name);
if (!$object) {
return null;
}
$serializer = new Serializer(array(new GetSetMethodNormalizer()), array('json' => new JsonEncoder()));
return $serializer->deserialize($object->getContent(), 'AppBundle\\Entity\\SMBundle\\' . ucfirst($name), 'json');
}
private function serializeFromObjectToString($object) {
$serializer = new Serializer(array(new GetSetMethodNormalizer()), array('json' => new JsonEncoder()));
return $serializer->serialize($object, 'json');
}
private function createFormFromArray(array $collection) {
$i = 0;
$formBuilder = $this->createFormBuilder();
foreach ($collection as $key => $value) {
$formBuilder
->add('url' . $i, TextType::class, ['label' => 'URL ', 'data' => '' . $key, 'attr' => ['class' => 'form-control']])
->add('name' . $i, TextType::class, ['label' => 'Titulek ', 'data' => '' . $value, 'attr' => ['class' => 'form-control']]);
$i++;
}
$formBuilder
->add('url' . $i, TextType::class, ['label' => 'URL ', 'attr' => ['class' => 'form-control']])
->add('name' . $i, TextType::class, ['label' => 'Titulek ', 'attr' => ['class' => 'form-control']])
->add('submit', \Symfony\Component\Form\Extension\Core\Type\SubmitType::class, ['label' => 'Uložit', 'attr' => ['class' => 'btn btn-primary']]);
$form = $formBuilder->getForm();
return $form;
}
private function save($set) {
$em = $this->getDoctrine()->getManager();
$em->persist($set);
$em->flush();
}
private function brutalHack($navigation, $form) {
$nav = array();
if (count($navigation->getLinksArray()) == 0) {
$nav[$form['url0']->getData()] = $form['name0']->getData();
}
for ($i = 0; $i < count($navigation->getLinksArray()); $i++) {
$key = $form['url' . $i]->getData();
$value = $form['name' . $i]->getData();
if ($key != NULL && $value != NULL) {
$nav[$key] = $value;
}
}
$key = $form['url' . $i]->getData();
$value = $form['name' . $i]->getData();
if ($key != NULL && $value != NULL) {
$nav[$key] = $value;
}
$navigation->setLinksArray($nav);
}
//...
}
Entity:
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="sett")
* #ORM\HasLifecycleCallbacks
*/
class Sett
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="name", length=255)
*/
private $name;
/**
* #ORM\Column(name="content", type="json_array")
*/
private $content;
/**
* #ORM\Column(name="edited_at", type="datetime")
*/
private $editedAt;
/**
* #ORM\Column(name="created_at", type="datetime")
*/
private $createdAt;
/**
* #ORM\PrePersist
*/
public function onPrePersist()
{
$this->createdAt = new \DateTime();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Set
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set content
*
* #param array $content
*
* #return Set
*/
public function setContent($content)
{
$this->content = $content;
return $this;
}
/**
* Get content
*
* #return array
*/
public function getContent()
{
return $this->content;
}
/**
* Set editedAt
*
* #param \DateTime $editedAt
*
* #return Set
*/
public function setEditedAt($editedAt)
{
$this->editedAt = $editedAt;
return $this;
}
/**
* Get editedAt
*
* #return \DateTime
*/
public function getEditedAt()
{
return $this->editedAt;
}
/**
* Set createdAt
*
* #param \DateTime $createdAt
*
* #return Set
*/
public function setCreatedAt($createdAt)
{
$this->createdAt = $createdAt;
return $this;
}
/**
* Get createdAt
*
* #return \DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
}
Data class:
class Navigation
{
private $linksArray;
public function __construct() {
$this->linksArray=array();
}
function getLinksArray() {
return $this->linksArray;
}
function setLinksArray($linksArray) {
$this->linksArray = $linksArray;
}
function add($key,$value){
$this->linksArray[$key]=$value;
}
}
I am not sure if this will work but you should give it a try.
2)How to parse data into the form that has variable number of fields.
You can send the data as form $options.
in your controller
$oForm = $this->createForm(YourFormType::class,
$FormObject, [
'your_options' => [
'Checkbox' => 'FieldName1',
'TextArea' => 'FieldName2'
]);
in your form
public function buildForm(FormBuilderInterface $builder, array $options)
{
foreach($options['your_options'] as $key, $option) { //you can name $option as $filedName or whatever you find convenient
$builder->add($option, $key.Type::class);
}
...}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'your_options' => null
])
}
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.
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'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;