If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Head to the controller. We've got this form flow mastered! Step 1: create a new
BattleModel
object: $battleModel = new BattleModel()
:
... lines 1 - 11 | |
class BattleController extends BaseController | |
{ | |
/** | |
* @Route("/api/battles") | |
* @Method("POST") | |
*/ | |
public function newAction(Request $request) | |
{ | |
$battleModel = new BattleModel(); | |
... lines 21 - 34 | |
} | |
} |
Step 2: create the form: $form = $this->createForm()
with BattleType::class
:
... lines 1 - 5 | |
use AppBundle\Form\BattleType; | |
... lines 7 - 11 | |
class BattleController extends BaseController | |
{ | |
... lines 14 - 17 | |
public function newAction(Request $request) | |
{ | |
$battleModel = new BattleModel(); | |
$form = $this->createForm(BattleType::class, $battleModel); | |
... lines 22 - 34 | |
} | |
} |
On my version of PhpStorm, I need to go back and re-type the e
to trigger auto-completion
so that the use
statement is added above.
For the second argument to createForm
: pass it $battleModel
.
Step 3: Use $this->processForm()
:
... lines 1 - 11 | |
class BattleController extends BaseController | |
{ | |
... lines 14 - 17 | |
public function newAction(Request $request) | |
{ | |
$battleModel = new BattleModel(); | |
$form = $this->createForm(BattleType::class, $battleModel); | |
$this->processForm($request, $form); | |
... lines 23 - 34 | |
} | |
} |
Remember, this is a method we added in BaseController
:
... lines 1 - 19 | |
abstract class BaseController extends Controller | |
{ | |
... lines 22 - 142 | |
protected function processForm(Request $request, FormInterface $form) | |
{ | |
$data = json_decode($request->getContent(), true); | |
if ($data === null) { | |
$apiProblem = new ApiProblem(400, ApiProblem::TYPE_INVALID_REQUEST_BODY_FORMAT); | |
throw new ApiProblemException($apiProblem); | |
} | |
$clearMissing = $request->getMethod() != 'PATCH'; | |
$form->submit($data, $clearMissing); | |
} | |
... lines 155 - 185 | |
} |
it decodes the Request
body and submits it into the form, which is what we do
on every endpoint that processes data.
Type-hint the Request
argument for the controller and pass this to processForm()
.
If the form is not valid, we need to send back errors. Use another method from
earlier: $this->throwApiProblemValidationException()
and pass it the $form
object:
... lines 1 - 11 | |
class BattleController extends BaseController | |
{ | |
... lines 14 - 17 | |
public function newAction(Request $request) | |
{ | |
$battleModel = new BattleModel(); | |
$form = $this->createForm(BattleType::class, $battleModel); | |
$this->processForm($request, $form); | |
if (!$form->isValid()) { | |
$this->throwApiProblemValidationException($form); | |
} | |
... lines 27 - 34 | |
} | |
} |
This will grab the validation errors off and create that response.
At this point, we have a BattleModel
object that's populated with the Programmer
and
Project
objects sent in the request. To create the battle, we need to use the
BattleManager
. Do that with $this->getBattleManager()
- that's just a shortcut
to get the service - ->battle()
and pass it $battleModel->getProgrammer()
and
$battleModel->getProject()
:
... lines 1 - 11 | |
class BattleController extends BaseController | |
{ | |
... lines 14 - 17 | |
public function newAction(Request $request) | |
{ | |
$battleModel = new BattleModel(); | |
$form = $this->createForm(BattleType::class, $battleModel); | |
$this->processForm($request, $form); | |
if (!$form->isValid()) { | |
$this->throwApiProblemValidationException($form); | |
} | |
$battle = $this->getBattleManager()->battle( | |
$battleModel->getProgrammer(), | |
$battleModel->getProject() | |
); | |
... lines 32 - 34 | |
} | |
} |
Put a little $battle =
in the beginning of all of this to get the new Battle
object.
Perfect!
Now that the battle has been heroically fought, let's send back the gory details.
Use return $this->createApiResponse()
and pass it $battle
and the 201 status code:
... lines 1 - 11 | |
class BattleController extends BaseController | |
{ | |
... lines 14 - 17 | |
public function newAction(Request $request) | |
{ | |
$battleModel = new BattleModel(); | |
$form = $this->createForm(BattleType::class, $battleModel); | |
$this->processForm($request, $form); | |
if (!$form->isValid()) { | |
$this->throwApiProblemValidationException($form); | |
} | |
$battle = $this->getBattleManager()->battle( | |
$battleModel->getProgrammer(), | |
$battleModel->getProject() | |
); | |
// todo - set Location header | |
return $this->createApiResponse($battle, 201); | |
} | |
} |
We aren't setting a Location
header yet, so let's at least add a todo
for that.
We are done! Controller, model and form: these are the only pieces we need to create a robust endpoint. Try the test:
./vendor/bin/phpunit --filter testPOSTCreateBattle
We are in prime battling shape.
Now, let's complicate things and learn how to really take control of every field in our endpoint. And, learn more about relations.
// 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
}
}