Symfony2 : Embedded form validation is not triggered when editing - php

Symfony version : 2.8.5
Context: I have an entity Restaurant which has a OneToOne relationship with an entity Coordinates which have several relations with other entities related to Coordinates informations. I my backend I create a form related to Restaurant entity with a custom nested form related to Coordinates.
Nota : I use EasyAdminBundle to generate my backend.
Entities relations scheme :
Restaurant
1 ________ 1 `Coordinates`
* ________ 1 `CoordinatesCountry`
1 ________ 1 `CoordinatesFR`
* ________ 1 `CoordinatesFRLane`
* ________ 1 `CoordinatesFRCity`
Backend view :
At this point I try the following scenario :
I create a new Restaurant, so I fill the fields related form for the first time. I let the Coordinates nested form blank (empty fields). So after form submission, validation messages are displayed (see image below).
I edit the previous form and this time I fill the fields of the Coordinates nested form. After form submission, a new Coordinates entity is hydrated and a relationship is created between Restaurant and Coordinates.
Once again I edit the previous form and this time I clear all the fields of the Coordinates nested form. The validation is not triggered and I get the following error :
Expected argument of type "FBN\GuideBundle\Entity\CoordinatesFRCity",
"NULL" given
I precise that in CoordinatesFRType (see code below), to trigger the validations message the first time I had to use the option empty_data with a closure (like described in the official doc) to instatiate a new CoordinatesFR instance in case of empty datas (all fields blank). But here, in this article (written by the creator of the Symfony form component), it is explained (see empty_data and datta mappers paragraphs) that the empty_data is only called at object creation. So I think this the reason why my validation does not work anymore in case of edition.
Question : why the validation is not effective anymore when editing my form and clearing all embedded form ?
The code (only what is necessary) :
Restaurant entity
use Symfony\Component\Validator\Constraints as Assert;
class Restaurant
{
/**
* #ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\Coordinates", inversedBy="restaurant", cascade={"persist"})
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
* #Assert\Valid()
*/
private $coordinates;
}
Coordinates entity
use Symfony\Component\Validator\Constraints as Assert;
class Coordinates
{
/**
* #ORM\ManyToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesCountry")
* #ORM\JoinColumn(nullable=false)
*/
private $coordinatesCountry;
/**
* #ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesFR", inversedBy="coordinates", cascade={"persist"})
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
* #Assert\Valid()
*/
private $coordinatesFR;
/**
* #ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\Restaurant", mappedBy="coordinates")
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
private $restaurant;
}
CoordinatesFR entity
use Symfony\Component\Validator\Constraints as Assert;
class CoordinatesFR extends CoordinatesISO
{
/**
* #ORM\ManyToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesFRLane")
* #ORM\JoinColumn(nullable=true)
* #Assert\NotBlank()
*/
private $coordinatesFRLane;
/**
* #ORM\ManyToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesFRCity")
* #ORM\JoinColumn(nullable=false)
* #Assert\NotBlank()
*/
private $coordinatesFRCity;
/**
* #ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\Coordinates", mappedBy="coordinatesFR")
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
private $coordinates;
}
Easy Admin config (equivalent to RestaurantType)
easy_admin:
entities:
Restaurant:
class : FBN\GuideBundle\Entity\Restaurant
form:
fields:
- { property: 'coordinates', type: 'FBN\GuideBundle\Form\CoordinatesType' }
CoordinatesType
class CoordinatesType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('CoordinatesCountry', EntityType::class, array(
'class' => 'FBNGuideBundle:CoordinatesCountry',
'property' => 'country',
))
->add('coordinatesFR', CoordinatesFRType::class)
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'FBN\GuideBundle\Entity\Coordinates',
));
}
}
CoordinatesFRType
class CoordinatesFRType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('laneNum', TextType::class)
->add('coordinatesFRLane', EntityType::class, array(
'class' => 'FBNGuideBundle:CoordinatesFRLane',
'property' => 'lane',
'placeholder' => 'label.form.empty_value',
))
->add('laneName', TextType::class)
->add('miscellaneous', TextType::class)
->add('locality', TextType::class)
->add('metro', TextType::class)
->add('coordinatesFRCity', EntityType::class, array(
'class' => 'FBNGuideBundle:CoordinatesFRCity',
'property' => 'display',
'query_builder' => function (CoordinatesFRCityRepository $repo) {
return $repo->getAscendingSortedCitiesQueryBuilder();
},
'placeholder' => 'label.form.empty_value',
))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'FBN\GuideBundle\Entity\CoordinatesFR',
// Ensures that validation error messages will be correctly displayed next to each field
// of the corresponding nested form (i.e if submission and CoordinatesFR nested form with all fields empty)
'empty_data' => function (FormInterface $form) {
return new CoordFR();
},
));
}
}

Related

Symfony Forms and ManyToMany. How to configure form with file upload field that is also EntityType field?

I need to make form field with file upload that is also part of ManyToMany entity. Now my configuration looks like below, and it works...
class ProductTypeNew extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('price')
->add('description', TextareaType::class)
->add('quantity')
->add('file', FileType::class, array('label' => 'Zdjęcie'))
;
... but I need to manually get form input in controller and sets to form entity
if ($form->isSubmitted() && $form->isValid())
{
$image = new ShopProductImages();
$file = $product->getFile();
$fileName = $this->generateUniqueFileName().'.'.$file->guessExtension();
$file->move(
$this->getParameter('shop_images_directory'),
$fileName
);
$image->setFile($fileName);
$product->addShopProductImages($image);
$product->setFile($fileName);
$em = $this->getDoctrine()->getManager();
$em->persist($image);
$em->persist($product);
$em->flush();
I would like to do something like this (but it's not working):
->add('shopProductImages', EntityType::class, array(
'by_reference' => false,
'entry_type' => FileType::class,
)
New version of form types with Embeded Forms that also cause problem:
Expected value of type "Doctrine\Common\Collections\Collection|array"
for association field
"AppBundle\Entity\ShopProducts#$shopProductImages", got
"Symfony\Component\HttpFoundation\File\UploadedFile" instead.
... with below configuration:
ProductTypeNew:
class ProductTypeNew extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', null, array('label' => 'Nazwa'))
->add('price', null, array('label' => 'Cena'))
->add('description', TextareaType::class, array('label' => 'Opis'))
->add('quantity', null, array('label' => 'Ilość'))
->add('shopProductImages', ShopProductsImagesType::class);
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ShopProducts::class,
]);
}
ShopProductsImagesType:
class ShopProductsImagesType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('file', FileType::class, array('label' => 'Zdjęcie'))
;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// 'data_class' => ShopProductImages::class,
'data_class' => null,
]);
}
Entity ShopProducts:
/**
* ShopProducts
*
* #ORM\Table(name="shop_products")
* #ORM\Entity
*/
class ShopProducts
{
....
/**
* INVERSE SIDE
*
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(
* targetEntity="AppBundle\Entity\ShopProductImages",
* mappedBy="shopProducts",
* cascade={"persist"}
* )
*/
private $shopProductImages;
Entity ShopProductImages:
* #ORM\Entity
*/
class ShopProductImages
{
/**
* #var string
*
* #ORM\Column(name="file", type="text", length=255, nullable=true)
*/
private $file;
If you use EntityType field class, the entry_type is not an expected type. It expects to use an Entity binded to your database through Doctrine, from yourbundleapp/Entity. EntityType acts like a ChoiceType, but it directly interact with the Doctrine entity declared in parameter class. You can find how it works here: https://symfony.com/doc/current/reference/forms/types/entity.html
From what I can understand, you want to be able to download files on your app, so maybe you have difficulties to understand how submission forms work on Symfony.
You have to first define your new entity (from yourbundleapp/Entity), and then pass it as an argument to your form (from yourbundleapp/Form), like this:
$image = new ShopProductImages();
$form = $this->get('form.factory')->create(ProductTypeNew::class, $image);
If you want, you can also add form in your first form by embedding it: https://symfony.com/doc/current/form/embedded.html
If I understood bad, please could you be more verbose about what you want to do and what you did?

How to make checkboxes work in Symfony 2?

I am currently creating a form that lets the user choose a certain skills from a Dropdown and a Checkbox for hobbies that lets the user check as much he/she wants.
Here is my Table for that: CurriculumVitae
/* namespace ........... */
use Doctrine\ORM\Mapping as ORM;
/**
* CurriculumVitae
*
* #ORM\Table(name="foo_cv")
* #ORM\Entity
*/
class CurriculumVitae
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var integer
* #ORM\ManyToOne(targetEntity="Foo\BarBundle\Entity\Skills")
* #ORM\JoinColumn(name="skills", referencedColumnName="id")
*/
private $skills;
/**
* #var integer
* #ORM\ManyToOne(targetEntity="Foo\BarBundle\Entity\Hobby", cascade={"persist"})
* #ORM\JoinColumn(name="hobbies", referencedColumnName="id")
*/
private $hobby;
/* Setters and Getters ....... */
}
Here are some codes for my Form Type: CurriculumVitaeType
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('skills', 'entity', array('class' =>'FooBarBundle:Skills','property' => 'skills'))
->add('hobby', 'entity', array( 'class' => 'FooBarBundle:Hobby','property' => 'hobbies', 'expanded'=>true,'multiple'=>true, 'label' => 'hobbies'))
->add('save','submit',array('label'=>'Submit'))
;
}
/* OptionsResolverInterface ..... */
/* getName() .... */
I call my form in my twig this way in: cv.twig.html
{{ form(curriculumForm) }}
And lastly in my controller: CurriculumController
$em = $this->getDoctrine()->getManager();
$cv = new CurriculumVitae();
$curriculumForm = $this->createForm(new CurriculumVitaeType(), $cv);
$curriculumForm->handleRequest($request);
if ($curriculumForm->isValid()) {
$em->persist($cv);
$em->flush();
return $this->redirect($this->generateUrl('foo_main_window'));
}
return array('curriculumForm'=> $curriculumForm->createView());
The Form Displays correctly but when I choose a skill from the dropdown and assign a certain hobby and click on submit, an error is thrown.
Found entity of type Doctrine\Common\Collections\ArrayCollection on association Foo\BarBundle\Entity\CurriculumVitae#hobby, but expecting Foo\BarBundle\Entity\Hobby
I dont know if i missed something but i think the error occurs in the process of persisting the data after the form is submitted.
That's because you have many-to-one relation, which means
Many CurriculumVitaes can have (the same) single Hobby
But on the other hand you've created in your form a field with option 'multiple'=>true, which means that you let the user to choose multiple hobbies. Therefore form returns ArrayCollection of Hobby entites instead of single instance.
That doesn't match. You need to either remove multiple option, or make many-to-many relation on $hobby property.

Symfony 2: Avoid persisting photo entity collection when no photo is uploaded

I have an entity AdsList which represents ads and I have linked entity Photos.
When I create or update the entity AdsList a new row is created in the Photos table in the database even when I do not upload a photo and I don't want that to happen. I wish the table to be updated ONLY if there is a photo uploaded.
The AdsList entity:
namespace obbex\AdsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use obbex\AdsBundle\Entity\Photos;
/**
* AdsList
*
* #ORM\Table()
* #ORM\Entity
* #ORM\Entity(repositoryClass="obbex\AdsBundle\Entity\AdsListRepository")
* #ORM\HasLifecycleCallbacks()
*/
class AdsList
{
... several properties
/**
* #ORM\OneToMany(targetEntity="obbex\AdsBundle\Entity\Photos",mappedBy="adslist", cascade={"persist","remove"})
* #ORM\JoinColumn(nullable=true)
*/
protected $photos;
... more getters and setters
And this is the entity Photos
namespace obbex\AdsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* Photos
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="obbex\AdsBundle\Entity\PhotosRepository")
* #ORM\HasLifecycleCallbacks
*/
class Photos
{
/**
* #ORM\ManyToOne(targetEntity="obbex\AdsBundle\Entity\AdsList", inversedBy="photos")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="adslist_id", referencedColumnName="id",onDelete="CASCADE")
* })
*/
protected $adslist;
And here is the form AdsListType
class AdsListType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email',TextType::class)
->add('telephone', IntegerType::class,array('required'=>false))
->add('displayPhone',CheckboxType::class,array('required'=>false,'attr'=>array('checked'=>false)))
->add('title',TextType::class)
->add('description', TextareaType::class)
->add('country', EntityType::class,array(
'class' => 'obbexAdsBundle:Countries',
'choice_label'=>'countryName',
'multiple'=>false
))
->add('region',TextType::class)
->add('department',TextType::class)
->add('address',TextType::class, array('required' => false))
->add('city',TextType::class)
->add('zipCode',TextType::class)
->add('statusPro',TextType::class)
->add('publication', CheckboxType::class)
->add('price', IntegerType::class)
->add('photos',CollectionType::class, array('entry_type'=> 'obbex\AdsBundle\Form\PhotosType',
'allow_add' => true,
'allow_delete'=>true,
'data'=>array(new Photos() ),
'required' => false
)
)
->add('save', SubmitType::class)
;
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'obbex\AdsBundle\Entity\AdsList',
));
}
}
I guess that there is a procedure in order to avoid persisting the Photos entity but I've tried in the controller:
public function editAdAction(Request $request,AdsList $ads){
$em = $this->getDoctrine()->getManager();
$form = $this->createForm(AdsListEditType::class, $ads);
$form->handleRequest($request);
if ($form->isValid()) {
if($form->get('save')->isClicked()){
//this condition in case no photo is uploaded
if($form->get('photos')->getData()[0]->getFile() === null){
$photos = $ads->getPhotos();
//what do here? or maybe the problem is somewhere else
//I've tried $em->refresh($photo); but it throws the error I gave
}
$em->flush();
$id = $ads->getId();
return $this->redirect($this->generateUrl('ad_edition',array('id'=>$id)));
}
return $this->render('obbexAdsBundle:Default:editAd.html.twig',
array(
'ads'=>$ads,
'form'=>$form->createView()
));
}
But I have the following error
Entity obbex\AdsBundle\Entity\Photos#000000000b2d669200007fb2152337c5
is not managed. An entity is managed if its fetched from the database or
registered as new through EntityManager#persist
Has anyone done this? it seems pretty standard to me. In case no photo is uploaded don't do anything in the database to keep it clean
In the case there is no file set
We should not touch the image property
/**
* Add image
*
* #param \obbex\AdsBundle\Entity\Photo $image
*
* #return Painting
*/
public function addImage(\Art\GeneralBundle\Entity\Photo $photo)
{
if($photos->getFile() !== NULL){
$this->photos[] = $photo;
$image->setAdlist($this);
}
return $this;
}
and then no photo will be persisted if the field file is not filled with information.
Note that I linked the AdsList entity with the Photo Entity via the object AdsList itself not from the controller which was incorrect.

Symfony ManyToMany in Form

I have entity developer and entity skill, skill have ManyToMany with Platforms, Language and Speciality and I need create form for developer, developer can selected skills and when selected some skill developer can selected Platforms, Language and Speciality for this skill. If developer selected two skill or more so have more Platforms, Language and Speciality for selected. And I don't know how this is create in Symfony. Now I create form only selected skills
class DeveloperProfessionalSkillsType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name');
$builder->add('skills','entity',
array(
'class'=>'Artel\ProfileBundle\Entity\Skill',
'property'=>'skill',
'multiple'=>true,
)
);
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Artel\ProfileBundle\Entity\Developer',
'validation_groups' => array('professional_skills')
));
}
/**
* #return string
*/
public function getName()
{
return 'developer_professional_skills';
}
but now I have error form is not valid, interesting when I add 'expanded' => true, all work fine, but I don't need expanded I need simple selected field:
this is my entity
class Skill
{
/**
* #ORM\ManyToMany(targetEntity="Artel\ProfileBundle\Entity\Skill", mappedBy="skills", cascade={"persist"})
*/
protected $developers;
/**
* #var \Artel\ProfileBundle\Entity\CodeDirectoryProgramLanguages
*
* #ORM\ManyToMany(targetEntity="CodeDirectoryProgramLanguages", inversedBy="skills", cascade={"persist"})
*/
protected $language;
/**
* #var \Artel\ProfileBundle\Entity\CodeDirectoryPlatforms
*
* #ORM\ManyToMany(targetEntity="CodeDirectoryPlatforms", inversedBy="skills", cascade={"persist"})
*/
protected $platforms;
/**
* #var \Artel\ProfileBundle\Entity\CodeDirectorySpecialities
*
* #ORM\ManyToMany(targetEntity="CodeDirectorySpecialities", inversedBy="skills", cascade={"persist"})
*/
protected $specialities;
and my action
public function submitProfessionalSkillsAction($securitytoken)
{
$em = $this->getDoctrine()->getManager();
$form = $this->createForm(new DeveloperProfessionalSkillsType(), $user->getDeveloper());
$form->handleRequest($request);
if ($form->isValid()) {
$em->flush();
Recommend, please, how best to solve this problem
Instead of
$builder->add('skills','entity',
array(
'class'=>'Artel\ProfileBundle\Entity\Skill',
'property'=>'skill',
'multiple'=>true,
)
);
You can create new form and add it to your current form like this:
$builder->add('skills', new SkillsType()
);
And in new form you can define all your fields like:
$builder->add('language','entity',
array(
'class'=>'Artel\ProfileBundle\Entity\Language',
'property'=>'some_property',
'multiple'=>true,
)
);
if you need many skills you can use collection form type:
->add('skills', 'collection', array('type' => new SkillsType(),
'allow_add' => true,'by_reference' => false))

Symfony form - how to add some logic while validating

I have a form, used for a course subscription, in which I have 2 entity fields, activite and etudiant. I would like NOT to validate this form IF the trainer i already booked (information that i can find in the DB through the entity activite).
How (where...) can i add some logic instructions to control the validation of this form?
Is anybody has an idea? A lead? It would be so simple WITHOUT Symfony (for me)!...
Thanks
class InscriptionType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('activiteId', 'entity',
array('label'=>'Activité',
'attr'=>array('class'=>'form-control'),
'class'=>'AssoFranceRussie\MainBundle\Entity\Activite',
'property'=>'nomEtNiveauEtJour',
))
->add('etudiantId', 'entity',array('label'=>'Etudiant',
'attr'=>array('class'=>'form-control'),
'class'=>'AssoFranceRussie\MainBundle\Entity\Etudiant',
'property'=>'NomEtPrenom',
))
;
}
You can write your own constraints and validators by extending the Symfony validation classes.
You need to extend Symfony\Component\Validator\Constraint to define the constraint and Symfony\Component\Validator\ConstraintValidator to define the validation code.
There are probably other ways to do this as well, but this gives you complete control of the validation.
You could do what you want in this way:
1- You need a variable to store the EntityManager in your InscriptionType class:
protected $em;
public function __construct($em) {
$this->em = $em;
}
2- Pass the entity manager to the FormType class from your controller as below:
new InscriptionType( $this->getDoctrine()->getManager() );
3- Add the logic what you want in the setDefaultOptions function to specify the validation_groups what you want for the form:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$p = $this->em->getRepository('YourBundle:YourEntity')->find(1);
if($p){
$resolver->setDefaults(array(
'data_class' => 'YourBundle\Entity\YourEntity',
'validation_groups' => array('myValidation1'),
'translation_domain'=>'custom'
));
}
else{
$resolver->setDefaults(array(
'data_class' => 'YourBundle\Entity\YourEntity',
'validation_groups' => array('myValidation2'),
'translation_domain'=>'custom'
));
}
}
4- In your entity, you need to specify the validation groups for the fields of the entity:
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
* #Assert\NotNull(groups={"myValidation1"})
*/
private $name;
/**
* #var date
*
* #ORM\Column(name="start", type="date")
* #Assert\NotNull(groups={"myValidation1", "myValidation2"})
* #Assert\Date()
*/
private $start;
In this case, the field 'start' would be validated in both cases, but the first only with the myValidation1 is the group to be validated.
In this way, you could control which fields you want to validate.

Categories