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

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!

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