Saving Related Resources in a Form
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.
Hello,
Why EntityType in web forms render a list of projects , and in Api world it renders just a project ?