Update Many to Many relationship - php

I am working on project with symfony 3 and doctrine .
I have a many to many relationship between Pack and Produit :
Pack Entity :
class Pack
{
/**
* #var ArrayCollection | Produit[]
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Produit", inversedBy="packs")
* #ORM\JoinTable(name="link_pack")
*/
private $produits;
public function __construct()
{
$this->produits = new ArrayCollection();
}
/**
* #return Produit[]|ArrayCollection
*/
public function getProduits()
{
return $this->produits;
}
public function addProduit(Produit $produit)
{
if ($this->produits->contains($produit)) {
return;
}
$this->produits[] = $produit;
}
public function removeProduit(Produit $produit)
{
if (! $this->produits->contains($produit)) {
return;
}
return $this->produits->removeElement($produit);
}
}
Produit Entity :
class Produit
{
/**
* #var ArrayCollection | Pack[]
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Pack", mappedBy="produits")
*/
private $packs;
public function __construct()
{
$this->packs = new ArrayCollection();
}
/**
* #return Pack[]|ArrayCollection
*/
public function getPacks()
{
return $this->packs;
}
public function addPack(Pack $pack)
{
if ($this->packs->contains($pack)) {
return;
}
$pack->addProduit($this);
$this->packs[] = $pack;
}
}
I want to assign products to a pack , so I have a form that contains products in a select field. (the pack and products are created before).
The form type :
class PackAffectProduitType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('produits', EntityType::class, array(
'class' => Produit::class,
'choice_label' => 'libelle',
'multiple' => true,
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Pack::class,
));
}
}
The controller :
public function affectProduitsAction(Pack $pack, Request $request)
{
$form = $this->createForm(PackAffectProduitType::class, $pack);
$form->handleRequest($request);
dump($pack);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
dump($pack);
$em->flush();
// ...
}
//...
}
The problem is when I select the products and submit, It assigns the selected products to the pack but the products assigned before are deleted. I want them to still assigned, So how to solve this problem ?

If you want your view to add OR remove values, if you manually generate your field view structure, you need not to forget to fill already set values (e.g. the products previously added to the Pack).
For example by adding the selected attribute to the choices of your select. Doing so will pre-fill your field with current values therefore removing at submit only those which have been deliberately unselected.
If you don't want your view to display nor know anything about already set Produit entities on this pack and just blindly perform adds, you can add mapped => false to your field options and handle manually the addition of your products to your pack in your controller.
This will only allow you to add though, since it does not have knowledge of already set values, and you would have to make another case/action to perform deletion. Above way is better in my opinion.
In this last case, your controller would look like :
public function affectProduitsAction(Pack $pack, Request $request)
{
$form = $this->createForm(PackAffectProduitType::class, $pack);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
if(is_array($form->get('produits')->getData()){
foreach($form->get('produits')->getData() as $produit){
$pack->addProduit($produit);
}
}
$em->flush();
// ...
}
//...
}
And your FormType :
class PackAffectProduitType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('produits', EntityType::class, array(
'class' => Produit::class,
'mapped' => false,
'choice_label' => 'libelle',
'multiple' => true,
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Pack::class,
));
}
}

Try to add 'by_reference' => false option to your form, it will force to use setters and getters from your entity

Related

How to make Symfony2 form optional field not null on save

I have an image field which is optional. When you upload image, it will save the filename on the database (using events via doctrine ).
The problem is when you edit an already uploaded form and don't add an image, it makes the image field to null.
Is there a way to check / remove the field value setting to null if no image is uploaded?
The Entity, Form code is as below :
class Product
{
/**
* #ORM\Column(type="string", nullable=true)
*
* #Assert\Image
*/
private $image;
}
Form
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('image', FileType::class, [
'required' => !$options['update'],
]);
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$product = $event->getData();
if (null == $form->get('image')->getData()) {
// $form->remove('image');
}
});
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Product'
));
}
public function getBlockPrefix()
{
return 'appbundle_product';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setRequired([
'update',
]);
}
}
// In controller
$editForm = $this->createForm(
'AppBundle\Form\ProductType',
$product,
[
'update' => true
]
);
You need the event PRE_SUBMIT, try this:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('image', FileType::class, [
'required' => !$options['update'],
]);
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$form = $event->getForm();
$product = $event->getData();
if(empty($product['image'])){
//No new image for existing field was given. Make field disabled so doctrine will not update with empty
$config = $event->getForm()->get('image')->getConfig();
$options = $config->getOptions();
$event->getForm()->add(
'image',
$config->getType()->getName(),
array_replace(
$options,
['disabled' => true]
)
);
}
});
}
Symfony Form has submit() method, which takes two arguments, $submittedData and $clearMissing
/**
* Submits data to the form, transforms and validates it.
*
* #param null|string|array $submittedData The submitted data
* #param bool $clearMissing Whether to set fields to NULL
* when they are missing in the
* submitted data.
*
* #return FormInterface The form instance
*
* #throws Exception\AlreadySubmittedException If the form has already been submitted.
*/
public function submit($submittedData, $clearMissing = true);
$clearMissing parameter is by the default set to true.
If you, in your controller do this: $form->submit($submittedData, false);, your image will not be set to null.
https://symfony.com/doc/current/form/direct_submit.html#calling-form-submit-manually
Similar question: Symfony2 - How to stop Form->handleRequest from nulling fields that don't exist in post data

Symfony2: Entity form field with empty value

i have a form definition which uses the so-far great field type entity. With the option query_builder I select my values and the are displayed.
The sad part is, I am required to display a null default value, like all (it's a filter form). I don't like the choices option of entity because I have database values and a FormType shouldn't query the database.
My approach so far was to implement a custom field type which extends entity and adds a null entry to the top of the list. The field type is loaded and used but unfortunately the dummy value is not displayed.
The field definition:
$builder->add('machine', 'first_null_entity', [
'label' => 'label.machine',
'class' => Machine::ident(),
'query_builder' => function (EntityRepository $repo)
{
return $repo->createQueryBuilder('m')
->where('m.mandator = :mandator')
->setParameter('mandator', $this->mandator)
->orderBy('m.name', 'ASC');
}
]);
The form type definition:
class FirstNullEntityType extends AbstractType
{
/**
* #var unknown
*/
private $doctrine;
public function __construct(ContainerInterface $container)
{
$this->doctrine = $container->get('doctrine');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setRequired('query_builder');
$resolver->setRequired('class');
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$class = $options['class'];
$repo = $this->doctrine->getRepository($class);
$builder = $options['query_builder']($repo);
$entities = $builder->getQuery()->execute();
// add dummy entry to start of array
if($entities) {
$dummy = new \stdClass();
$dummy->__toString = function() {
return '';
};
array_unshift($entities, $dummy);
}
$options['choices'] = $entities;
}
public function getName()
{
return 'first_null_entity';
}
public function getParent()
{
return 'entity';
}
}
Here is what works in Symfony 3.0.3
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
$builder->add('example' EntityType::class, array(
'label' => 'Example',
'class' => 'AppBundle:Example',
'placeholder' => 'Please choose',
'empty_data' => null,
'required' => false
));
You can use placeholder from 2.6
An alternative approach would be to use a ChoiceList with choices that are generated from the database and then use that in a custom choice form type that will allow for an empty_value.
Choice List
namespace Acme\YourBundle\Form\ChoiceList;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
class MachineChoiceList extends LazyChoiceList
{
protected $repository;
protected $mandator;
public function __construct(ObjectManager $manager, $class)
{
$this->repository = $manager->getRepository($class);
}
/**
* Set mandator
*
* #param $mandator
* #return $this
*/
public function setMandator($mandator)
{
$this->mandator = $mandator;
return $this;
}
/**
* Get machine choices from DB and convert to an array
*
* #return array
*/
private function getMachineChoices()
{
$criteria = array();
if (null !== $this->mandator) {
$criteria['mandator'] = $this->mandator;
}
$items = $this->repository->findBy($criteria, array('name', 'ASC'));
$choices = array();
foreach ($items as $item) {
$choices[** db value **] = ** select value **;
}
return $choices;
}
/**
* {#inheritdoc}
*/
protected function loadChoiceList()
{
return new SimpleChoiceList($this->getMachineChoices());
}
}
Choice List Service (YAML)
acme.form.choice_list.machine:
class: Acme\YourBundle\Form\ChoiceList\MachineChoiceList
arguments:
- #doctrine.orm.default_entity_manager
- %acme.model.machine.class%
Custom Form Type
namespace Acme\YourBundle\Form\Type;
use Acme\YourBundle\Form\ChoiceList\MachineChoiceList;
..
class FirstNullEntityType extends AbstractType
{
/**
* #var ChoiceListInterface
*/
private $choiceList;
public function __construct(MachineChoiceList $choiceList)
{
$this->choiceList = $choiceList;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$choiceList = $this->choiceList;
$resolver->setDefault('mandator', null);
$resolver->setDefault('choice_list', function(Options $options) use ($choiceList) {
if (null !== $options['mandator']) {
$choiceList->setMandator($options['mandator']);
}
return $choiceList;
});
}
public function getName()
{
return 'first_null_entity';
}
public function getParent()
{
return 'choice';
}
}
Custom Form Type Service (YAML)
acme.form.type.machine:
class: Acme\YourBundle\Form\Type\FirstNullEntityType
arguments:
- #acme.form.choice_list.machine
tags:
- { name: form.type, alias: first_null_entity }
In Your Form
$builder
->add('machine', 'first_null_entity', [
'empty_value' => 'None Selected',
'label' => 'label.machine',
'required' => false,
])
;

Using some dynamic forms related to the same class

Symfony 2.3
I'm embedding some Forms to be able to change each user property related to permissions. I've created an UserAdminType which is displayed for each user in the same page:
<?php
namespace Msalsas\UserAdminBundle\Form;
use Msalsas\UserBundle\Entity\User;
use Msalsas\UserBundle\Entity\ExtendedUser;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
class UserAdminType extends AbstractType
{
private $user;
public function __construct(User $user)
{
$this->user = $user;
$this->extendedUser = new ExtendedUser($this->user);
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$extendedUser = $this->extendedUser;
$builder
->add('extendedRole',
'choice', array('choices' => array(
$extendedUser::ROLE_1 => "Role 1",
$extendedUser::ROLE_2 => "Role 2",
$extendedUser::ROLE_3 => "Role 3",
),
'label' => $this->user->getUsername()
))
->add('Change roles for '.$this->user->getUsername(), 'submit')
;
$builder->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
if( ! $form->getClickedButton('Change roles for '.$this->user->getUsername()) )
{
// Here I should avoid submitting the form
}
}
);
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Msalsas\UserBundle\Entity\ExtendedUser',
'empty_data' => function (FormInterface $form) {
return null;
}
));
}
/**
* #return string
*/
public function getName()
{
return 'extendedUserRoleForm';
}
}
The problem is that when I submit one of those forms, all other forms are also submitted, returning an error, because the extendedUser uses a constructor to initialize the object with the User as parameter:
Catchable Fatal Error: Argument 1 passed to Msalsas\UserBundle\Entity\ExtendedUser::__construct() must be an instance of Msalsas\UserBundle\Entity\User, none given, called in .../vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/FormType.php on line 140 and defined
I've also tried to set the empty_data with a new default ExtendedUser:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Msalsas\UserBundle\Entity\ExtendedUser',
'empty_data' => function (FormInterface $form) {
return $this->extendedUser;
}
));
}
Now, when submitting the form, the new entity is persisted, but the other forms are still submitted, and returning an error: This form should not contain extra fields. This seems to be due to the duplicated property name (extendedRole).
How could I avoid the other forms to be submitted?
I've found out the solution here.
Each Form must have different name. So I've added a $name property, and assigned it in the constructor:
private $user;
private $name = 'default_name_';
private $extendedUser;
public function __construct(User $user, $formName)
{
$this->user = $user;
$this->extendedUser = new ExtendedUser($this->user);
$this->name = $this->name.$formName;
}
//... (No event required)
/**
* #return string
*/
public function getName()
{
return $this->name;
}
The $formName parameter is relative to the current user. In this way, only the "clicked" form is submitted. Wish it helps.

Symfony2 FormType entity field type

I have three entity. Profile, Car and Trip. When user(Profile) create Trip, he can chose the Car(only his own) and assign it to the Trip. I know the field must be the entity type. But I dont know how I can set to chose list a cars of current user(Profile). Any ideas? Thanks.
Filter the cars by user, I think this example is what you need:
$builder->add('car', 'entity', array(
'class' => '/path/to/entity/Car',
'property' => 'title',
'empty_value' => 'Choose a car',
'query_builder' => function(EntityRepository $em) use ($userId) {
return $em->createQueryBuilder('c')
->join('c.user', 'u')
->where('u.id = :userId')
->setParameter('userId', $userId);
}
)
)
You can add $userId as one form option:
$form = $this->createForm(new MyFormType(), $object, array( 'userId' => $userId ));
And inside your form retrieve it:
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'userId' => function (Options $options, $value) {
return $options['userId'];
}
));
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if($options['userId']){
$userId = $options['userId'];
}
}
How I mentioned. other solution, even personally I don't prefer it:
$form = $this->createForm(new MyFormType($userId), $object);
And in your form, store it in a protected variable to be used later in your query:
/**
* Class MyFormType
*/
class MyFormType extends AbstractType
{
protected $userId;
/**
* #param $userId
*/
public function __construct($userId) {
$this->userId = $userId;
}
}
Take a look at this Symfony cookbook entry:
http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html#how-to-dynamically-generate-forms-based-on-user-data

How to setup a data transformer in Symfony to reuse existing entities?

I'm working on an article editor in Symfony with built-in tagging capability:
The controller
class MainController extends Controller
{
public function indexAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
// $article = ...
$form = $this->createForm(new ArticleType(), $article);
$form->handleRequest($request);
if ($form->isValid()) {
$em->persist($article);
$em->flush();
return $this->redirect($this->generateUrl('acme_edit_success'));
}
return $this->render('AcmeBundle:Main:index.html.twig', array(
'form' => $form->createView()
));
}
}
The forms
The tag form is registered as a service with the #Doctrine argument, so I can use the entity manager inside the class. The tag form is embed inside the article form.
ArticleType.php
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('content')
->add('tags', 'collection', array(
'type' => 'acme_bundle_tagtype',
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
))
->add('save', 'submit')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\Bundle\Entity\Article',
'cascade_validation' => true
));
}
public function getName()
{
return 'acme_bundle_articletype';
}
}
TagType.php
class TagType extends AbstractType
{
private $entityManager;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new TagTransformer($this->entityManager);
$builder->add(
$builder->create('name')
->addModelTransformer($transformer)
);
}
function __construct(\Doctrine\Bundle\DoctrineBundle\Registry $doctrine) {
$this->entityManager = $doctrine->getManager();
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\Bundle\Entity\Tag'
));
}
public function getName()
{
return 'acme_bundle_tagtype';
}
}
The data transformer
I created this data transformer to check if the given tag already exists and then transform the tag object to the one that already exists in the database:
class TagTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
public function transform($tag)
{
if (null === $tag) {
return '';
}
return $tag;
}
public function reverseTransform($name)
{
if (!$name)
return null;
$tag = $this->om
->getRepository('AcmeBundle:Tag')
->findOneByName($name)
;
if (!$tag) {
$tag = new Tag();
$tag->setName($name);
}
return $tag;
}
}
When I try to save an article with an already existing tag, the reverseTransform() function successfully returns the original tag objects, but the DBAL converts the object back to a string by its __toString() method, and Doctrine still initiates an INSERT query instead of UPDATE, so I get the next error:
An exception occurred while executing 'INSERT INTO Tag (name)
VALUES (?)' with params [{}]:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry
'An Existing Tag' for key 'UNIQ_0123456789ABCDE'
How can I fix this? When I enter a tag name that's already in use, I want Symfony to use the same tag in the article-tag relationship. The entity classes appear in my previous question about how to avoid duplicate entries in a many-to-many relationship with Doctrine.
There's one mistake in your transformer. Instead of checking if name is null you should verify if a tag was returned:
if (!$tag) {
$tag = new Tag();
$tag->setName($name);
}
You also don't need to persist the tag since by default doctrine will cascade persist all the related entities.
Full method:
public function reverseTransform($name)
{
if (!$name) {
return null;
}
$tag = $this->om
->getRepository('AcmeBundle:Tag')
->findOneByName($name)
;
if (!$tag) {
$tag = new Tag();
$tag->setName($name);
}
return $tag;
}

Categories