Buy Access to Course
03.

Saving Related Resources in a Form

Share this awesome video!

|

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:

55 lines | src/AppBundle/Battle/BattleManager.php
// ... 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:

107 lines | src/AppBundle/Entity/Battle.php
// ... 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:

33 lines | src/AppBundle/Form/Model/BattleModel.php
// ... 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:

33 lines | src/AppBundle/Form/Model/BattleModel.php
// ... 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:

33 lines | src/AppBundle/Form/BattleType.php
// ... 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:

33 lines | src/AppBundle/Form/BattleType.php
// ... 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:

33 lines | src/AppBundle/Form/BattleType.php
// ... 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:

33 lines | src/AppBundle/Form/BattleType.php
// ... 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:

33 lines | src/AppBundle/Form/BattleType.php
// ... 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.