Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Handling data with a Form

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

So what's different between this API controller and one that handles an HTML form submit? Really, not much. The biggest difference is that an HTML form sends us POST parameters and an API sends us a JSON string. But once we decode the JSON, both give us an array of submitted data. Then, everything is the same: create a Programmer object and update it with the submitted data. And you know who does this kind of work really well? Bernhard Schussek err Symfony forms!

Create a new directory called Form/ and inside of that, a new class called ProgrammerType. I'll quickly make this into a form type by extending AbstractType and implementing the getName() method - just return, how about, programmer.

Now, override the two methods we really care about - setDefaultOptions() and buildForm():

<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ProgrammerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
... lines 13 - 29
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
... lines 34 - 36
}
public function getName()
{
return 'programmer';
}
}

In Symfony 2.7, setDefaultOptions() is called configureOptions() - so adjust that if you need to.

In setDefaultOptions, the one thing we want to do is $resolver->setDefaults() and make sure the data_class is set so this form will definitely give us an AppBundle\Entity\Programmer object:

... lines 1 - 31
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Programmer'
));
}
... lines 38 - 43

Building the Form

In build form, let's see here, let's build the form! Just like normal use $builder->add() - the first field is nickname and set it to a text type. The second field is avatarNumber. In this case, the value will be a number from 1 to 6. So we could use the number type. But instead, use choice. For the choices option, I'll paste in an array that goes from 1 to 6:

... lines 1 - 10
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('nickname', 'text')
->add('avatarNumber', 'choice', [
'choices' => [
// the key is the value that will be set
// the value/label isn't shown in an API, and could
// be set to anything
1 => 'Girl (green)',
2 => 'Boy',
3 => 'Cat',
4 => 'Boy with Hat',
5 => 'Happy Robot',
6 => 'Girl (purple)',
]
])
... line 28
;
}
... lines 31 - 43

Using the choice Type in an API

Why choice instead of number or text? Because it has built-in validation. If the client acts a fool and sends something other than 1 through 6, validation will fail.

TIP To control this message, set the invalid_message option on the field.

For the API, we only care about the keys in that array: 1-6. The labels, like "Girl (green)", "Boy" and "Cat" are meaningless. For a web form, they'd show up as the text in the drop-down. But in an API, they do nothing and could be set to anything.

Finish with an easy field: tagLine and make it a textarea, which for an API, does the exact same thing as a text type:

... lines 1 - 10
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('nickname', 'text')
->add('avatarNumber', 'choice', [
'choices' => [
// the key is the value that will be set
// the value/label isn't shown in an API, and could
// be set to anything
1 => 'Girl (green)',
2 => 'Boy',
3 => 'Cat',
4 => 'Boy with Hat',
5 => 'Happy Robot',
6 => 'Girl (purple)',
]
])
->add('tagLine', 'textarea')
;
}
... lines 31 - 43

So, there's our form. Can you tell this form is being used in an API? Nope! So yes, you can re-use forms for your API and web interface. Sharing is caring!

Using the Form

Back in the controller, let's use it! $form = $this->createForm() passing it a new ProgrammerType and the $programmer object. And now that the form is handling $data for us, get rid of the Programmer constructor arguments - they're optional anyways. Oh, and remove the setTagLine stuff, the form will do that for us too:

... lines 1 - 14
/**
* @Route("/api/programmers")
* @Method("POST")
*/
public function newAction(Request $request)
{
$data = json_decode($request->getContent(), true);
$programmer = new Programmer();
$form = $this->createForm(new ProgrammerType(), $programmer);
... lines 25 - 26
$programmer->setUser($this->findUserByUsername('weaverryan'));
$em = $this->getDoctrine()->getManager();
... lines 30 - 33
}
... lines 35 - 36

Normally, this is when we'd call $form->handleRequest(). But instead, call $form->submit() and pass it the array of $data:

... lines 1 - 18
public function newAction(Request $request)
{
$data = json_decode($request->getContent(), true);
$programmer = new Programmer();
$form = $this->createForm(new ProgrammerType(), $programmer);
$form->submit($data);
$programmer->setUser($this->findUserByUsername('weaverryan'));
$em = $this->getDoctrine()->getManager();
... lines 30 - 33
}
... lines 35 - 36

Ok, this is really cool because it turns out that when we call $form->handleRequest(), all it does is finds the form's POST parameters array and then passes that to $form->submit(). With $form->submit(), you're doing the same thing as normal, but working more directly with the form.

And that's all the code you need! So let's try it:

php testing.php

Yep! The server seems confident that still worked. That's all I need to hear!

Creating a Resource? 201 Status Code

On this create endpoint, there are 2 more things we need to do. First, whenever you create a resource, the status code should be 201:

... lines 1 - 14
/**
* @Route("/api/programmers")
* @Method("POST")
*/
public function newAction(Request $request)
{
... lines 21 - 32
return new Response('It worked. Believe me - I\'m an API', 201);
}
... lines 35 - 36

That's our first non-200 status code and we'll see more as we go. Try that:

php testing.php

Cool - the 201 status code is hiding up top.

Creating a Resource? Location Header

Second, when you create a resource, best-practices say that you should set a Location header on the response. Set the new Response line to a $response variable and then add the header with $response->headers->set(). The value should be the URL to the new resource... buuuut we don't have an endpoint to view one Programmer yet, so let's fake it:

... lines 1 - 18
public function newAction(Request $request)
{
... lines 21 - 32
$response = new Response('It worked. Believe me - I\'m an API', 201);
$response->headers->set('Location', '/some/programmer/url');
return $response;
}
... lines 38 - 39

We'll fix it soon, I promise! Don't forget to return the $response.

Try it once more:

php testing.php

Just like butter, we're on a roll!

Leave a comment!

18
Login or Register to join the conversation
Default user avatar
Default user avatar Michael Sypes | posted 5 years ago

Something that's clear as mud is WHY I would want to include all this form business in the API controller. I have two guesses:
1) You get built-in validation of submitted data, since Symfony conflates an entity with a form
2) You can use this to update an existing programmer as well as create a new one by creating the form with an entity pulled from the database, rather than a blank one.

What's lacking is information on what calling $form->submit($data) does that's useful. I'm hoping this becomes clear in the next section or two.

1 Reply

Hey Michael!

Actually, good question - I may have over-assumed some points. Some thoughts:

A) you can get validation without using forms. If you have an object (entity or otherwise), you can pass it directly to the validator service and get back a list of validation errors (which you could then display just in an API just as easily as with a form).

B) The form is really good at basically calling your set* functions. So the very first thing you get is the ability to avoid manually parsing through the request body and calling setName(), setDescription(), setPrice(), etc etc. But if you didn't use forms - as long as you centralized that "setting" logic somewhere, you could use it to create a new entity or update an entity.

C) The form has built-in data transformers. So, if the client sends a Y-m-d date, then your "date" field will convert this to a DateTime object before setting it on your object. Or, if the client sends some foreign key id value (e.g. imagine Product has a category_id in the database, and 5 is sent as the categoryId value in an API), the "entity" form type would convert that to the entity object (e.g. Category).

D) One of the most important things - that I didn't mention - is that eventually we'll use NelmioApiDocBundle to get some really slick API documentation. It's able to automatically read forms - meaning that it'll know exactly what input your endpoint accepts just by looking at the form that the endpoint uses. That's a big reason - but it won't show up until we talk about documentation.

P.S. I *am* a proponent, however, that if using a form gets too tough or confusing for some reason, feel more than free to back up and just manually parse through the data yourself. That's no big deal.

Cheers!

1 Reply
Default user avatar
Default user avatar Michael Sypes | weaverryan | posted 5 years ago

So, then, is calling $form->submit($data) sufficient to have done all this work for us automatically? I.e., has it set all the $programmer properties for us behind the scenes, along with any data transformations? And am I correct in assuming that a call to $form->isValid() could be used here?

Reply

You got it :). submit() calls the setters (after the normal data transformations of the form). And yes, you'd absolutely be able to do a $form->isValid() - it's something we'll do in the next episode (recording now) to return a nice response with validation errors.

2 Reply
Default user avatar

Hi Ryan

I'm just curious what is your opinion here? Do you think it would be better if that data binding logic was separated from the forms?

Regards,
Rob

1 Reply

Yo Robert!

Hmm, yea! Really, the *only* reason I'm using the form system here is for this "data binding logic": the fact that it will take the data from the request (pretty minor), run the data transformers over each piece of data to convert it to something else when necessary (major) and then set this data on the objects (minor). In a perfect world, there would be a smaller system to help us with this. At some point, I'm going to look again at using the deserializer for this... which historically I found too inflexible (but it's being used successfully in ApiPlatform, I'm told).

Let me know if that answers your question :).

Cheers!

Reply
Default user avatar

Hi Ryan,

Sorry for the delay. Yes this does answer my question. Thank you very much. Now I know what should I do :)

Regards,
Rob

Reply
Andranik H. Avatar
Andranik H. Avatar Andranik H. | posted 4 years ago

Hi Ryan.
Suppose I have a Profile entity which has links field with json type. I want to create and Rest Api which will create that entity. But I found these lines of code which throw an exception in this case.
Form.php:538
.....
elseif (\is_array($submittedData) && !$this->config->getCompound() && !$this->config->hasOption('multiple')) {
$submittedData = null;
$this->transformationFailure = new TransformationFailedException('Submitted data was expected to be text or number, array given.');
}

Reply

Hey Andranik H.

I think what you need is a DataTransformer, so you can transform the submitted data before validating it.
You can learn more about DataTransformers here: https://symfonycasts.com/sc...
Or at the docs: https://symfony.com/doc/cur...

Cheers!

Reply
Danilo D. Avatar
Danilo D. Avatar Danilo D. | posted 5 years ago

Hello, I have a problem with the api I'm implementing. There is an entity called Report that has foreign key of the entity City (Many-To-One). I can not save the Report object. Help me please

Reply
Osagie Avatar

Hey Danilo,

So your Report entity relates to the City one as ManyToOne, right? It sounds like you need to set a City on your Report entity before saving it. Try to set a City first and only then call flush().

Cheers!

Reply
Default user avatar
Default user avatar Sylvain Cyr | posted 5 years ago

in Symfony 3.0.6, $form->submit doesn't seem to validate a collection of forms.
Let's say you have an order which has some information and also an array of lines (BlanketOrderLine object).
BlanketOrderLineType is the form type for BlanketOrderLine .
Parent form is validated but not the BlanketOrderLineType ....

$builder
->add('lines', CollectionType::class,

array('entry_type' => BlanketOrderLineType::class,
'label' => false,
'allow_add' => true,
'allow_delete' => true,
'prototype' => false,
'required' => true,
'by_reference' => false,
'entry_options' => array('constraints' => [new Assert\Valid()])
));

Any idea why?

Reply
Default user avatar

Forget about that. I upgraded from symfony 2.7 to 3. setDefaultOptions is gone and need to use the configureOptions.

Reply

Hey, Sylvain Cyr !

You are totally right! The setDefaultOptions() method was removed in favor of configureOptions() in Symfony 3. The best way is to upgrade to the Symfony 2.8 first, fix all deprecations and then move up to the 3.x.

Cheers!

-1 Reply
Default user avatar
Default user avatar Nikola Dimitrijevic | posted 5 years ago

Hi, im using Symfony 3.2, and have a little bit of a problem on inserting the json data into forms. I have my Note entity, that i need to validate using forms, but i keep getting the error "Cannot use object of type AppBundle\Entity\Note as array". All im doing is creating a named builder form, and submiting with json object that i previously decoded, so it's array now. This is the code:
$note = new Note();
$form = $this->get('form.factory')->createNamedBuilder('noteForm', FormType::class, $note)
->add('type', TextType::class, [
'required' => true
])
->add('title', TextType::class, [
'required' => true
])
->add('content', TextType::class, [
'required' => true
])
->getForm();
$form->submit($data);
if($form->isValid()){
...
}
I also tried with the regular formBuilder, but i keep getting the same error. And yes, im sending data as a json object: {noteForm: {type: "note", title: "something", content: "something"}}
Can you please help me?
Thanks in advace!

Reply

Hey Nikola Dimitrijevic

Have you tried creating your own FormType for your Note class ? Just as we do in ProgrammerType

Cheers!

Reply
Default user avatar
Default user avatar Nikola Dimitrijevic | MolloKhan | posted 5 years ago

Yes, i did. But i worked it out in the end. So, there was a piece of code inside of $form->isValid(), and that piece of code was the following: $formData = $form->getData();
$note->setType($formData['type']);
... And the error was "Cannot use object of type AppBundle\Entity\Note as array". The thing i was doing was exactly that i was treating $formData as an array, when in reality, $formData was Note object. Rookie mistake, but at least, now i know for sure how does forms work! :)
Cheers and thanks for the answer!

Reply

I'm glad to hear you could fix the problem by yourself, keep going!

Have a nice day :)

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony. The concepts of REST are still valid, but I recommend using API Platform in new Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.6.*", // v2.6.11
        "doctrine/orm": "~2.2,>=2.2.3,<2.5", // v2.4.7
        "doctrine/dbal": "<2.5", // v2.4.4
        "doctrine/doctrine-bundle": "~1.2", // v1.4.0
        "twig/extensions": "~1.0", // v1.2.0
        "symfony/assetic-bundle": "~2.3", // v2.6.1
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.8
        "symfony/monolog-bundle": "~2.4", // v2.7.1
        "sensio/distribution-bundle": "~3.0,>=3.0.12", // v3.0.21
        "sensio/framework-extra-bundle": "~3.0,>=3.0.2", // v3.0.7
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "hautelook/alice-bundle": "0.2.*", // 0.2
        "jms/serializer-bundle": "0.13.*" // 0.13.0
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3", // v2.5.3
        "behat/behat": "~3.0", // v3.0.15
        "behat/mink-extension": "~2.0.1", // v2.0.1
        "behat/mink-goutte-driver": "~1.1.0", // v1.1.0
        "behat/mink-selenium2-driver": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~4.6.0" // 4.6.4
    }
}
userVoice