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

Saving Related Resources in 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.

In the Controller/Api directory, create a new BattleController. Make it extend the same BaseController as before: we've put a lot of shortcuts in this:

... lines 1 - 2
namespace AppBundle\Controller\Api;
use AppBundle\Controller\BaseController;
... lines 6 - 8
class BattleController extends BaseController
{
... lines 11 - 17
}

Then, add public function newAction(). Set the route above it with @Route - make sure you hit tab to autocomplete this: it adds the necessary use statement. Finish the URL: /api/battles. Do the same thing with @Method to restrict this to POST:

... lines 1 - 5
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class BattleController extends BaseController
{
/**
* @Route("/api/battles")
* @Method("POST")
*/
public function newAction()
{
}
}

Awesome! Our API processes input through a form - you can see that in ProgrammerController:

... lines 1 - 22
class ProgrammerController extends BaseController
{
/**
* @Route("/api/programmers")
* @Method("POST")
*/
public function newAction(Request $request)
{
$programmer = new Programmer();
$form = $this->createForm(ProgrammerType::class, $programmer);
$this->processForm($request, $form);
... lines 34 - 52
}
... lines 54 - 149
}

This form modifies the Programmer entity directly and we save it. Simple.

BattleManager Complicates Things...

Well, not so simple in this case. What? I know, I like to make things as difficult as possible.

To create battles on the frontend, our controller uses a service class called BattleManager. It's kind of nice: it has a battle() function:

... lines 1 - 9
class BattleManager
{
... lines 12 - 18
/**
* Creates and wages an epic battle
*
* @param Programmer $programmer
* @param Project $project
* @return Battle
*/
public function battle(Programmer $programmer, Project $project)
{
... lines 28 - 53
}
}

We pass it a Programmer and Project and it takes care of all of the logic for creating a Battle, figuring out who won, and saving it to the database. We even gave Battle a __construct() function with two required arguments:

... lines 1 - 10
class Battle
{
... lines 13 - 46
/**
* Battle constructor.
* @param $programmer
* @param $project
*/
public function __construct(Programmer $programmer, Project $project)
{
$this->programmer = $programmer;
$this->project = $project;
$this->foughtAt = new \DateTime();
}
... lines 58 - 105
}

This is a really nice setup, so I don't want to change it. But, it doesn't work well with the form system: it prefers to instantiate the object and use setter functions.

Tip

Actually, it is possible to use the form system with the Battle entity by taking advantage of data mappers.

But that's ok! In fact, I like this complication: it shows off a very nice workaround. Just create a new model class for the form. In fact, I recommend this whenever you have a form that stops looking like or working nicely with your entity class.

Adding the BattleModel

In the Form directory, create a Model directory to keep things organized. Inside, add a new class called BattleModel:

... lines 1 - 2
namespace AppBundle\Form\Model;
... lines 4 - 7
class BattleModel
{
... lines 10 - 32
}

Give it the two properties we're expecting as API input: $project and $programmer. Hit command+N - or go to the "Code"->"Generate" menu - and generate the getter and setter methods for both properties:

... lines 1 - 4
use AppBundle\Entity\Programmer;
use AppBundle\Entity\Project;
class BattleModel
{
private $project;
private $programmer;
public function getProject()
{
return $this->project;
}
public function setProject(Project $project)
{
$this->project = $project;
}
public function getProgrammer()
{
return $this->programmer;
}
public function setProgrammer(Programmer $programmer)
{
$this->programmer = $programmer;
}
}

To be extra safe and make your code more hipster, type-hint setProgrammer() with the Programmer class and setProject() with Project. The form system will love this class.

Designing the Form

In the Form directory, create a new class for the form: BattleType. Make this extend the normal AbstractType and then hit command+N - or "Code"->"Generate" - and go to "Override Methods". Select the two we need: buildForm and configureOptions:

... lines 1 - 2
namespace AppBundle\Form;
... lines 5 - 6
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class BattleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
... lines 15 - 22
}
public function configureOptions(OptionsResolver $resolver)
{
... lines 27 - 30
}
}

Take out the parent calls - the parent methods are empty.

EntityType to the Rescue!

Okay, let's think about this. The API client will send programmer and project fields and both will be ids. Ultimately, we want to turn those into the entity objects corresponding to those ids before setting the data on the BattleModel object.

Well, this is exactly what the Entity type does. Use $builder->add() with project set to EntityType::class. To tell it what entity to use, add a class option set to AppBundle\Entity\Project:

... lines 1 - 5
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
... lines 7 - 10
class BattleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('programmer', EntityType::class, [
'class' => 'AppBundle\Entity\Programmer'
])
... lines 19 - 21
;
}
... lines 24 - 31
}

Do the same for programmer and set its class to AppBundle\Entity\Programmer:

... lines 1 - 10
class BattleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('programmer', EntityType::class, [
'class' => 'AppBundle\Entity\Programmer'
])
->add('project', EntityType::class, [
'class' => 'AppBundle\Entity\Project'
])
;
}
... lines 24 - 31
}

In a web form, the entity type renders as a drop-down of projects or programmers. But it's perfect for an API: it transforms the project id into a Project object by querying for it. That's exactly what we want.

In configureOptions(), add $resolver->setDefaults() and pass it two things: first the data_class set to BattleModel::class:

... lines 1 - 4
use AppBundle\Form\Model\BattleModel;
... lines 6 - 10
class BattleType extends AbstractType
{
... lines 13 - 24
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => BattleModel::class,
... line 29
]);
}
}

Make sure PhpStorm adds the use statement for that class. Then, set csrf_protection to false because we can't use normal CSRF protection in an API:

... lines 1 - 10
class BattleType extends AbstractType
{
... lines 13 - 24
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => BattleModel::class,
'csrf_protection' => false,
]);
}
}

Form, ready! Now let's hit the controller.

Leave a comment!

6
Login or Register to join the conversation
mehdi Avatar

Hello,
Why EntityType in web forms render a list of projects , and in Api world it renders just a project ?

Reply

Hey mehdi

Sorry for the confusion but Ryan meant that it would return an array of Project objects

Cheers!

Reply
Default user avatar

Hello,
I read that we should avoid in 90% caces Entity Form type because we will have usually hundreds of entities and app will work slowly (Iin this case api). What we can do in API if we have 500 Programmers and 200 Projects. How to optimize app (api)?

Reply

Hey again!

Hmm, I would wait for it to be a problem, if you start seeing that your API response slowly, then you can hunt down which part is the bottle neck and look for an optimization (a query, a process, etc)

Cheers!

Reply
Default user avatar

One question about flush().
For example:
If we have controller which calls 3 services (every create new collection of objects), so we will need to call ->persist($entity) method inside foreach() in every service. But where to call flush()? Only once in controller before return Response() or in every servise - so this is 3 times.
I am not sure whether is good idea that service only persist objects and controllers call flush()?

Reply

Hey axa!

It depends, as always, you can call flush more than once in a request, is not a big deal, but if you are going to do it, is better if you have a reason. In your case, I would consider making every service to call flush so you can save your work in case that another service fails, or maybe you want all the opposite and only flush if everything worked as spected

Cheers!

Reply
Cat in space

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

This tutorial uses an older version of Symfony. The concepts of Hypermedia & HATEOAS are still valid. But I recommend using API Platform in modern Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.0.*", // v3.0.3
        "doctrine/orm": "^2.5", // v2.5.4
        "doctrine/doctrine-bundle": "^1.6", // 1.6.2
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // v2.10.0
        "sensio/distribution-bundle": "^5.0", // v5.0.4
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.14
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.2
        "jms/serializer-bundle": "^1.1.0", // 1.1.0
        "white-october/pagerfanta-bundle": "^1.0", // v1.0.5
        "lexik/jwt-authentication-bundle": "^1.4", // v1.4.3
        "willdurand/hateoas-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.6
        "symfony/phpunit-bridge": "^3.0", // v3.0.3
        "behat/behat": "~3.1@dev", // dev-master
        "behat/mink-extension": "~2.2.0", // v2.2
        "behat/mink-goutte-driver": "~1.2.0", // v1.2.1
        "behat/mink-selenium2-driver": "~1.3.0", // v1.3.1
        "phpunit/phpunit": "~4.6.0", // 4.6.10
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}
userVoice