Validation of embedded forms with Symfony2 - php

I have a Parents form embedded into another form Student containing the data of the parents of a student. I need to validate the embedded form, because in my code just makes the validation of another form.
StudentType.php
//...
->add('responsible1', new ParentsType(),array('label' => 'Mother'))
->add('responsible2', new ParentsType(),array('label'=> 'Father'))
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'BackendBundle\Entity\Student'
));
}
Entity Parents
//...
/**
* #ORM\OneToMany(targetEntity="Student", mappedBy="$responsible1")
* #ORM\OneToMany(targetEntity="Student", mappedBy="$responsible2")
*/
private $students;
Entity Student
//...
/**
*
* #ORM\ManyToOne(targetEntity="Parents", inversedBy="students", cascade={"persist"})
*/
private $responsible1;
/**
*
* #ORM\ManyToOne(targetEntity="Parents", inversedBy="students", cascade={"persist"})
*/
private $responsible2;
Using the following code in the controller I got the name and the error message of all invalid fields in the main form (Student), but I get get errors embedded forms (Parents), just get the name of the object (responsible1 or responsible2) and the message I get [object Object].
StudentController.php
protected function getErrorMessages(\Symfony\Component\Form\Form $form)
{
$errors = array();
foreach ($form->getErrors() as $key => $error) {
$errors[] = $error->getMessage();
}
foreach ($form->all() as $child) {
if (!$child->isValid()) {
$errors[$child->getName()] = $this->getErrorMessages($child);
}
}
return $errors;
}
/**
* Creates a new Student entity.
*
*/
public function createAction(Request $request)
{
// if request is XmlHttpRequest (AJAX) but not a POSt, throw an exception
if ($request->isXmlHttpRequest() && !$request->isMethod('POST')) {
throw new HttpException('XMLHttpRequests/AJAX calls must be POSTed');
}
$entity = new Student();
$form = $this->createCreateForm($entity);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($entity);
$em->flush();
if ($request->isXmlHttpRequest()) {
return new JsonResponse(array('message' => 'Success!'), 200);
}
return $this->redirect($this->generateUrl('student_show', array('id' => $entity->getId())));
}
if ($request->isMethod('POST')) {
return new JsonResponse(array(
'result' => 0,
'message' => 'Invalid form',
'data' => $this->getErrorMessages($form)),400);
}
return $this->render('BackendBundle:Student:new.html.twig', array(
'entity' => $entity,
'form' => $form->createView(),
));
}
I tried the above code with the function getErrorsAsString() for errors in a string and so if they appear all, so I guess I'll have to add something in the above code to validate objects when "responsible1" or "responsible2" validate all fields.
I need to get all those errors are invalid fields on both forms.I read something to add 'cascade_validation' => true , validation_group or #Assert\Valid() by the code, but I tried and I failed to get it. If someone can explain to me a little worth those, I thank you because I'm new to all this.

Following example flatterns form and subform errors into assoc array, let me know if this is what you are trying to achieve
<?php
namespace Example\Bundle\UtilityBundle\Form;
use Symfony\Component\Form\Form;
class FormErrors
{
public function getArray(Form $form, $style = 'KO')
{
$method = sprintf('get%sErrors', $style);
$messages = $this->$method($form->all());
return $messages;
}
private function getKOErrors(Form $children)
{
$errors = array();
/* #var $child \Symfony\Component\Form\Form */
foreach ($children as $child) {
$type = $child->getConfig()->getType()->getName();
if ($child->count() && ($type !== 'choice')) {
$childErrors = $this->getKOErrors($child->all());
if (sizeof($childErrors)) {
$errors = array_merge($errors, $childErrors);
}
} else {
if (!$child->isValid()) {
// I need only one error message per field
$errors[$child->getName()] = $child->getErrors()->current()->getMessage();
}
}
}
return $errors;
}
}

Related

Dynamically changing properties of row in Symfony 4.4 [duplicate]

This question already has answers here:
Symfony2 Form Entity Update
(3 answers)
Closed 2 years ago.
I'm making a REST API with Symfony 4.4. The API largely revolves around putting data into a database, using Doctrine. I have figured out how to add rows to the database, but now I'm stuck on changing data. I know how I can take a row from the database and that, in theory, I can change fields by calling the setter of a property, but right now, I seem to be getting an array instead of the desired entity and, seemingly more difficult, I want to be able to dynamically change the properties of the existing row, so that I don't have to include every field of the object of the row I'm changing and call every setter.
Here is my code:
// PersonController.php
/**
* #IsGranted("ROLE_USER")
* #Rest\Post("/addperson")
* #param Request $request
* #return Response
*/
public function addOrUpdatePerson(Request $request)
{
$data = json_decode($request->getContent(), true);
$em = $this->getDoctrine()->getManager();
$person = new Person();
$form = $this->createForm(PersonType::class, $person);
$form->submit($data);
if (!$form->isSubmitted() || !$form->isValid())
{
return $this->handleView($this->view($form->getErrors()));
}
if (isset($data['id']))
{
// This person exists, change the row
// What to do?
}
// This person is new, insert a new row
$em->persist($person);
$em->flush();
return $this->handleView($this->view(['status' => 'ok'], Response::HTTP_CREATED));
}
// PersonType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id', IntegerType::class, ['mapped' => false])
->add('inits')
->add('firstname')
->add('lastname')
->add('email')
->add('dateofbirth', DateTimeType::class, [
'widget' => 'single_text',
// this is actually the default format for single_text
'format' => 'yyyy-MM-dd',
])
// Some other stuff
->add('save', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Person::class,
'csrf_protection' => false
));
}
I doubt the Person entity is relevant here, but if it is, please let me know and I'll include it ASAP!
As a response to the suggestion of the other question from Symfony 2; it doesn't seem to fix my problem (entirely). As a result of this question, I have changed my function to this (which doesn't work, but doesn't throw any errors):
public function addOrUpdatePerson(Request $request)
{
$data = json_decode($request->getContent(), true);
$em = $this->getDoctrine()->getManager();
if (isset($data['id'])) {
// This person exists
$existing = $em->getRepository(Person::class)->find(['id' => $data['id']]);
$this->getDoctrine()->getManager()->flush();
$form = $this->createForm(PersonType::class, $existing);
$form->handleRequest($request);
// this doesn't seem to do anything
// $em->persist($existing);
$em->flush();
return $this->handleView($this->view($existing));
}
}
I think I'm still missing some info, like what to do at // perform some action, such as save the object to the database. I also notice a lot has changed since Symfony 2, and as a result it is not obvious to me what I should do.
After '$person = new Person()' juste add :
If (isset($data['id']) && 0 < $data['id']) {
$person=$em->getRepository(Person::class)->find($data['id']);
}
If (!$person) {
Throw new \Exception('Person not found');
}
1.) You don't have to use json_decode directly. You can use the following code instead:
// Person controller
/**
* #Route("/person", name="api.person.add", methods={"POST"})
* #Security("is_granted('ROLE_USER')")
*/
public function addPerson(Request $request)
{
$person = new Person();
$form = $this->createForm(PersonType::class, $person);
$form->submit($request->request->all());
if (!$form->isSubmitted() || !$form->isValid()) {
throw new \Exception((string) $form->getErrors(true));
}
$em = $this->getDoctrine()->getManager();
$em->persist($person);
$em->flush();
...
}
2.) When you're updating entity you need to load it first and skip the $em->persist($entity); part. In this case, we provide the ID of the entity via the path variable (there are various ways to provide it but this one is quite common). NOTE: I've set $id parameter as mixed because it can be integer or string if you're using UUID type of IDs.
// Person controller
/**
* #Route("/person/{id}", name=api.person.patch", methods={"PATCH"})
* #Security("is_granted('ROLE_USER')")
*/
public function patchPerson(Request $request, mixed $id)
{
// Load person
$personRepository = $this->getDoctrine()->getRepository(Person::class);
$person = $personRepository->find($id);
if (!$person) { throw new \Exception('Entity not found'); }
$form = $this->createForm(PersonType::class, $person);
$form->submit($request->request->all());
if (!$form->isSubmitted() || !$form->isValid()) {
throw new \Exception((string) $form->getErrors(true));
}
$em = $this->getDoctrine()->getManager();
$em->flush();
...
}
3.) In general usage, we don't set the ID property via posted data (unless it is required). We rather use generated value instead. When you insert new entity you gen use its ID to address it for modifications. Sample:
<?php
namespace App\Entity;
use Ramsey\Uuid\Uuid;
use Doctrine\ORM\Mapping as ORM;
class Person
{
/**
* #var Uuid
*
* #ORM\Id
* #ORM\Column(type="uuid", unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
* #Groups({"public"})
*/
protected $id;
// Other entity properties ...
public function getId(): ?string
{
return $this->id;
}
public function setId(string $id): self
{
$this->id = $id;
return $this;
}
// Setters and getters for other entity properties ...
}
4.) Entity class in FormType (PersonType.php) is very relevant. After form submission and validation you access properties of the entity itself within the controller - not the decoded payload data from the request directly. Symfony's form system will make sure that the input data is valid and matches the requirements and constraints set in the entity model or form type specification.
// Person controller
/**
* #Route("/person", name="api.person.add", methods={"POST"})
* #Security("is_granted('ROLE_USER')")
*/
public function addPerson(Request $request)
{
$person = new Person();
$form = $this->createForm(PersonType::class, $person);
$form->submit($request->request->all());
if (!$form->isSubmitted() || !$form->isValid()) {
throw new \Exception((string) $form->getErrors(true));
}
$em = $this->getDoctrine()->getManager();
$em->persist($person);
$em->flush();
$id = $person->getId();
$firstName = $person->getFirstname();
$lastName = $person->getLastname();
// etc
...
}
5.) If you want to use the same method/endpoint for adding and updating entity you can do something like #lasouze mentioned.
// Person controller
/**
* #Route("/person", name=api.person.add_or_update", methods={"POST", "PATCH"})
* #Security("is_granted('ROLE_USER')")
*/
public function patchPerson(Request $request)
{
$id = $request->request->get('id', null);
if (!$id) {
$person = new Person();
} else {
// Load person
$personRepository = $this->getDoctrine()->getRepository(Person::class);
$person = $personRepository->find($id);
if (!$person) { throw new \Exception('Entity not found'); }
}
$form = $this->createForm(PersonType::class, $person);
$form->submit($request->request->all());
if (!$form->isSubmitted() || !$form->isValid()) {
throw new \Exception((string) $form->getErrors(true));
}
$em = $this->getDoctrine()->getManager();
$em->flush();
...
}
PS: $form->submit($request->request->all()); will not work for file uploads because $request->request->all() does not contain parameters provided by $_FILES. In my case I ended up merging data like $form->submit(array_merge($request->request->all(), $request->files->all())); but this is probably not the best solution. I'll update my answer if I'll figure out anything better.

Objects are not persisted by Doctrine

last couple of days I've been busy making a form using Doctrine and MongoDB. Companies should be able to reserve tables, chairs, .. at a certain event by use of this form. The snippet below shows the controller for this form.
The 'ObjectMap' object maps the amount of a certain object to the object itself. The controller creates all the 'ObjectMap' objects, and adds them to the company object. However, the 'ObjectMap' objects are persisted by Doctrine (they show up in the database) but the company object isn't modified at all, there is no database request made by MongoDB. The persist() function seems to have no effect at all.
public function logisticsAction(Company $company)
{
$form = $this->createForm(new LogisticsForm($this->getDoctrine()->getManager()), $company);
if ($this->getRequest()->isMethod('POST')) {
$form->bind($this->getRequest());
if ($form->isValid()) {
$formData = $this->getRequest()->request->get('_company_logistics_edit');
$objects = $this->getDoctrine()->getManager()
->getRepository('Jobfair\AppBundle\Document\Company\Logistics\Object')
->findAll();
foreach($objects as $object) {
$requirement = $formData['objectRequirement_'.$object->getId()];
$map = new ObjectMap($requirement, $object);
$this->getDoctrine()->getManager()->persist($map);
$company->addObjectMap($map);
//print_r($company->getObjectMaps());
}
$this->getDoctrine()->getManager()->persist($company);
$this->getDoctrine()->getManager()->flush();
$this->getRequest()->getSession()->getFlashBag()->add(
'success',
'The information was successfully updated!'
);
return $this->redirect(
$this->generateUrl(
'_company_settings_logistics',
array(
'company' => $company->getId(),
)
)
);
}
}
The Company object is defined here:
class Company{
/**
* #ODM\Id
*/
private $id;
/*
* #ODM\ReferenceMany(targetDocument="Jobfair\AppBundle\Document\Company\Logistics\ObjectMap", cascade={"persist", "remove"})
*/
private $objectMaps;
public function __construct($name = null, $description = null)
{
$this->objectMaps = new ArrayCollection();
}
public function getId()
{
return $this->id;
}
public function getObjectMaps()
{
return $this->objectMaps;
}
public function getObjectsArray()
{
$objects = array();
foreach($this->objectMaps as $map)
$objects[] = array(
'name' => $map->getObject()->getName(),
'amount' => $map->getRequirement()
);
return $objects;
}
public function addObjectMap(ObjectMapDocument $objectMap)
{
$this->objectMaps[] = $objectMap;
}
public function removeObject(ObjectMapDocument $objectMap)
{
$this->objectMaps->removeElement($objectMap);
}
}

Keep good data in form after failed validation on symfony form collection

I have a form with a dynamic form collection. The point is that I don't want to allow the user to remove specific entries (that are modified in another part of the app). So I added a specific validation constraint that works: the form is not valid if I remove an element that is not "deletable".
The problem is that, as the element was removed in what the user submitted, the element is not in the form anymore, and after submit form data is locked.
Here is an example to show the problem:
class AppointmentController extends Controller
{
public function editAppointment(Request $request, Appointment $appointment)
{
// Here
// count($appointment->getSlot()) === 3
$form = $this->createForm('appointment', $appointment, [
'questionnaire' => $questionnaire
]);
$form->handleRequest($request);
if ($form->isValid()) {
// Persisting
}
// Here on failing validation, there is
// count($appointment->getSlot()) === 2
// Because we removed one slot from "dynamically" in the form, but the user can't do that,
// so we need to reset the slots but it's not possible because form data is locked after "submit".
return $this->render('App:Appointment:edit.html.twig', ['form' => $form->createView()]);
}
}
class AppointmentType extends AbstractTYpe
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('slots', 'collection', [
'type' => new SlotType(),
'allow_add' => true,
'prototype' => true,
'allow_delete' => true,
'error_bubbling' => false,
'by_reference' => false,
'constraints' => [
new WrongSlotRemoval($builder->getData()->getSlots())
]
])
;
}
}
class WrongSlotRemoval extends Constraint
{
public $message = 'Impossible to delete slog.';
/**
* #var null|\App\Entity\AppointmentSlot[]
*/
private $slots;
/**
* #param \App\Entity\AppointmentSlot[]|null $slots
*/
public function __construct($slots = null)
{
// Clone the collection because it can be modified by reference
// in order to add or delete items.
if ($slots !== null) {
$this->slots = clone $slots;
}
}
/**
* #return \App\Entity\AppointmentSlot[]
*/
public function getSlots()
{
return $this->slots;
}
/**
* #param \App\Entity\AppointmentSlot[] $slots
* #return self
*/
public function setSlots($slots)
{
$this->slots = $slots;
return $this;
}
}
class WrongSlotRemovalValidator extends ConstraintValidator
{
/**
* #param \App\Entity\AppointmentSlot[] $object
* #param WrongSlotRemoval $constraint
*/
public function validate($object, Constraint $constraint)
{
foreach($constraint->getSlots() as $slot) {
if (!$object->contains($slot) && !$slot->isDeletable()) {
$this->context
->buildViolation($constraint->message)
->addViolation()
;
return;
}
}
}
}
Any idea about how to modify form data after submit ?
Here is a screen of the problem: http://file.nekland.fr/dev/pb_form_collection.jpeg
This is a Symfony limitation: https://github.com/symfony/symfony/issues/5480
A workaround exists: re-creating the form and copy errors of each node of the form to the new form (that contains good data).
My solution will be to throw an error in the validator because the user is not able to remove an item (the cross is disabled), so he hacked the system. Showing an error is not a problem.

Memory management Symfony forms and Doctrine

When building a form using Symfony the build of the form is terribly slow and the memory spikes.
The form is build using some subforms and uses some one-to-many relations. When the data of the form becomes larger (more entities in the many side) the form is slower and memory is usage is getting larger this seem okey though the amount of time and memory usage don't seem to.
Example when having about 71 enities in the many side the memory usage is about 116 MB and takes 14 seconds to load.
I already deduced the number of queries done (from 75 to 4) though the memory spike still happens the moment the form is created
$form = $this->createForm(new TapsAndAppliancesType(), $taps);
Any tips and tricks to speed this up?
I assume you use type entity in your form. They are quite heavy, since first all entities are fetched as objects and then reduced to some id => label style.
So you could write your own entityChoice type, which works with an id => label -array (so nothing is fetched as an object in the frist place) and add a DataTransformer to this type:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use MyNamespace\EntityToIdTransformer;
class EntityChoiceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addModelTransformer(new EntityToIdTransformer($options['repository']));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'empty_value' => false,
'empty_data' => null,
));
$resolver->setRequired(array(
'repository'
));
}
public function getParent()
{
return 'choice';
}
public function getName()
{
return 'entityChoice';
}
}
And as DataTransformer:
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class EntityToIdTransformer implements DataTransformerInterface
{
private $entityRepository;
public function __construct(EntityRepository $entityRepository)
{
$this->entityRepository = $entityRepository;
}
/**
* #param object|array $entity
* #return int|int[]
*
* #throws TransformationFailedException
*/
public function transform($entity)
{
if ($entity === null) {
return null;
}
elseif (is_array($entity) || $entity instanceof \Doctrine\ORM\PersistentCollection) {
$ids = array();
foreach ($entity as $subEntity) {
$ids[] = $subEntity->getId();
}
return $ids;
}
elseif (is_object($entity)) {
return $entity->getId();
}
throw new TransformationFailedException((is_object($entity)? get_class($entity) : '').'('.gettype($entity).') is not a valid class for EntityToIdTransformer');
}
/**
* #param int|array $id
* #return object|object[]
*
* #throws TransformationFailedException
*/
public function reverseTransform($id)
{
if ($id === null) {
return null;
}
elseif (is_numeric($id)) {
$entity = $this->entityRepository->findOneBy(array('id' => $id));
if ($entity === null) {
throw new TransformationFailedException('A '.$this->entityRepository->getClassName().' with id #'.$id.' does not exist!');
}
return $entity;
}
elseif (is_array($id)) {
if (empty($id)) {
return array();
}
$entities = $this->entityRepository->findBy(array('id' => $id)); // its array('id' => array(...)), resulting in many entities!!
if (count($id) != count($entities)) {
throw new TransformationFailedException('Some '.$this->entityRepository->getClassName().' with ids #'.implode(', ', $id).' do not exist!');
}
return $entities;
}
throw new TransformationFailedException(gettype($id).' is not a valid type for EntityToIdTransformer');
}
}
And finally register the FormType as new Type in service.yml
services:
myNamespace.form.type.entityChoice:
class: MyNamespace\EntityChoiceType
tags:
- { name: form.type, alias: entityChoice }
You can then use it in your form as
$formBuilder->add('appliance', 'entityChoice', array(
'label' => 'My Label',
'repository' => $repository,
'choices' => $repository->getLabelsById(),
'multiple' => false,
'required' => false,
'empty_value' => '(none)',
))
with $repository as an instance of your desired repository and 'choices' as an array with id => label

Zend form and annotations validation

I'm trying using annotatnions to build and validate zend forms.
But currently I recive an error when I open showformAction:
"Fatal error: Uncaught exception 'Zend\Form\Exception\InvalidElementException' with message 'No element by the name of [username] found in form' ..."
So below is my code. What I doing wrong ?
Entity\User.php
namespace Application\Model;
use Zend\Form\Annotation;
/**
* #Annotation\Hydrator("Zend\Stdlib\Hydrator\ObjectProperty")
* #Annotation\Name("user")
*/
class User
{
/**
* #Annotation\Attributes({"type":"text" })
* #Annotation\Validator({"type":"Regex","options":{"regex":"/^[a-zA-Z][a-zA-Z0-9_-]{1,19}/"}})
* #Annotation\Options({"label":"Username:"})
*/
public $username;
}
Controller\ProductsController.php
namespace Application\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\Json\Json;
use Zend\View\Model\JsonModel;
use Zend\View\Model\ViewModel;
use Zend\Debug\Debug;
use Application\Entity\Products;
use Application\Entity\Category;
use Application\Form\ProductsForm;
use Doctrine\ORM\EntityManager;
use Application\Model\User;
use Zend\Form\Annotation\AnnotationBuilder;
class ProductsController extends AbstractActionController {
protected $albumTable;
protected $em;
protected $form;
public function savetodb($data) {
//code save to db ....
}
protected function getForm() {
$entity = new User();
$builder = new AnnotationBuilder();
$this->form = $builder->createForm($entity);
return $this->form;
}
public function showformAction() {
$viewmodel = new ViewModel();
$form = $this->getForm();
$request = $this->getRequest();
//disable layout if request by Ajax
$viewmodel->setTerminal($request->isXmlHttpRequest());
$is_xmlhttprequest = 1;
if (!$request->isXmlHttpRequest()) {
//if NOT using Ajax
$is_xmlhttprequest = 0;
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
//save to db <span class="wp-smiley emoji emoji-wink" title=";)">;)</span>
$this->savetodb($form->getData());
}
}
}
$viewmodel->setVariables(array(
'form' => $form,
// is_xmlhttprequest is needed for check this form is in modal dialog or not
// in view
'is_xmlhttprequest' => $is_xmlhttprequest
));
return $viewmodel;
}
public function validatepostajaxAction() {
$form = $this->getForm();
$request = $this->getRequest();
$response = $this->getResponse();
$messages = array();
if ($request->isPost()) {
$form->setData($request->getPost());
if (!$form->isValid()) {
$errors = $form->getMessages();
foreach ($errors as $key => $row) {
if (!empty($row) && $key != 'submit') {
foreach ($row as $keyer => $rower) {
//save error(s) per-element that
//needed by Javascript
$messages[$key][] = $rower;
}
}
}
}
if (!empty($messages)) {
$response->setContent(\Zend\Json\Json::encode($messages));
} else {
//save to db <span class="wp-smiley emoji emoji-wink" title=";)">;)</span>
$this->savetodb($form->getData());
$response->setContent(\Zend\Json\Json::encode(array('success' => 1)));
}
}
return $response;
}
}
Your annotation should be
/**
* #Annotation\Type("Zend\Form\Element\Text")
* #Annotation\Validator({"type":"Regex","options":{"regex":"/^[a-zA-Z][a-zA-Z0-9_-]{1,19}/"}})
* #Annotation\Options({"label":"Username:"})
*/

Categories