Deserialize entity with null id and doctrine constructor - php

I'm having many problems when I receive an entity to deserialize with null id.
My JMS Serializer configuration file is:
jms_serializer:
handlers:
datetime:
default_format: Y-m-d\TH:i:s+
array_collection:
initialize_excluded: false
property_naming:
id: jms_serializer.identical_property_naming_strategy
separator: ''
lower_case: false
enable_cache: true
object_constructors:
doctrine:
fallback_strategy: "fallback"
So, when the entity has an id field, it tries to retrieve the entity with that id from database via doctrine (something like $repository->find($id)).
If the Id exists, it retrieves the entity filled. If it doesn't exist return an exception. The problem is that when I receive a JSON with an entity to persist, the id field is null and it tries to find an entity in the database with ìd==null, so it throws an exception.
I have tried to change the fallback_strategy to: "null","exception" or "fallback" without success.
Edit: the Controller where it happens
protected function post(Request $request)
{
$content = $request->getContent();
try {
$entity = $this->get('jms_serializer')->deserialize($content, 'App\Entity\Service', 'json');
} catch (\Exception $e){
throw new ValidationException($e);
}
....
}
I put a try-catch block to capture and log the exception with a custom class.
I hope you can help me,
Thanks

It's more a workaround but for my CRUD controllers I prefer to use form to deserialize my objects. It allows me to be more flexible, I can have different form of payload for the same entity and I can check in a better way my payload.
The common form looks like this (you have to adapt it to your entity)
class YourEntityType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('description')
...
}
}
And in the controller :
protected function post(Request $request)
{
$yourEntity = new YourEntity();
$form = $this->createForm(YourEntityType::class, $yourEntity);
$form->submit($request->request->all());
if ($form->isSubmitted() && $form->isValid()) {
$em->persist($yourEntity);
$em->flush();
return $yourEntity;
}
return $form;
}
Don't know if it's gonna fit with your project but that the cleanest way I know and it bypass you id issue because you don't have to put it in your form.

Related

Symfony form unchanged field returns validation type error

Trying to update an entity, and submitting a field with a value that is unchanged results in a type error. What am I doing wrong?
Entity:
<?php
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
...
class User implements UserInterface
{
...
/**
* #ORM\Column(type="bigint", nullable=true)
* #Groups({"default", "listing"})
* #Assert\Type("integer")
*/
private $recordQuota;
...
FormType:
<?php
namespace App\Form;
...
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
...
->add('recordQuota', IntegerType::class)
;
}
...
}
Controller:
...
/**
* #Route("/api/user/{id}", name="editUser")
* #Method({"PUT", "PATCH"})
* #Rest\View()
*/
public function updateAction(Request $request, User $user)
{
$form = $this->createForm(UserType::class, $user);
$data = $request->request->get('user');
$clearMissing = $request->getMethod() != 'PATCH';
$form->submit($data, $clearMissing);
if ($form->isSubmitted() && $form->isValid()) {
...
I'm using PostMan to submit form data.
If the entity I am updating has a recordQuota of 1000, and I submit the form with a different value. It all works and updates.
But if I submit my form with recordQuota: 1000, which should leave the value unchanged I get an incorrect type error:
"recordQuota": {
"errors": [
"This value should be of type integer."
]
}
Additional info:
I am using $form->submit instead of handleRequest because I am using patch. So I need to be able to enable/disable $clearMissing. But even using handleRequest creates the same issue.
Even typecasting the recordQuota as int before passing it to the form still fails.
If I remove all of the type information from the Form and the Entity, I get "This value should be of type string" when actually making a change.
Edit: note that the following is true if field type is TextType, but IntegerType works fine with #Assert\Type("integer"). Which kinda renders my answer invalid/irrelevant...
You're using #Assert\Type("integer") annotation, but it means this:
value must be integer -- as a PHP type, like calling is_int($value)
and since data comes from form (and probably without any transformers, as I see in your code), it's type is string
and thus, validation always fails
What you need is #Assert\Type("numeric"):
it is equivalent of is_numeric($value)
down the line it will be converted to string when it reaches field of your entity
This was an issue with a combination of Symfony 4.3 validator auto_mapping described here:
https://symfony.com/blog/new-in-symfony-4-3-automatic-validation
And the maker bundle adding the wrong typecast to bigint fields.
See here:
https://github.com/symfony/maker-bundle/issues/429
The answer was to change the getters and setters in the entity from:
public function getRecordQuota(): ?int
{
return $this->recordQuota;
}
public function setRecordQuota(?int $recordQuota): self
{
$this->recordQuota = $recordQuota;
return $this;
}
to
public function getRecordQuota(): ?string
{
return $this->recordQuota;
}
public function setRecordQuota(?string $recordQuota): self
{
$this->recordQuota = $recordQuota;
return $this;
}
Alternatively, one can turn off auto_mapping in the validator config.

Symfony form state backup to database and restore / or: how to prepopulate a form from database

I have a big form with time, date, select and EntityType fields in it. The form is not linked to an entity, but it do contain fields from other entities. In other words I have no data_class in the OptionsResolver of the FormType.
Here is my formtype: (showing just one field for simplicity)
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('company', EntityType::class, array(
'placeholder' => 'Companies',
'class' => 'AppBundle:Company',
'choice_label' => 'name',
'multiple' => true,
'query_builder' => function (EntityRepository $repository) {
return $repository->createQueryBuilder('c')->orderBy('c.name', 'ASC');
},
'required' => false
))
//... much more fields
}
public function configureOptions(OptionsResolver $resolver)
{
// This form is not linked to an Entity
// Therefore no `data_class`
}
In the controller I can backup the data of a form. Called a FormState. I save a FormState to the database as follows:
namespace AppBundle\Controller;
class ReportController extends Controller
{
/** // ... */
public function listAction(Request $request)
{
$form = $this->createForm(ReportType::class);
$form->handleRequest($request);
// Save the form to a FormState
if ($form->isSubmitted() && $form->getClickedButton()->getName() == 'saveFormState') {
$formStateManager = $this->get('app.manager.form_state');
// Get form data that we want to save
$data = $form->getData();
$name = 'Stackoverflow example'; // give it a name
$formState = $formStateManager->createNewFormState(); // Create new FormState Entity object
$formState->setName( $name );
$formState->setData( $data );
$formStateManager->save( $formState );
}
// ...
}
}
All this above works perfect. But now the tricky part, I have to set the backupdata back to the form. The user can select a previous form state from a list of FormStates. And then hit the load button.
The data attribute of a FormState object I try to load into the form is just a $form->getData() result. Thus a normal array without objects. Maybe that is the problem, but I can't get it to work whatsoever.
That data array is what I trying to load into the form so it takes its values over. I tried it via $form->setData(), or by creating a new form with $this->createForm(ReportType::class, $data). Both fail with the same error message: Entities passed to the choice field must be managed. Maybe persist them in the entity manager?.
I have tried two ways of adding the data:
First try, in controller:
namespace AppBundle\Controller;
class ReportController extends Controller
{
/** // ... */
public function listAction(Request $request)
{
if ($form->isSubmitted() && $form->getClickedButton()->getName() == 'loadFormState') {
// ...
$form = $this->createForm(ReportType::class, $formState->getData()); // <-- throws the error
$form->submit($formState->getData());
}
}
}
Second try, via FormEvent subscriber:
When I do it via a FormEvent subscriber, like below, I get the same error. Here is my code:
namespace AppBundle\Form\EventListener;
class ReportFieldsSubscriber implements EventSubscriberInterface
{
// ...
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SUBMIT => array(
array('loadFormState'),
),
);
}
/**
* Load selected FormState in form
* #param FormEvent $event
*/
public function loadFormState(FormEvent $event)
{
$form = $event->getForm();
//...
// Choosen FormState
$formState = $formStateManager->findOneById($formStateId);
$formStateData = $formState->getData();
$form->setData($formStateData); // <-- Set data, trows error
// ...
}
}
As I said both solutions throwing the error: Entities passed to the choice field must be managed. Maybe persist them in the entity manager?
What is the preferred solution in this case? I can create a dummy Entity, and fix this problem, but it feels a bit ugly and not the way to go.
After days of fiddling I found the solution.
My struggles where not how to save form data, as I showed in the question. However, I made a mistake by saving view data to the database and trying to load that later. Have a good read here for what I mean: https://symfony.com/doc/current/form/events.html#registering-event-listeners-or-event-subscribers
So, I also rewrite the saving of a form state to be in the form subscriber. Note: remember my form has no data_class in the OptionsResolver settings of the FormType. So there is no auto datamapping and such.
In my initial question, you can see I have multiple submit buttons, and in the controller you can check which one was clicked easily. Because in the controller you have access to the getClickedButton() function. But not in the form subscriber. So I found a way to get the clicked button in the EventSubscriberInterface of the form:
class ReportFieldsSubscriber implements EventSubscriberInterface
{
/**
* Find which button was clicked
* #param $data
* #return string
*/
public function getClickedButton($data)
{
$requestParams = serialize($data);
if (strpos($requestParams, 'saveFormState') !== false) {
return 'saveFormState';
}
if (strpos($requestParams, 'loadFormState') !== false) {
return 'loadFormState';
}
if (strpos($requestParams, 'deleteFormState') !== false) {
return 'deleteFormState';
}
return "";
}
}
Below you find the actual solution how I saved and loadedthe form the right way. I hook them up in the FormEvent::PRE_SUBMIT stage. At that stage you can save and load Request data into the FormEvent. Perfect.
Saving and loading/prepopulating the form data via FormEvent::PRE_SUBMIT:
class ReportFieldsSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SUBMIT => array(
array('saveFormState'),
array('loadFormState'),
),
);
}
/**
* Save form data
* #param FormEvent $event
*/
public function saveFormState(FormEvent $event)
{
$data = $event->getData();
if ($this->getClickedButton($data) == 'saveFormState') {
$formStateManager = $this->formStateManager;
// ...
$formState = $formStateManager->createNewFormState();
// ...
$formState->setData($data);
$formStateManager->save($formState);
}
}
/**
* Load choosen form data
* #param FormEvent $event
*/
public function loadFormState(FormEvent $event)
{
$data = $event->getData();
if ($this->getClickedButton($data) == 'loadFormState') {
$formStateId = $data['formState']['formStates'];
if (is_numeric($formStateId)) {
$formStateManager = $this->formStateManager;
$formState = $formStateManager->findOneById(intval($formStateId));
// Choosen FormState data
$formStateData = $formState->getData();
$event->setData($formStateData); // <-- Call setData() on the FormEvent, not on the form itself
}
}
}
}
I am pretty sure this solution will save someone days of coding!

How to properly do a REST API POST call using FOSRest and Symfony 3.0

For an API I'm currently building I'd like to be able to send a request with a JSON body with the following content
{"title": "foo"}
to create a new database record for an Entity called Project.
I made a controller which subclasses FOSRestController. To create a project, I made an action
/**
* #Route("/")
*
* #ApiDoc(
* section="Project",
* resource=true,
* input={"class"="AppBundle\Form\API\ProjectType"},
* description="Creates a new project",
* statusCodes={
* 201="Returned when successful",
* }
* )
*
* #Method("POST")
* #Rest\View(statusCode=201)
*/
public function createProjectAction(Request $request)
{
$project = new Project();
$form = $this->createForm(ProjectType::class, $project);
$form->submit(($request->request->get($form->getName())));
if ($form->isSubmitted() && $form->isValid()) {
return $project;
}
return View::create($form, 400);
}
The ProjectType looks like this
class ProjectType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('title');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Project'
));
}
}
However, when I try to post said JSON to the API, it responds that the title property cannot be blank, which is good because that's the validation rule set for it. However, it IS set. I suddenly realized I have to send the JSON prefixed by the actual object's name to make this work:
{"project":{"title": "bla"}}
Which feels a little strange to be fair, it should be enough to just post the properties.
So, based on this information I simply have 2 questions:
Why do I need to "submit" this form with ($request->request->get($form->getName())), shouldn't $request be enough?
What do I need to change for the FormType to validate the entity as is, instead of prefixing it with the entity's name?
Edit 1: adding or removing the data_class in the Default Options does not change the behaviour at all.
This is because of how Symfony Controller "createForm" helper method works. Reasoning behind it is that multiple forms could have same target URL. By prefixing with form name, Symfony can know which form was submitted.
This can be seen by looking at "createForm" method implementation:
public function createForm($type, $data = null, array $options = array())
{
return $this->container->get('form.factory')->create($type, $data, $options);
}
If you don't want this behavior, it's really easy to change it:
public function createProjectAction(Request $request)
{
$project = new Project();
$form = $this->get('form.factory')->createNamed(null, new ProjectType(), $project);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
return $project;
}
return View::create($form, 400);
}
So you're basically creating a "nameless" form. Since you're building an API, it's probably a good idea to pull this into a createNamelessForm($type, $data, $options) helper method in your base controller so that you don't have to get Form Factory from container explicitly all the time and make it easier on the eyes.
Comment on your edit
Wrapper key is not generated by "data_class" option, but by "getName()" method on your form type.

Symfony 2 form validation - extending request

Im using FOSRestBundle to build a REST API in Symfony.
In my tax Controller i have:
private function processForm(Request $request, Tax $tax )
{
$form = $this->createForm(new TaxType(),$tax);
$req = $request->request->all();
$form->submit($req['tax']);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($tax);
$em->flush();
return $this->getTaxAction($tax->getId());
}
return array(
'form' => $form,
'request' => $request->request->all()
);
}
This function called from the POST and PUT functions.
TaxType
class TaxType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('createDate')
->add('writeDate')
->add('name')
->add('amount')
->add('active')
->add('createUid')
->add('writeUid')
->add('company')
;
}
...
It worked fine so far, but now i added some extra column to the table (and proprty to the Tax Entity) like: to which company belongs the record, date of creation. This wont come from the client but set on server side.
How do i add eg createData?
I have tried
$req['tax']['createDate'] = new \DateTime("now");
but im getting:
{"code":400,
"message":"Validation Failed",
"errors":{"children":{"createDate":{"errors":["This value is not valid."],
"children":{"date":{"children":{"year":[],"month":[],"day":[]}},
"time":{"children":{"hour":[],"minute":[]}}}},
"writeDate":{"children":{"date":{"children":{"year":[],"month":[],"day":[]}},
"time":{"children":{"hour":[],"minute":[]}}}},"name":[],"amount":[],"active":[],"createUid":[],"writeUid":[],"company":[]}}}
from entity Tax.php
/**
* #var \DateTime
*/
private $createDate;
I guess im extending the request with the correct data type, but im getting validation error.
If I understand the question correctly you don't have to take the request but just change the property on the $tax-object.
You can also do this after the form validation if you want.
You can Remove add('createDate') from your form builder.
$builder
->add('createDate')
->add('writeDate')
->add('name')
->add('amount')
->add('active')
->add('createUid')
->add('writeUid')
->add('company')
Then set the createdDate value on your $tex object before persist.
if ($form->isValid()) {
$tax->setCreateDate(new \DateTime("now"));
$em = $this->getDoctrine()->getManager();
$em->persist($tax);
$em->flush();
return $this->getTaxAction($tax->getId());
}
Alternative Method
Or you can use Doctrine's Lifecycle Callbacks to achieve this.
Happy coding!!

Symfony2 FOSRestBundle PUT Action FORM returns empty results

I am using Symfony 2.2 and the latest version of FOSRestBundle. So I have manage to make most of the actions work but I seem to have an issue with the FormBuilder that I am passing the Request of my PUT call in.
I have checked the request object and it comes from my Backbone.je model as it should (.save()) But after binding to the form the entity comes back with only the id which causes flush() to throw an error since required fields are not filled.
The action in the Controller:
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS ');
header('Allow GET, POST, PUT, DELETE, OPTIONS ');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: Content-Type, *');
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Routing\ClassResourceInterface;
use FOS\Rest\Util\Codes;
use Symfony\Component\HttpFoundation\Request;
use Greenthumbed\ApiBundle\Entity\Container;
use Greenthumbed\ApiBundle\Form\ContainerType;
class ContainerController extends FOSRestController implements ClassResourceInterface
{
/**
* Put action
* #var Request $request
* #var integer $id Id of the entity
* #return View|array
*/
public function putAction(Request $request, $id)
{
$entity = $this->getEntity($id);
$form = $this->createForm(new ContainerType(), $entity);
$form->bind($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($entity);
$em->flush();
return $this->view(null, Codes::HTTP_NO_CONTENT);
}
return array(
'form' => $form,
);
}
/**
* Get entity instance
* #var integer $id Id of the entity
* #return Container
*/
protected function getEntity($id)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('GreenthumbedApiBundle:Container')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Container entity');
}
return $entity;
}
The Form that is called:
namespace Greenthumbed\ApiBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ContainerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('description')
->add('isVisible')
->add('type')
->add('size')
->add('creationDate')
->add('userId')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Greenthumbed\ApiBundle\Entity\Container',
'csrf_protection' => false,
));
}
public function getName()
{
return 'greenthumbed_apibundle_containertype';
}
}
I have tried everything so far but I am fairly new with Symfony and I cannot understand why the $entity does not contain the values received by the request.
FYI: I have tried doing it manually as in instantiating a Container class with the ID of the request and putting use the setters to input values into it and it works just fine, I just want to do things the right way as Symfony suggests it should be done.
Thank you very much in advance.
Try changing the following line in putAction:
$form = $this->createForm(new ContainerType(), $entity);
to:
$form = $this->createForm(new ContainerType(), $entity, array('method' => 'PUT'));
I think you are experiencing the same problem I had:
The mistake is in the name of the Form.
In your Form definition the name is "greenthumbed_apibundle_containertype".
public function getName()
{
return 'greenthumbed_apibundle_containertype';
}
So to bind a request to this form the json should have looked like this:
{"greenthumbed_apibundle_containertype": [{"key": "value"}]}
Since Backbone .save() method ship this kind of json
{"key":"value","key2":"value2"}
you have to remove the name from the Form:
public function getName()
{
return '';
}
In general if you want to post a json with a placeholder like
"something":{"key":"value"}
your form name must be exactly "something"
from my own question here
Use the ParamConverter to have your Entity injected as an argument in your method automatically.
use Greenthumbed\ApiBundle\Entity\Container;
// ...
public function putAction(Request $request, Container $container)
{
$form = $this->createForm(new ContainerType(), $container);
$form->bind($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
// Entity already exists -> no need to persist!
// $em->persist($entity);
$em->flush();
return $this->view(null, Codes::HTTP_NO_CONTENT);
}
return array('form' => $form);
}
see http://symfony.com/doc/current/cookbook/routing/method_parameters.html
Unfortunately, life isn't quite this simple, since most browsers do
not support sending PUT and DELETE requests. Fortunately Symfony2
provides you with a simple way of working around this limitation. By
including a _method parameter in the query string or parameters of an
HTTP request, Symfony2 will use this as the method when matching
routes. Forms automatically include a hidden field for this parameter
if their submission method is not GET or POST. See the related chapter
in the forms documentation for more information.
and http://symfony.com/doc/current/book/forms.html#book-forms-changing-action-and-method
If the form's method is not GET or POST, but PUT, PATCH or DELETE,
Symfony2 will insert a hidden field with the name "_method" that
stores this method. The form will be submitted in a normal POST
request, but Symfony2's router is capable of detecting the "_method"
parameter and will interpret the request as PUT, PATCH or DELETE
request. Read the cookbook chapter "How to use HTTP Methods beyond GET
and POST in Routes" for more information.

Categories