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.

GET Your (One) Battle On

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

Next, let's keep going with viewing a single battle. Scenario: GETting a single battle. And thinking about this, we're going to need to make sure that there's a battle in the database first. I'm going to use similar language as before to create a Fred programmer and a project called project_facebook. I also have another step that allows me to say And there has been a battle between "Fred" and "project_facebook":

... lines 1 - 25
Scenario: GET one battle
Given there is a project called "projectA"
And there is a programmer called "Fred"
And there has been a battle between "Fred" and "projectA"
... lines 30 - 34

By the way, the nice auto-completion I'm getting is from the new PHPStorm 8 version, which has integration with Behat. I highly recommend it.

Great, so this makes sure there's something in the database. Next, we'll make the GET request to /api/battles/something. Here's the problem: the only way we can really identify our Battles are by their id. They're not like Programmer, where each has a unique nickname that we can use.

The Special %battles.last.id% Syntax

Here, we know there's a Battle in the database, but just like before when we were building the request body, we have no idea what that id was going to be. Fortunately, we can use that same magic % syntax. This time we can say %battles.last.id%:

... lines 1 - 25
Scenario: GET one battle
Given there is a project called "projectA"
And there is a programmer called "Fred"
And there has been a battle between "Fred" and "projectA"
When I request "GET /api/battles/%battles.last.id%"
... lines 31 - 34

Before, we used this syntax to query for a Programmer by its nickname. But it also has a special "last" keyword, which queries for the last record in the table. Again, this is me adding special things to my Behat project. Which is really handy for testing the API.

Next, go to programmer.feature and find its "GET one programmer". We'll copy the endpoint and "Then" lines and do something similar. The status code looks good. The Battle has a didProgrammerWin field and we'll also make sure that the notes field is returned in the response:

... lines 1 - 25
Scenario: GET one battle
Given there is a project called "projectA"
And there is a programmer called "Fred"
And there has been a battle between "Fred" and "projectA"
When I request "GET /api/battles/%battles.last.id%"
Then the response status code should be 200
And the "notes" property should exist
And the "didProgrammerWin" property should exist

You guys know the drill. We're going to try this first to make sure it fails. This is on line 26, so we'll add :26 to only run this scenario:

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

And there we go - we get the 404 instead of the 200 and that's perfect.

Creating the GET Endpoint

Let's get this working! In BattleController, add a new GET endpoint for /api/battles/{id} and change the method to showAction. Because we have a {id} in the path, the showAction will have an $id argument.

... lines 1 - 10
protected function addRoutes(ControllerCollection $controllers)
{
... line 13
$controllers->get('/api/battles/{id}', array($this, 'showAction'));
}
... lines 16 - 50
public function showAction($id)
{
... lines 53 - 60
}
... lines 62 - 63

From here, life is really familiar. First, do we need security? - always ask yourself that. I'm going to decide that anyone can fetch battle details out without being authenticated. So we won't add any protection.

We will need to go and query for the Battle object that represents the given id. We always want to check if that matches anything, and if it doesn't, we want to return a really nice 404 response. In episode 1, we did that by using a function called throw404. That's going to throw a special exception, which is mapped to a 404 status code, and because we have our nice error handling, the ApiProblem response format will be returned:

... lines 1 - 50
public function showAction($id)
{
$battle = $this->getBattleRepository()->find($id);
if (!$battle) {
$this->throw404('No battle with id '.$id);
}
... lines 57 - 60
}
... lines 62 - 63

We have the object and we know we want to serialize it to get that consistent response. Once again, this is really easy, because we can just re-use the createApiResponse method, and that's going to do all the work for us. We don't need the 2nd argument, because that defaults to 200 already:

... lines 1 - 50
public function showAction($id)
{
$battle = $this->getBattleRepository()->find($id);
if (!$battle) {
$this->throw404('No battle with id '.$id);
}
$response = $this->createApiResponse($battle, 200);
return $response;
}
... lines 62 - 63

That's it guys - let's run the test:

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

Wow, and it already passes. This is getting really really easy, which is why we put in all the work before this.

Don't Forget to Fix the Location Header

Now that we have a proper showAction(), we can go back and fix the "todo" in the header. First, we'll need to give the route an internal name - api_battle_show:

... lines 1 - 10
protected function addRoutes(ControllerCollection $controllers)
{
... line 13
$controllers->get('/api/battles/{id}', array($this, 'showAction'))
->bind('api_battle_show');
}
... lines 17 - 65

In newAction(), we'll use generateUrl() to make the URL for us:

... lines 1 - 17
public function newAction(Request $request)
{
... lines 20 - 46
$url = $this->generateUrl('api_battle_show', array('id' => $battle->id));
$response->headers->set('Location', $url);
return $response;
}
... lines 52 - 65

Again, these shortcuts are things I added to my project, but this is just using the standard method in Silex to generate the URL based on the name of the route. And you can see what all of the shortcut methods really do by opening up the BaseController class.

First, let's make sure we didn't break anything by re-running the entire feature.

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

Green green green!

Leave a comment!

7
Login or Register to join the conversation

Hi, is there a way to implement the same %battles.last.id% in Symfony? I mean, since "id" is usually a protected or private variable, the only way I found is to use %battles.last.getId()%, which is not as elegant as yours... Any suggestion?

Reply

Hey Pietrino!

Ah, so you got all the hard stuff working then with the entity manager, etc - nice work! For this last piece, use the PropertyAccess component: http://symfony.com/doc/curr.... This is used internally by Symfony's form system, which is how you're able to say "name" in your form and it calls getName().

Let me know if that works!

Reply

Hi Ryan, I managed for it to work in behat features, with something like creating a wrapper class, that implements the __get method and returns a PropertyAccessor. It works, but leaves me with two similar problems:

1. it works only on the first level, so it doesn't let me access "protected properties" for something like %item.children[0].id%
2. it happens to be a very similar problem with Expression Language formulas used in Hateoas\Relations (such as "expr(object.id)"

Any ideas?
Maybe I can extend Expression Language?

Reply

Hey Pietrino!

Hmm, yes, I see the issue. I'm sure it's possible to get this working, but I might stick with the %battles.last.getId()% instead - trying to get this working just like %battles.last.id%` with the expression language might not be worth it (but might be fun to try!). The easiest way I can see to do this would be to NOT use the expression language, and instead parse the string manually - exploding on the first "." and using the pieces separately. If you give "children[0].id" to the PropertyAccessor, it'll *love* that.

Good question and good luck :)

Reply

Yup, might be interesting to dive into Expression Language and extend it beyond providers and registered functions... I'll give it a try! Thanks!

Reply
Default user avatar
Default user avatar argy_13 | posted 5 years ago

Hi guys,

i just want to mention that on the script at the last two code that you re describing the first says

"$url = $this->generateUrl('api_battle_show', array('id' => $battle->id));"

and the second says

"$controllers->get('/api/battles/{id}', array($this, 'showAction'))->bind('api_battles_show');"

so there is a tiny difference that "api_battles_show" at first is in singular and at second is plural. Just to mention this "s" on script.

You re doing a perfect work,
Argy

Reply

Hey Argy,

Wow, it was difficult to spot, well done! In the video we use singular form, i.e. "api_battle_show" in both places. And thanks for this report, I'll fix it.

Cheers!

1 Reply
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