If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOn 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.
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?
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.
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 | |
} | |
} |
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 |
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!
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!
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 :)
Oh, except that we *may* do a course on documentation - that is one topic that we haven't covered yet.
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 :(
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!
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
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!
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????
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!
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.
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!
// 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
}
}
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.