Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Weird Endpoint: Command: Power-Up a Programmer

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

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

On our web interface, if you select a programmer, you can start a battle, or you can hit this "Power Up" button. Sometimes our power goes up, sometimes it goes down. And isn't that just like life.

The higher the programmer's power level, the more likely they will win future battles.

Notice: all we need to do is click one button: Power Up. We don't fill in a box with the desired power level and hit submit, we just "Power Up"! And that makes this a weird endpoint to build for our API.

Why? Basically, it doesn't easily fit into REST. We're not sending or editing a resource. No, we're more issuing a command: "Power Up!".

Let's design this in a test: public function testPowerUp():

... lines 1 - 7
class ProgrammerControllerTest extends ApiTestCase
{
... lines 10 - 333
public function testPowerUp()
{
... lines 336 - 348
}
}

Grab the $programmer and Response lines from above, but replace tagLine with a powerLevel set to 10:

... lines 1 - 7
class ProgrammerControllerTest extends ApiTestCase
{
... lines 10 - 333
public function testPowerUp()
{
$this->createProgrammer(array(
'nickname' => 'UnitTester',
'avatarNumber' => 3,
'powerLevel' => 10
));
$response = $this->client->post('/api/programmers/UnitTester/powerup', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 345 - 348
}
}

Now we know that the programmer starts with this amount of power.

The URL Structure of a Command

From here, we have two decisions to make: what the URL should look like and what HTTP method to use. Well, we're issuing a command for a specific programmer, so make the URL /api/programmers/UnitTester/powerup:

... lines 1 - 7
class ProgrammerControllerTest extends ApiTestCase
{
... lines 10 - 333
public function testPowerUp()
{
... lines 336 - 341
$response = $this->client->post('/api/programmers/UnitTester/powerup', [
... line 343
]);
... lines 345 - 348
}
}

Here's where things get ugly. This is a new URI... so philosophically, this represents a new resource. Following what we did with the tag line, we should think of this as the "power up" resource. So, are we editing the "power up" resource... or are we doing something different?

The "Power Up?" Resource???

Are you confused? I'm kind of confused. It just doesn't make sense to talk about some "power up" resource. "Power up" is not a resource, even though the rules of REST want it to be. We just had to create some URL... and this made sense.

So if this isn't a resource, how do we decide whether to use PUT or POST? Here's the key: when REST falls apart and your endpoint doesn't fit into it anymore, use POST.

POST for Weird Endpoints

Earlier, we talked about how PUT is idempotent, meaning if you make the same request 10 times, it has the same effect as if you made it just once. POST is not idempotent: if you make a request 10 times, each request may have additional side effects.

Usually, this is how we decide between POST and PUT. And it fits here! The "power up" endpoint is not idempotent: hence POST.

But wait! Things are not that simple. Here's the rule I want you to follow. If you're building an endpoint that fits into the rules of REST: choose between POST and PUT by asking yourself if it is idempotent.

But, if your endpoint does not fit into REST - like this one - always use POST. So even if the "power up" endpoint were idempotent, I would use POST. In reality, a PUT endpoint must be idempotent, but a POST endpoint is allowed to be either.

So, use ->post(). And now, remove the body: we are not sending any data. This is why POST fits better: we're not really updating a resource:

... lines 1 - 7
class ProgrammerControllerTest extends ApiTestCase
{
... lines 10 - 333
public function testPowerUp()
{
... lines 336 - 341
$response = $this->client->post('/api/programmers/UnitTester/powerup', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 345 - 348
}
}

And the Endpoint Returns....?

Assert that 200 matches the status code:

... lines 1 - 341
$response = $this->client->post('/api/programmers/UnitTester/powerup', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
$this->assertEquals(200, $response->getStatusCode());
... lines 346 - 351

And now, what should the endpoint return?

We're not in a normal REST API situation, so it matters less. You could return nothing, or you could return the power level. But to be as predictable as possible, let's return the entire programmer resource. Read the new power level from this with $this->asserter()->readResponseProperty() and look for powerLevel:

... lines 1 - 341
$response = $this->client->post('/api/programmers/UnitTester/powerup', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
$this->assertEquals(200, $response->getStatusCode());
$powerLevel = $this->asserter()
->readResponseProperty($response, 'powerLevel');
... lines 348 - 351

This is a property that we're exposing:

... lines 1 - 31
class Programmer
{
... lines 34 - 67
/**
... lines 69 - 71
* @Serializer\Expose
*/
private $powerLevel = 0;
... lines 75 - 206
}

We don't know what this value will be, but it should change. Use assertNotEquals() to make sure the new powerLevel is no longer 10:

... lines 1 - 341
$response = $this->client->post('/api/programmers/UnitTester/powerup', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
$this->assertEquals(200, $response->getStatusCode());
$powerLevel = $this->asserter()
->readResponseProperty($response, 'powerLevel');
$this->assertNotEquals(10, $powerLevel, 'The level should change');
... lines 349 - 351

Implement the Endpoint

Figuring out the URL and HTTP method was the hard part. Let's finish this. In ProgrammerController, add a new public function powerUpAction():

... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 187
public function powerUpAction(Programmer $programmer)
{
... lines 190 - 193
}
}

Add a route with /api/programmers/{nickname}/powerup and an @Method set to POST:

... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 183
/**
* @Route("/api/programmers/{nickname}/powerup")
* @Method("POST")
*/
public function powerUpAction(Programmer $programmer)
{
... lines 190 - 193
}
}

Once again, type-hint the Programmer argument:

... lines 1 - 7
use AppBundle\Entity\Programmer;
... lines 9 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 187
public function powerUpAction(Programmer $programmer)
{
... lines 190 - 193
}
}

To power up, we have a service already made for this. Just say: $this->get('battle.power_manager') ->powerUp() and pass it the $programmer:

... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 187
public function powerUpAction(Programmer $programmer)
{
$this->get('battle.power_manager')
->powerUp($programmer);
... lines 192 - 193
}
}

That takes care of everything. Now, return $this->createApiResponse($programmer):

... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 187
public function powerUpAction(Programmer $programmer)
{
$this->get('battle.power_manager')
->powerUp($programmer);
return $this->createApiResponse($programmer);
}
}

Done! Copy the testPowerUp() method name and run that test:

./vendor/bin/phpunit -—filter testPowerUp

Success!

And that's it - that's everything. I really hope this course will save you from some frustrations that I had. Ultimately, don't over-think things, add links when they're helpful and build your API for whoever will actually use it.

Ok guys - seeya next time!

Leave a comment!

14
Login or Register to join the conversation
Lijana Z. Avatar
Lijana Z. Avatar Lijana Z. | posted 4 years ago

REST for me felt always like waste of time thinking about correct urls, status codes. As you showed also - there are situations where it does not fit in REST rules anyway. I remember long time ago I was using only GET and POST and whatever url felt good for me. And it was enough. No http codes. And I had no problems.

Reply

Hey Lijana Z.!

I think knowing what the "rules" are can help us normalize our APIs and make them all "work" a little more consistently. But yea, you can take this to an extreme - and that's exactly what I want people to not worry about. Not sure what the correct status code or HTTP verb is for an endpoint? Spend 5 seconds thinking about it, then choose your first choice and move on ;).

So, we're thinking alike!

Cheers!

Reply
Vladimir Z. Avatar
Vladimir Z. Avatar Vladimir Z. | posted 5 years ago

Will there be a REST Course 6?

Reply

Right now, no - I don't see enough topics that we still haven't covered. But, if you have some topics that are still troubling you, then you're probably not the only one, and there might be a course 6 :)

Reply

Oh, except that we *may* do a course on documentation - that is one topic that we haven't covered yet.

Reply
Default user avatar

The only problem I'm having with all this code right now is that I can't change the URL structure. If I wanted to use singular resource names, so "api/programmer" instead of "api/programmers" I'd be in trouble... I'd have to change all the tests and all the annotations in the controllers :(

Reply

Hey Johan,

But it's a good practice to hardcode URLs in tests. It's a bad idea to use router to generate URLs in tests. Imagine, you've accidentally changed some URL - then some tests will fail and show you the problem. of course you can move duplicated URL prefix in a private property, but probably it just worse test readability: tests should be well readable. And btw, URLs change very rare, i'd say API URLs never change ;)

Cheers!

Reply
Default user avatar

Ye you're right. I checked the symfony docs and they say the same thing. Thanks :)

But what about we put @Route("/api/programmers") as a class annotation in the ProgrammerController and create routes relative to that route instead? I think it would be good :)

Thanks Victor

Reply

Yeah, I'm definitely +1 for it! We always try to do that if it possible - it reduces misprints. But here we left it as is to reduce complexity and do not produce questions: some parts of the class is hidden due to our dynamic code blocks.

Cheers!

1 Reply
Default user avatar

That makes sense, thanks! :)

Reply
Default user avatar
Default user avatar Neandher Carlos | posted 5 years ago | edited

I need help to upload.

$request->getContent()```


With PUT and PATCH:

------WebKitFormBoundaryUFCyBVhvYZjTos7t
Content-Disposition: form-data; name="avatarImageFile"; filename="cadastro_consultas.jpg"
Content-Type: image/jpeg

���JFIFHH���ExifMM*bj

....



With POST:

object(Symfony\Component\HttpFoundation\File\UploadedFile)[15]
private 'test' => boolean false
private 'originalName' => string 'cadastro_membros.jpg' (length=20)
private 'mimeType' => string 'image/jpeg' (length=10)
private 'size' => int 276654
private 'error' => int 0
private 'pathName' (SplFileInfo) => string 'C:\Windows\Temp\phpD540.tmp' (length=27)
private 'fileName' (SplFileInfo) => string 'phpD540.tmp' (length=11)`

In update resource with files to upload, showld i use POST instead PUT or PATCH????

Reply

Hey Carlos,

If you'd like to use PUT / PATCH methods, then you have to put the file's content in the body of your request I think, otherwise use POST to upload files. Here's a bit more details on how to upload a file using PUT: http://php.net/manual/en/fe...

Cheers!

Reply
Default user avatar

Hi, thanks for this course. Shouldn't in real life the ProgrammerController->newAction better be available for anonymous? I you change this only by annotations. You get stuck with $this->getUser() which can't be used anonymously.

Reply

Hey Till,

Yes, the "ProgrammerController::newAction()" shouldn't be available for anonymous users, otherwise we will have a problem, since in "Programmer::setUser()" has the "public function setUser(User $user)" signature, i.e. we don't allow passing null here. However, "@Security("is_granted('ROLE_USER')")" annotation for the "ProgrammerController" class ensure that we always has a user in this controller, so if user is null - you can't get these end points.

Cheers!

Reply
Cat in space

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

This tutorial uses an older version of Symfony. The concepts of Hypermedia & HATEOAS are still valid. But I recommend using API Platform in modern Symfony apps.

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice