I've been trying to implement embed forms in Sonata Admin Bundle 2.0, Sonata User Bundle 2.0 and Symfony 2.0.16 (yes, I know it's kind of old right now) and after reading a lot of forum posts and manual I could be able to implement it... but just at form level, it can't display data in edition mode, or persist it in creation mode.
Being more expecific, I'm trying to make work a relationship between an User entity (from Sonata User Bundle) with an Email entity, in an one-to-many relationship (one User has many Emails, or just one). So in the User form is gonna have one or more emails forms dynamically embeded, which seems to be working, but are disconnected with the email table.
systemUser is pointing to the table user. I got to change it because I'm using PostgreSQL and word is reserved.
UserAdmin.php
<?php
class UserAdmin extends Admin
{
// more code
$formMapper
->with('General')
->add('username')
->add('email')
->add('plainPassword', 'text', array('required' => false))
->end()
->with('Groups')
->add('groups', 'sonata_type_model', array('required' => false))
->end()
->with('Profile')
->add('dateOfBirth', 'date', array('required' => false))
->add('firstname', null, array('required' => false))
->add('lastname', null, array('required' => false))
->add('website', 'url', array('required' => false))
->add('locale', null, array('required' => false))
->add('timezone', null, array('required' => false))
->end()
->with('Emails')
->add('emails', 'collection', array(
'label' => 'Emails',
'required' => true,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
),
array(
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'id',
'targetEntity'=> 'MyProject\xBundle\Entity\Email',
'link_parameters' => array('context' => 'user'),
)
)
;
// more code ...
}
User.php
<?php
class User extends BaseUser
{
/**
* #var array emails
*/
protected $emails;
/**
* #var string user
*/
protected $user;
public function __construct()
{
$this->emails = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add emails
*
* #param MyProject\xBundle\Email $email
*/
public function addEmails(\MyProject\xBundle\Entity\Email $email)
{
$this->emails[] = $email;
}
/**
* Get emails
*
* #return Doctrine\Common\Collections\Collection
*/
public function getEmails()
{
return $this->emails;
}
/**
* Set emails
*
* #param $emails
* #return Email
*/
public function setEmails($emails)
{
$this->$emails = $emails;
foreach ($emails as $email) {
$email->setUser($this);
}
}
/**
*
* #param string $user
*/
public function setUser($user)
{
$this->user = $user;
}
}
Email.php
<?php
class Email
{
/**
* #var SystemUser
*
* #ORM\ManyToOne(targetEntity="User", cascade={"persist"})
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="system_user_id", referencedColumnName="id")
* })
*
*/
private $systemUser;
public function __construct()
{
$this->systemUser = new ArrayCollection();
}
/**
*
* #param MyProject\xBundle\Entity\User $systemUser
*/
public function setSystemUser(\MyProject\xBundle\Entity\User $systemUsers = null)
{
$this->systemUser = $systemUser;
return $this;
}
/**
* Get systemUser
*
* #return MyProject\xBundle\Entity\User
*/
public function getSystemUser()
{
return $this->systemUser;
}
}
Your approach needs a 'forward' relationship from the User to the Email entity, otherwise the admin doesn't know what to change and persist. Change your code in User.php so that the email variable actually knows about its relationship. That means you should add something like
/**
* #var array emails
* #ORM\OneToMany(targetEntity="Email", mappedBy="systemUser", cascade={"all"})
*/
protected $emails;
Related
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 would like to make one form in which I could have elements to populate two entities.
How can I do that? One of the objects is mapped to the other one.
For example I have something like this:
Table user:
id | login | password
Table user_email:
id | user_id | email
There can be more than one row user_email mapped to user - user can have more than one email.
But - when I'm adding first occurrence of the user I have to get his first email.
I know everything how to map entities but I have problem with populating objects from ZF2 form.
Can anyone suggest how should I do this? I've tried to make two fieldsets but I cannot bind object to fieldset. If this is the solution how bind objects to form which have two filedsets? Every fieldset have mapped doctrine2 hydrator but when I'm trying to bind one of the entity to form (not fieldset which i cannot do) I have error message:
Zend\Stdlib\Hydrator\ArraySerializable::extract expects the provided object to implement getArrayCopy()
Before my example I have to explan what i want to achieve. I have a table cameras - which contain link and some other information about streaming cameras. Second table: cameras_desc contain descriptions to those cameras in different languages. In my CMS i want to add two rows: one in cameras and second in cmaeras_desc in one form. cameras_desc will have first translation in polish language (which would be default language for CMS). As u can see cameras and cameras_desc are mapped with some other entites (cameras with checkpoints and cameras_desc with languages). but thats not a point. What i want to achieve is to populate two rows in two tables by one form. lang in cameras_desc is set by PHP code, but checkpoint is set by user in form by select element. everything works in code below but it isnt made honestly.
This is my code:
First entity:
namespace Granica\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Cameras
*
* #ORM\Table(name="cameras", indexes={#ORM\Index(name="IDX_6B5F276AF27C615F", columns={"checkpoint_id"})})
* #ORM\Entity
*/
class Cameras
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="link", type="text", nullable=false)
*/
private $link;
/**
* #var string
*
* #ORM\Column(name="direction", type="string", length=4, nullable=false)
*/
private $direction;
/**
* #var \Granica\Entity\Checkpoints
*
* #ORM\ManyToOne(targetEntity="Granica\Entity\Checkpoints")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="checkpoint_id", referencedColumnName="id")
* })
*/
private $checkpoint;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set link
*
* #param string $link
* #return Cameras
*/
public function setLink($link)
{
$this->link = $link;
return $this;
}
/**
* Get link
*
* #return string
*/
public function getLink()
{
return $this->link;
}
/**
* Set direction
*
* #param string $direction
* #return Cameras
*/
public function setDirection($direction)
{
$this->direction = $direction;
return $this;
}
/**
* Get direction
*
* #return string
*/
public function getDirection()
{
return $this->direction;
}
/**
* Set checkpoint
*
* #param \Granica\Entity\Checkpoints $checkpoint
* #return Cameras
*/
public function setCheckpoint(\Granica\Entity\Checkpoints $checkpoint = null)
{
$this->checkpoint = $checkpoint;
return $this;
}
/**
* Get checkpoint
*
* #return \Granica\Entity\Checkpoints
*/
public function getCheckpoint()
{
return $this->checkpoint;
}
}
Second entity:
<?php
namespace Granica\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* CamerasDesc
*
* #ORM\Table(name="camera_desc", indexes={#ORM\Index(name="lang", columns={"lang"}), #ORM\Index(name="camera_id", columns={"camera_id"})})
* #ORM\Entity
*/
class CameraDesc
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="description", type="text", nullable=false)
*/
private $description;
/**
* #var \Granica\Entity\Languages
*
* #ORM\ManyToOne(targetEntity="Granica\Entity\Languages")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="lang", referencedColumnName="id")
* })
*/
private $lang;
/**
* #var \Granica\Entity\Cameras
*
* #ORM\ManyToOne(targetEntity="Granica\Entity\Cameras")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="camera_id", referencedColumnName="id")
* })
*/
private $camera;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set description
*
* #param string $description
* #return CamerasDesc
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set lang
*
* #param \Granica\Entity\Languages $lang
* #return CamerasDesc
*/
public function setLang(\Granica\Entity\Languages $lang = null)
{
$this->lang = $lang;
return $this;
}
/**
* Get lang
*
* #return \Granica\Entity\Languages
*/
public function getLang()
{
return $this->lang;
}
/**
* Set camera
*
* #param \Granica\Entity\Cameras $camera
* #return CamerasDesc
*/
public function setCamera(\Granica\Entity\Cameras $camera = null)
{
$this->camera = $camera;
return $this;
}
/**
* Get camera
*
* #return \Granica\Entity\Cameras
*/
public function getCamera()
{
return $this->camera;
}
}
Fieldset ( for camera_desc ):
namespace Granica\Form;
use Zend\Form\Fieldset;
use DoctrineModule\Persistence\ObjectManagerAwareInterface;
use Doctrine\Common\Persistence\ObjectManager;
use DoctrineModule\Stdlib\Hydrator\DoctrineObject as DoctrineHydrator;
use DoctrineORMModule\Stdlib\Hydrator\DoctrineEntity;
use Granica\Entity\Languages;
class CameraDescFieldset extends Fieldset implements ObjectManagerAwareInterface
{
protected $objectManager;
public function __construct(ObjectManager $objectManager)
{
parent::__construct('camera_desc');
$this->setObjectManager($objectManager);
$this->setHydrator(new DoctrineHydrator($this->getObjectManager(),'Granica\Entity\CameraDesc'));
$this->add(array(
'name' => 'description',
'attributes' => array(
'type' => 'text',
'placeholder' => 'Opis',
'required' => 'true',
),
'options' => array(
'label' => 'Opis kamery',
),
));
}
// implementacja interfajsu objectmanager
public function setObjectManager(ObjectManager $objectManager)
{
$this->objectManager = $objectManager;
return $this;
}
public function getObjectManager()
{
return $this->objectManager;
}
}
Form (for camera):
use Zend\Form\Form;
use DoctrineModule\Persistence\ObjectManagerAwareInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Granica\Form\CameraDescFieldset;
class AddCameraForm extends Form implements ObjectManagerAwareInterface
{
protected $objectManager;
public function __construct(ObjectManager $objectManager)
{
parent::__construct('checkpoint');
$this->setObjectManager($objectManager);
// tworzenie formularza
$this->setAttribute('method', 'post');
$this->add(array(
'name' => 'checkpoint',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'options' => array(
'label' => 'Punkt graniczny',
'object_manager' => $this->getObjectManager(),
'target_class' => 'Granica\Entity\Checkpoints',
'property' => 'name',
'empty_option' => '--- wybierz przejście ---'
),
));
$this->add(array(
'name' => 'link',
'attributes' => array(
'type' => 'text',
'placeholder' => 'Link',
'required' => 'true',
),
'options' => array(
'label' => 'Adres URL kamery',
),
));
$this->add(array(
'type' => 'Zend\Form\Element\Select',
'name' => 'direction',
'attributes' => array(
'required' => 'true',
),
'options' => array(
'label' => 'Kierunek',
'empty_option' => '--- wybierz kierunek ---',
'value_options' => array(
'from' => 'FROM: Wyjazd z Polski',
'to' => 'TO: Wjazd do Polski',
),
)
));
$this->add(new CameraDescFieldset($this->getObjectManager()));
$this->add(array(
'name' => 'submit',
'attributes' => array(
'type' => 'submit',
'value' => 'Zapisz',
'id' => 'submitbutton',
),
));
}
// implementacja interfajsu objectmanager
public function setObjectManager(ObjectManager $objectManager)
{
$this->objectManager = $objectManager;
return $this;
}
public function getObjectManager()
{
return $this->objectManager;
}
}
And the add controller:
public function addAction()
{
$camera = new Cameras();
$cameraDesc = new CameraDesc();
$lang = $this->getEntityManager()->getRepository('Granica\Entity\Languages')->find('pl');
$form = new AddCameraForm($this->getEntityManager());
$cameraDescFieldset = new CameraDescFieldset($this->getEntityManager());
$form->setHydrator(new DoctrineHydrator($this->getEntityManager(),'Granica\Entity\Cameras'));
$form->bind($camera);
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
$data = $form->getData();
$cameraDesc->setLang($lang);
$cameraDesc->setDescription($request->getPost()['camera_desc']['description']);
$this->getEntityManager()->persist($camera);
$cameraDesc->setCamera($camera);
$this->getEntityManager()->persist($cameraDesc);
$this->getEntityManager()->flush();
return $this->redirect()->toRoute('cameras');
}
}
return new ViewModel(array('form' => $form));
}
As you can see I've managed to overcome my problem - I've populate camera_desc with data which I get from post. This work but isn't the best solution - for example filters didn't work on cameraDesc descrition.
I have been searching before for an answer for this problem but after hours I need to ask you.
I have one Entity called Pedidos, that have another entity related called PedidosMateriales, as you can see:
(...)
/**
* Pedidos
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="ochodoscuatro\IntranetBundle\Entity\PedidosRepository")
*/
class Pedidos
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
(...)
/**
* #ORM\OneToMany(targetEntity="PedidosMateriales", mappedBy="pedido")
*/
private $pedidosmateriales;
public function __construct() {
$this->pedidosmateriales = new \Doctrine\Common\Collections\ArrayCollection();
$this->pagos = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addPedidosmateriales(\ochodoscuatro\IntranetBundle\Entity\PedidosMateriales $pedidosmateriales){
$this->pedidosmateriales[] = $pedidosmateriales;
}
public function getPedidosmateriales(){
return $this->pedidosmateriales;
}
(...)
}
When I submit a form, if I get $_POST, I can see all the information good there, but just after doing
$form->handleRequest($request);
I get this error:
Neither the property "pedidosmateriales" nor one of the methods "addPedidosmaterial()", "addPedidosmateriale()", "setPedidosmateriales()", "__set()" or "__call()" exist and have public access in class "ochodoscuatro\IntranetBundle\Entity\Pedidos".
But I have them already written!
I've read that with "multiple" => true I could get the answer, but it launch another error telling that the option "multiple" does not exist.
My form is this:
$builder->add('fechapedido', 'date', array(//Fecha del pedido
'widget' => 'single_text',
'format' => 'dd/MM/yyyy',
'attr' => array('class' => 'datepicker'),
'data' => new \DateTime() //Valor (Fecha actual)
))
->add('cliente', 'entity', array(
'class' => 'ochodoscuatroIntranetBundle:Clientes',
'property' => 'nombre',
'empty_value' => 'Nombre cliente'))//Nombre del "placeholder"
->add('pedidosmateriales', 'collection', array('type' => new PedidosMaterialesType(), 'allow_add' => true, 'by_reference' => false,));
}
For now I've successful used validation groups, but now I'm stuck with validation groups and nested mapped entities.
I'll explain the problem by a simplified example.
My entities: Address, Damage, Appliance
/**
* #ORM\Entity()
*/
class Address extends ...
{
/**
* #var string
* #ORM\Column(type="string", name="postcode", nullable=true)
* #Assert\NotBlank(
* groups={
* "damage_responsible_address",
* "appliance_repairer_address",
* })
*/
private $postcode;
...
/**
* #ORM\Entity()
*/
class Damage extends ...
{
/**
* #var boolean
* #ORM\Column(type="boolean", name="responsible", nullable=true)
* #Assert\NotBlank(groups={"damage"})
*/
private $responsible;
/**
* #ORM\OneToOne(targetEntity="Address", cascade={"persist","remove"})
* #ORM\JoinColumn(name="responsible_address_id", referencedColumnName="id")
* #Assert\Valid()
*/
private $responsibleAddress;
/**
* #ORM\ManyToMany(targetEntity="Appliance", orphanRemoval=true, cascade={"persist", "remove"})
* #ORM\JoinTable(name="coronadirect_cuzo_home_damage_appliances",
* joinColumns={#ORM\JoinColumn(name="damage_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="appliance_id", referencedColumnName="id")}
* )
*/
private $appliances;
...
/**
* #ORM\Entity()
*/
class Appliance extends ...
{
/**
* #var boolean
* #ORM\Column(type="boolean", name="to_repair", nullable=true)
* #Assert\NotBlank(groups={"appliance"})
*/
private $toRepair;
/**
* #ORM\OneToOne(targetEntity="Address", cascade={"persist","remove"})
* #ORM\JoinColumn(name="repairer_address_id", referencedColumnName="id")
* #Assert\Valid()
*/
private $repairAddress;
...
To define my forms I use a AddressType, DamageType and ApplianceType:
class DamageType extends ...
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('appliances', 'collection', array(
'type' => 'home_damage_appliance_type',
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'options' => array(
'cascade_validation' => true,
)
));
$builder->add('responsible', 'choice', array(
'choices' => $this->getYesNoChoiceArray(),
'expanded' => true,
'multiple' => false,
));
$builder->add('responsibleAddress', 'address_type', array(
'required' => true
));
...
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Damage',
'cascade_validation' => true,
'validation_groups' =>
function(FormInterface $form) {
$groups = array('damage');
if ($form->getData()->getResponsible() == true) {
$groups[] = 'damage_responsible_address';
}
return $groups;
}
));
}
I'm adding the damage_responsible_address group when responsible is set to true in the form.
Otherwise I don't want the address to be validated.
class ApplianceType extends ...
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('toRepair', 'choice', array(
'choices' => $this->getYesNoChoiceArray(),
'expanded' => true,
'multiple' => false,
));
$builder->add('repairAddress', 'address_type', array(
'required' => true
));
...
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Appliannce',
'cascade_validation' => true,
'validation_groups' =>
function(FormInterface $form) {
$groups = array('appliance');
if ($form->getData()->getToRepair() == true) {
$groups[] = 'appliance_repairer_address';
}
return $groups;
}
));
}
Same as previous, when toRepair is true I want to validate the address.
What's going wrong ?
I the Damage responsible is true and the appliance toRepair is false, the form does give validation errors on the responsible address BUT also on the the appliance address.
The same for the other way arround: When an appliance address is invalid (toRepar is true), then the responsibleAddress is also invalid (even when responsible is false).
The address validation groups don't look on which form they are defined, but just attatch them to every address item in the form.
Is it possible to define validation groups specific for a form only?
I am using Doctrine and Symfony 2.3.6.
The problem is that symfony uses OR-logic for validation groups. So then your apply one of groups to form it will validate address in both cases.
If you move the groups to a parent entity, it will not solve the problem?
/**
* #ORM\Entity()
*/
class Damage extends ...
{
/**
* #ORM\OneToOne(targetEntity="Address", cascade={"persist","remove"})
* #ORM\JoinColumn(name="responsible_address_id", referencedColumnName="id")
* #Assert\Valid(
* groups={
* "damage_responsible_address"
* })
* )
*/
private $responsibleAddress;
}
italic is for general case, bold for the particular case of this question.
The address validation groups don't look on which form they are defined, but just attach them to every address item in the form.
True, your validation_group callback in DamageType set validation_group for all the form, and as you use cascade_validation, for all embed AddressType the form contains. There is no reason it should understand that you want it set only for responsible address.
Generally, setting a validation group through callback for a parent form, with cascade validation set to true, will make all child forms validated against this validation group. If we want to set different validation groups for each children, we have to put a validation group callback in children formtype.
You must place your validation callback in AddressType, so it will be called for each Address Form. Of course, in order to do so, each Adress entity must be aware of its owner.
An alternative to validation callback is Group Sequence Provider. But in any case you still have to set your validation groups in each Address entity and not in Damage, so your Adress must still be aware of its owner.
I know it has been a while, but here is the documentation that may answer the problem with symfony v3:
http://symfony.com/doc/current/form/data_based_validation.html