Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ?valid, but the code we use can't be used in a real application.

Start (Create) an Epic Battle (Resource)

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

What's cool is that because we've done so much work up to this point, coding up our API is going to get really easy. I'll copy TokenController - I like having one resource per Controller. Update the class name. And this already has some code we want. So let's change the URL to /api/battles:

<?php
namespace KnpU\CodeBattle\Controller\Api;
use KnpU\CodeBattle\Controller\BaseController;
use Silex\ControllerCollection;
class BattleController extends BaseController
{
protected function addRoutes(ControllerCollection $controllers)
{
$controllers->post('/api/battles', array($this, 'newAction'));
}
... lines 14 - 18
}

In newAction, we're going to reuse a lot of this. To create a Battle, you do need to be logged in, so we'll keep the enforceUserSecurity(). This decodeRequestBodyIntoParameters() is what goes out and reads the JSON on the request and gives us back this array-like object called a ParameterBag. So that's all good too.

I am going to remove most of the rest of this, because it's specific to creating a token.

Finding the Battle and Project

What we need to do is read the programmer and project id's off of the request and then create and save a new Battle object based off of those. So first, let's go get the projectId. We're able to use this nice get function, because the decodeRequestBodyIntoParameters function gives us that ParametersBag object. Let's also get the programmerId.

... lines 1 - 15
public function newAction(Request $request)
{
$this->enforceUserSecurity();
$data = $this->decodeRequestBodyIntoParameters($request);
$programmerId = $data->get('programmerId');
$projectId = $data->get('projectId');
... lines 24 - 33
}
... lines 35 - 36

Perfect!

And with these 2 things, I need to query for the Project and Programmer objects, because the way I'm going to create the Battle will need them, not just their ids. Plus, this will tell us if these ids are even real. I'll use one of my shortcuts to query for the Project:

... lines 1 - 15
public function newAction(Request $request)
{
... lines 18 - 22
$projectId = $data->get('projectId');
... lines 24 - 25
$project = $this->getProjectRepository()->find($projectId);
... lines 27 - 33
}
... lines 35 - 36

All this is doing is going out and finding the Project with that id and returning the Project model object that has the data from that row. We'll do the same thing with Programmer:

... lines 1 - 15
public function newAction(Request $request)
{
... lines 18 - 21
$programmerId = $data->get('programmerId');
... lines 23 - 24
$programmer = $this->getProgrammerRepository()->find($programmerId);
... lines 26 - 33
}
... lines 35 - 36

The BattleManager

Normally, to create a battle, you'd expect me to instantiate it manually. We've done that before for Tokens and Programmers. But for Battles, I have a helper called BattleManager that will do this for us. So instead of creating the Battle by hand, we'll call this battle() function and pass it the Programmer and Project:

... lines 1 - 4
use KnpU\CodeBattle\Model\Battle;
use KnpU\CodeBattle\Model\Programmer;
use KnpU\CodeBattle\Model\Project;
... lines 8 - 10
class BattleManager
{
... lines 13 - 29
public function battle(Programmer $programmer, Project $project)
{
$battle = new Battle();
$battle->programmer = $programmer;
$battle->project = $project;
$battle->foughtAt = new \DateTime();
if ($programmer->powerLevel < $project->difficultyLevel) {
// not enough energy
$battle->didProgrammerWin = false;
$battle->notes = 'You don\'t have the skills to even start this project. Read the documentation (i.e. power up) and try again!';
} else {
if (rand(0, 2) != 2) {
$battle->didProgrammerWin = true;
$battle->notes = 'You battled heroically, asked great questions, worked pragmatically and finished on time. You\'re a hero!';
} else {
$battle->didProgrammerWin = false;
$battle->notes = 'Requirements kept changing, too many meetings, project failed :(';
}
$programmer->powerLevel = $programmer->powerLevel - $project->difficultyLevel;
}
$this->battleRepository->save($battle);
$this->programmerRepository->save($programmer);
return $battle;
}

It takes care of all of the details of creating the Battle, figuring out who won, setting the foughtAt time, adding some notes and saving all of this. So we do need to create a Battle object, but this will do it for us.

Creating the Battle

Back in BattleController, I already have a shortcut method setup to give us the BattleManager object. Then we'll use the battle() function we just saw and pass it the Programmer and Project:

... lines 1 - 15
public function newAction(Request $request)
{
... lines 18 - 24
$programmer = $this->getProgrammerRepository()->find($programmerId);
$project = $this->getProjectRepository()->find($projectId);
$battle = $this->getBattleManager()->battle($programmer, $project);
... lines 29 - 33
}
... lines 35 - 36

And that's it - the Battle is created and saved for us. Now all we need to do is pass the Battle to the createApiResponse() method. And that will take care of the rest:

... lines 1 - 15
public function newAction(Request $request)
{
... lines 18 - 27
$battle = $this->getBattleManager()->battle($programmer, $project);
$response = $this->createApiResponse($battle, 201);
... lines 31 - 32
return $response;
}
... lines 35 - 36

The createApiResponse method uses the serializer object to turn the Battle object into JSON. We haven't done any configuration on this class for the serializer, which means that it's serializing all of the fields. And for now, I'm happy with that - we're getting free functionality.

This looks good to me - so let's try it!

php vendor/bin/behat features/api/battle.feature

Oh! It almost passes. It gets the 201 status code, but it's missing the Location header. In the response, we can see the created Battle, with notes on why our programmer lost.

Adding the Location Header

Back in newAction, we can just set createApiResponse to a variable and then call $response->headers->set() and pass it Location add a temporary todo:

... lines 1 - 15
public function newAction(Request $request)
{
... lines 18 - 29
$response = $this->createApiResponse($battle, 201);
$response->headers->set('Location', 'TODO');
return $response;
}
... lines 35 - 36

Remember, this is the location to view a single battle, and we don't have an endpoint for that yet. But this will get our tests to pass for now:

php vendor/bin/behat features/api/battle.feature

Perfect!

Battle Validation

So let's add some validation. Since the battle() function is doing all of the work of creating the Battle, we don't need to worry about it too much. We just need to make sure that projectId and programmerId are valid. I'll just do validation manually here by creating an $errors variable and then check to see if we didn't find a Project in the database for some reason. If that's the case, let's add an error with a nice message. And we'll do the same thing with the Programmer:

... lines 1 - 15
public function newAction(Request $request)
{
... lines 18 - 24
$programmer = $this->getProgrammerRepository()->find($programmerId);
$project = $this->getProjectRepository()->find($projectId);
$errors = array();
if (!$project) {
$errors['projectId'] = 'Invalid or missing projectId';
}
if (!$programmer) {
$errors['programmerId'] = 'Invalid or missing programmerId';
}
if ($errors) {
$this->throwApiProblemValidationException($errors);
}
... lines 38 - 47
}
... lines 49 - 50

Finally at the bottom, if we actually have at least one thing in the $errors variable, we're going to call a nice method we made in a previous chapter called throwApiProblemValidationException and just pass it the array of errors. It's just that easy.

We don't have a scenario setup for this, so let's tweak ours temporarily to try it - foobar is definitely not a valid id:

... lines 1 - 10
Scenario: Create a battle
Given there is a project called "my_project"
And there is a programmer called "Fred"
And I have the payload:
"""
{
"programmerId": "foobar",
"projectId": "%projects.my_project.id%"
}
"""
... lines 21 - 25

php vendor/bin/behat features/api/battle.feature:11

Now, we can see that the response code is 400 and we have this beautifully structured error response. So that's why we went to all of that work of setting up our error handling correctly, because the rest of our API is so easy and so consistent.

Let's change the scenario back and re-run the tests to make sure we haven't broken anything:

php vendor/bin/behat features/api/battle.feature:11

Perfect! This is a really nice endpoint for creating a battle.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

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

This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ?valid, but the code we use can't be used in a real application.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "silex/silex": "~1.0", // v1.3.2
        "symfony/twig-bridge": "~2.1", // v2.7.3
        "symfony/security": "~2.4", // v2.7.3
        "doctrine/dbal": "^2.5.4", // v2.5.4
        "monolog/monolog": "~1.7.0", // 1.7.0
        "symfony/validator": "~2.4", // v2.7.3
        "symfony/expression-language": "~2.4", // v2.7.3
        "jms/serializer": "~0.16", // 0.16.0
        "willdurand/hateoas": "~2.3" // v2.3.0
    },
    "require-dev": {
        "behat/mink": "~1.5", // v1.5.0
        "behat/mink-goutte-driver": "~1.0.9", // v1.0.9
        "behat/mink-selenium2-driver": "~1.1.1", // v1.1.1
        "behat/behat": "~2.5", // v2.5.5
        "behat/mink-extension": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~5.7.0", // 5.7.27
        "guzzle/guzzle": "~3.7" // v3.9.3
    }
}
userVoice