Extra field in form when changing HTTP method in Symfony / FOSRestBundle - php

I am building a REST API in Symfony using FOSRestBundle and I've stumbled upon a problem which seems to be trivial, but I can't find a nice solution.
I want to create a controller method to handle PATCH requests:
/**
* #param Article $article
* #param Request $request
*
* #Patch("/articles/{slug}")
* #ParamConverter("article", converter="doctrine.orm")
*
* #return Response
*/
public function patchAction(Article $article, Request $request)
{
$form = $this->createForm(new ArticleType(), $article);
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager = $this->get('doctrine.orm.entity_manager');
$entityManager->merge($article);
$entityManager->flush();
$view = $this->view($article)
->setTemplate('MyBundle::articleSubmit.html.twig')
->setTemplateVar('article')
;
return $this->handleView($view);
}
$view = $this->view($form)
->setTemplate('MyBundle::articleForm.html.twig')
->setTemplateVar('form')
->setTemplateData(array('article' => $article))
;
return $this->handleView($view);
}
I've configured form type like this, setting method option to PATCH:
namespace MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('content', 'textarea');
}
public function getName()
{
return '';
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MyBundle\Entity\Article',
'method' => 'PATCH',
));
}
}
When I want to see this working, everything goes fine as long as I make a real PATCH request (for example using Open HttpRequester). But as most browsers support only GET and POST method, Symfony makes a trick adding _method parameter to regular POST request, and that's where my problem begins, because when testing this in browser I get the message:
This form should not contain extra fields.
This is obviously because ArticleType::getName() returns empty string, but I would like my request parameters look more like content=foo than like article[content]=foo, as it's supposed in REST API. That's why I want this method to return blank value.
This works normally either when I change the action to POST, or when I set the form's name, or when I make a real PATCH request, but not when I'm sending a POST request with _method parameter.
I've managed to solve this problem creating a body listener that removes fields having names prefixed by _, but that's more a dirty hack than a nice and clean solution.
I'm sure there must be some better workaround, thank you in advance for any help.

Related

set valid constraint for embedded entity only if flag in embedded object property is set

I am using symfony3.4.
Now i have a form presented by a data-class which uses several child entities as property:
contact {
protected $address;
protected $user;
protected $message;
}
The address entity includes a field "address->validate" which can be set by the user in the frontend. The requirement now is, that address only should be validated, if the user checks the field "address->validatable".
address {
private $street;
//[...]
private $validatable; //bool
public function isValidatable(){...}
}
Normally, I would use a callback-constraint, to check the field. But since the callback function is static, all the examples only show callbacks with one single field-property and ExecutionContextInterface does not allow me, to add constraints afterwards, I don't know how to implement it.
Can anybody tell me how to solve this case?
EDIT:
Thanks to Marc I solved it this way:
I have a form-type-class called "ContactGeneralType". In this class I add the group like described on https://symfony.com/doc/3.4/form/data_based_validation.html:
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults($this->addDefaults([
'data_class' => ContactGeneral::class,
'cascade_validation' => true,
]));
$resolver->setDefaults([
'validation_groups' => function (FormInterface $form) {
$data = $form->getData();
if ($data->getAddress()->isValidatable()) {
return ['User', 'Address'];
} else {
return ['User'];
}
},
]);
}
That does all the trick. If you have more address-validation-groups (e.g. if you validate different countries), you already can inject your service in the form-type-class and set the group-name dynamically, too:
if ($data->getAddress()->isValidatable()) {
return ['User', $this->locationManager>getGroupName()];
}
Instead of using callback, try creating custom validator that is explained here: https://symfony.com/doc/current/validation/custom_constraint.html. You get your Object inside of validator and you can check if field is checked or w/e and validate based on that.
EDIT:
Maybe changing the validation group on form submit is better solution as described here https://symfony.com/doc/current/form/data_based_validation.html

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.

How to pass custom options to a Symfony form

I've to pass a custom option to a symfony form. I followed the documentation setp by step but my options will not pass.
The FormType
class AdvertType extends AbstractType
{
/**
* {#inheritDoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$entityManager = $options['em'];
}
/**
* {#inheritDoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setRequired(array(
'em',
));
$resolver->setAllowedTypes(array(
'em' => 'Doctrine\Common\Persistence\ObjectManager',
));
}
/**
* {#inheritDoc}
*/
public function getName()
{
return 'advert';
}
}
Here is the Code from the controller.
$form = $this->createForm(new AdvertType(), new Advert(), array(
'em' => $this->getDoctrine()->getManager(),
));
Symfony will throw an exception that my option em is missing.
Exception: The required option "em" is missing.
I followed the tutorial to add a data transformers: http://symfony.com/doc/current/cookbook/form/data_transformers.html
I cleared my cache and restarted my web server but nothing will work. What did I wrong? Did I missed a configuration to pass my $options? It looks like the $options array from the controller I passed will never reach the buildForm method.
I'm using Symfony v2.3.5. For testing I updated to the latest (2.3.6) but the problem still exists.
Cheers.
I've copied your code and used it successfully (Symfony 2.3.6). It worked! You shouldn't have to clear your cache. So I'm not sure what's wrong. You should also consider adding the data_class option in your resolver if you want to constrain the form to your Advert object, e.g.
$resolver
->setDefaults(array(
'data_class' => 'Your\Bundle\Entity\Advert',
))
;
I used my form twice (in different ways) so the error results from the wrong usage. I looked on the wrong place for the error. The code in general is correct. Sorry for that.

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.

Custom form type symfony

I need to set up a custom form type in Symfony that uses the choice type as a parent but doesn't actually require choices to be preloaded. As in I want to be able to populate the select with an ajax call and then submit with one of the options from the call without getting This value is not valid. errors, presumably because its not one of the preloaded options.
I don't need a custom data transformer as I am doing that through the bundle controller, I just need Symfony not to complain when I submit with an option that wasn't originally on the list. Here is what my custom form type looks like so far:
<?php
namespace ISFP\Index\IndexBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class NullEntityType extends AbstractType
{
public function getDefaultOptions(array $options)
{
$defaultOptions = array(
'em' => null,
'class' => null,
'property' => null,
);
$options = array_replace($defaultOptions, $options);
return $options;
}
public function getParent()
{
return 'choice';
}
public function getName()
{
return 'null_entity';
}
}
Dude look at the EntityType it has a parent as a choice. But entire display was handle by ChoiceType. When I was doing similar things I've started from overload Both ChoiceType and EntityType. And then set in overloaded Entity the getParent() to mine overloaded choice.
Finally In my case I modify the new choice and put there my embedded form. It's tricky to do It. And it consumes lot's of time.
But with that approach i don't have any problem with Validation.

Categories