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.
Related
I have a Business Entity and a BusinessObject Entity, and I would like to link the BusinessObject to the current Business when I create a new BusinessObject.
For example, if my route is business/{id}/object/new, I would like to have the object related with the Business (thanks to the id).
In my BusinessObject Controller, I managed to use #ParamConverter to get the Business id.
In my BusinessObject Form, I put an HiddenType to my business entry because I don't want it to appear, and set data to business_ID.
I struggle in configureOptions to get the business ID, I can't figure out how to get the business id from here.
BusinessObject Controller (route new):
/**
* #Route("/{post_id}/new", name="business_object_new", methods="GET|POST")
* #ParamConverter("business", options={"id" = "post_id"})
*/
public function new(Request $request,Business $business): Response
{
$businessObject = new BusinessObject();
$businessID = $business->getId();
$form = $this->createForm(BusinessObjectType::class, $businessObject,array(
'business_ID'=>$businessID,
));
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($businessObject);
$em->flush();
return $this->redirectToRoute('business_object_index');
}
return $this->render('business_object/new.html.twig', [
'business_object' => $businessObject,
'business'=>$business,
'form' => $form->createView(),
]);
}
BusinessObjectType:
class BusinessObjectType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('object',TextType::class)
->add('complement')
->add('status')
->add('durationExpected')
->add('durationAchieved')
->add('client')
->add('projectManager')
->add('business',HiddenType::class,array(
'data' => $options['business_ID']
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => BusinessObject::class,
'business_ID'=>Business::class
]);
}
}
With this code, I get an error Expected argument of type "App\Entity\Business or null", "string" given. I think this have something to do with the function configureOptions() in my Form
The approach can be:
public function new(Request $request,Business $business): Response
{
$businessObject = new BusinessObject();
$form = $this->createForm(BusinessObjectType::class, $businessObject);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// I suppose your setter is `setBusiness`, otherwise use more suitable one
$businessObject->setBusiness($business);
$em = $this->getDoctrine()->getManager();
$em->persist($businessObject);
$em->flush();
Form builder is:
builder
->add('object',TextType::class)
->add('complement')
->add('status')
->add('durationExpected')
->add('durationAchieved')
->add('client')
->add('projectManager'); // No business field
Another option is to embed BusinessType form into BusinessObjectType, you can read more about form embedding here.
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.
when I try to update one of my entites, I get this exception:
UndefinedMethodException: Attempted to call method "bindRequest" on class "Symfony\Component\Form\Form" in /Applications/MAMP/htdocs/Seotool/src/Seotool/MainBundle/Controller/TaskController.php line 64.
edit Action:
/**
#Route(
* path = "/tasks/edit/{id}",
* name = "edit_task"
* )
* #Template()
*/
public function edit_taskAction($id, Request $request)
{
$request = $this->get('request');
if (is_null($id)) {
$postData = $request->get('task');
$id = $postData['id'];
}
$em = $this->getDoctrine()->getManager();
$task = $em->getRepository('SeotoolMainBundle:Task')->find($id);
$form = $this->createForm(new TaskType(), $task);
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
if ($form->isValid()) {
// perform some action, such as save the object to the database
$em->flush();
return $this->redirect($this->generateUrl('taskmanager'));
}
}
return array('form' => $form->createView());
}
What's wrong with my code?
Because there is no method bindRequest. The exception is quite explicit. If you check the official API, I suppose you want to use handleRequest
I create a form type UserBundle/form/UserType.php.
$form->isValid() returns true if the username or the email exist even if other fields are empty.
class UserType extends AbstractType
{
private $type;
const TYPE_CREATE = 1;
public function __construct($type) {
$this->type = $type;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
switch ($this->type){
case self::TYPE_CREATE :
$builder
->add('email', 'email', array('label' => 'user.email'))
->add('username', null, array('label' => 'user.name'))
;
break;
default :
$builder
->add('username')
->add('usernameCanonical')
->add('email')
->add('emailCanonical')
->add('enabled')
->add('salt')
->add('password')
->add('lastLogin')
->add('locked')
->add('expired')
->add('expiresAt')
->add('confirmationToken')
->add('passwordRequestedAt')
->add('roles')
->add('credentialsExpired')
->add('credentialsExpireAt')
;
break;
}
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Project\UserBundle\Entity\User'
));
}
public function getName()
{
return 'project_userbundle_user';
}
}
And my controller
class UserController extends Controller
{
public function CreateAction()
{
//create user
$userManager = $this->get('fos_user.user_manager');
$user = $userManager->createUser();
//create form
$form = $this->createForm(new UserType(UserType::TYPE_CREATE),$user);
//form submit
$request = Request::createFromGlobals();
if($request->getMethod() === "POST"){
$form->submit($request);
//test if form is valid
if($form->isValid()){
//generate random password
$password = User::randomPassword();
$user->setPassword($password);
//save user
$userManager->updateUser($user);
}
}
return $this->render('Project:User:create.html.twig',array(
'form' => $form->createView()
));
}
}
tip:
You shouldn't try to re-create the Request inside a controller. It is definitely a bad practice and can lead to undesired behavior of your application - as you're currently experiencing.
symfony will pass the request automatically to your controller actions if you add it as a method parameter:
public function createAction(Request $request)
{
// $request is holding the current request
}
Further you can get the current request from the container inside a ContainerAware class like a controller.
$request = $this->getRequest();
$request = $this->get('request');
$request = $this->container->get('request');
Further please stick with the symfony coding standards. There are multiple violations in your code.
Method names should be lower camelCased not CamelCased.
UserBundle/form should be UserBundle/Form.
A method's opening curly brackets/braces belong to the next line.
public function __construct($type) // {
{
$this->type = $type;
}
You have to use Validation Constraints NotNull or NotBlank ( http://symfony.com/doc/current/reference/constraints.html ).
My guess is that, default "required=true" on fields isn't working because You are recreating request.
I have this code in two methods (create and update). Each time I need to update or create a new user I need to encode the user password with the salt.
$factory = $this->get('security.encoder_factory');
$encoder = $factory->getEncoder($entity);
$password = $encoder->encodePassword($entity->getPassword(), $entity->getSalt());
$entity->setPassword($password);
To avoid code duplication what should I do?
Create a new method in controller getEncondedPassword($entity) : return $encodedPassword
Add this logic to the Form using DI injecting the $encoder as required field
Add this logic to model, and pass the $encoder in the constructor of the entity object.
Thank you!
If your create and edit are fairly simple and pretty much the same, you can combine it to one function which actually generates and validates the form.
Some code:
class ProductController extends Controller
{
/**
* #Route("/create", name="_product_create")
*/
public function createAction()
{
$product = new Product();
return $this->productForm($product, $this->getRequest(), 'create');
}
/**
* #Route("/edit/{product_id}", name="_product_edit_id")
*/
public function editIdAction($product_id)
{
$entity_manager = $this->getDoctrine()->getEntityManager();
$product_repository = $entity_manager->getRepository('VendorBundle:Product');
$product = $product_repository->findOneBy(
array('id' => $product_id)
);
return $this->productForm($product, $this->getRequest(), 'editId');
}
protected function productForm(Product $product, Request $request, $twig_name)
{
$form = $this->createForm(new ProductType(), $product);
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
if ($form->isValid()) {
// Do whatever we want before persisting/flushing
return $this->redirect($redirect_url);
}
}
$twig_params = array(
);
return $this->render(
'VendorBundle:Product:' . $twig_name . '.html.twig', $twig_params
);
}
}
this will render create.html.twig and editId.html.twig depending on the route.
if $product->getId() === null we are creating a new entity, else we are editing.
I think that the correct option is the model/entity approach.
So, I leave here the my solution:
public function hashPassword($container)
{
$factory = $container->get('security.encoder_factory');
$encoder = $factory->getEncoder($this);
$password = $encoder->encodePassword($this->getPassword(), $this->getSalt());
return $password;
}
In the controller:
//hash user password
$userEntity->setPassword($userEntity->hashPassword($this->container));
Right now I have improved(I at least think...) the answer to this question.
I have created an class that will receive the $encoderFactory form the DI
#services.yml
parameters:
password_encoder.class: Beubi\SignatureBundle\Handler\PasswordEncoder
services:
password_encoder:
class: %password_encoder.class%
arguments: [#security.encoder_factory]
So, I create a class that will be used in Service container:
class PasswordEncoder
{
protected $encoderFactory;
public function __construct(EncoderFactory $encoderFactory)
{
$this->encoderFactory = $encoderFactory;
}
public function encodePassword($entity){
$encoder = $this->encoderFactory->getEncoder($entity);
return $encoder->encodePassword($entity->getPassword(), $entity->getSalt());
}
}
And then in my controller:
$password = $this->get('password_encoder')->encodePassword($entity);
$entity->setPassword($password);
This way, my User object doesn't have any knowledge of $factoryEncoder or how to encode an password.
I'm expecting more comments on this question...