If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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
:
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.
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 |
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.
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.
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!
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.
"Houston: no signs of life"
Start the conversation!
// 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
}
}