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 SubscribeBefore we code up the endpoint, start with the test. But wait! This test is going to be pretty cool: we'll make a request for a programmer resource and follow that link to its battles.
In ProgrammerControllerTest
, add a new public function testFollowProgrammerBattlesLink()
:
... lines 1 - 7 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 10 - 64 | |
public function testFollowProgrammerBattlesLink() | |
{ | |
... lines 67 - 87 | |
} | |
... lines 89 - 315 | |
} |
Copy the first 2 parts from testGETProgrammer()
that create the programmer and make the request. Add those here:
... lines 1 - 7 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 10 - 64 | |
public function testFollowProgrammerBattlesLink() | |
{ | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
... lines 71 - 78 | |
$response = $this->client->get('/api/programmers/UnitTester', [ | |
'headers' => $this->getAuthorizedHeaders('weaverryan') | |
]); | |
... lines 82 - 87 | |
} | |
... lines 89 - 315 | |
} |
Okay: before the request, we need to add some battles to the database so we have something results to check out. Create a project first with $this->createProject('cool_project')
:
... lines 1 - 66 | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
$project = $this->createProject('cool_project'); | |
... lines 72 - 317 |
Now, let's add 3 battles. And remember! To do that, we need the BattleManager
service. Set that up with $battleManager = $this->getService()
- that's a helper method in ApiTestCase
- and look up battle.battle_manager
:
... lines 1 - 66 | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
$project = $this->createProject('cool_project'); | |
/** @var BattleManager $battleManager */ | |
$battleManager = $this->getService('battle.battle_manager'); | |
... lines 75 - 317 |
Let's add some inline PHPDoc so PhpStorm auto-completes the next lines.
Love it!
Now, life is easy. Add, $battleManager->battle()
and pass it $programmer
:
... lines 1 - 66 | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
$project = $this->createProject('cool_project'); | |
/** @var BattleManager $battleManager */ | |
$battleManager = $this->getService('battle.battle_manager'); | |
$battleManager->battle($programmer, $project); | |
... lines 76 - 317 |
And, whoops - make sure you have a $programmer
variable set above. Now, add $project
. Copy that and paste it 2 more times:
... lines 1 - 66 | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
$project = $this->createProject('cool_project'); | |
/** @var BattleManager $battleManager */ | |
$battleManager = $this->getService('battle.battle_manager'); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
... lines 78 - 317 |
And we are setup! After we make the request for the programmer, we should get back a link we can follow. Get that link with $uri = $this->asserter()->readResponseProperty()
. Read _links.battles
:
... lines 1 - 66 | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
$project = $this->createProject('cool_project'); | |
/** @var BattleManager $battleManager */ | |
$battleManager = $this->getService('battle.battle_manager'); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
$response = $this->client->get('/api/programmers/UnitTester', [ | |
'headers' => $this->getAuthorizedHeaders('weaverryan') | |
]); | |
$url = $this->asserter() | |
->readResponseProperty($response, '_links.battles'); | |
... lines 84 - 317 |
Make sure you pass $response
as the first argument.
Now, follow that link! Be lazy and copy the $response =
code from above, because we still need that Authorization
header. But change the url to be our dynamic $uri
:
... lines 1 - 66 | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
$project = $this->createProject('cool_project'); | |
/** @var BattleManager $battleManager */ | |
$battleManager = $this->getService('battle.battle_manager'); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
$response = $this->client->get('/api/programmers/UnitTester', [ | |
'headers' => $this->getAuthorizedHeaders('weaverryan') | |
]); | |
$url = $this->asserter() | |
->readResponseProperty($response, '_links.battles'); | |
$response = $this->client->get($url, [ | |
'headers' => $this->getAuthorizedHeaders('weaverryan') | |
]); | |
... lines 87 - 317 |
Before we assert anything, let's dump the response and decide later how this should all exactly look:
... lines 1 - 66 | |
$programmer = $this->createProgrammer(array( | |
'nickname' => 'UnitTester', | |
'avatarNumber' => 3, | |
)); | |
$project = $this->createProject('cool_project'); | |
/** @var BattleManager $battleManager */ | |
$battleManager = $this->getService('battle.battle_manager'); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
$battleManager->battle($programmer, $project); | |
$response = $this->client->get('/api/programmers/UnitTester', [ | |
'headers' => $this->getAuthorizedHeaders('weaverryan') | |
]); | |
$url = $this->asserter() | |
->readResponseProperty($response, '_links.battles'); | |
$response = $this->client->get($url, [ | |
'headers' => $this->getAuthorizedHeaders('weaverryan') | |
]); | |
$this->debugResponse($response); | |
... lines 88 - 317 |
Test, check! Let's hook this up. Open ProgrammerController
. At first, it's pretty easy. Exchange the nickname
for a Programmer
object. I'll use a magic param converter for this: just type-hint the argument with Programmer
, and it will magically make the query for us:
... lines 1 - 22 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 25 - 150 | |
/** | |
* @Route("/api/programmers/{nickname}/battles", name="api_programmers_battles_list") | |
*/ | |
public function battlesListAction(Programmer $programmer) | |
{ | |
... lines 156 - 159 | |
} | |
} |
Next, get battles the way you always do: $this->getDoctrine()->getRepository('AppBundle:Battle')
:
... lines 1 - 22 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 25 - 153 | |
public function battlesListAction(Programmer $programmer) | |
{ | |
$battles = $this->getDoctrine()->getRepository('AppBundle:Battle') | |
... lines 157 - 159 | |
} | |
} |
Use findBy()
to return an array that match programmer => $programmer
:
... lines 1 - 22 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 25 - 153 | |
public function battlesListAction(Programmer $programmer) | |
{ | |
$battles = $this->getDoctrine()->getRepository('AppBundle:Battle') | |
->findBy(['programmer' => $programmer]); | |
... lines 158 - 159 | |
} | |
} |
What now? Why not a simple return? return $this->createApiResponse()
and pass it $battles
:
... lines 1 - 22 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 25 - 153 | |
public function battlesListAction(Programmer $programmer) | |
{ | |
$battles = $this->getDoctrine()->getRepository('AppBundle:Battle') | |
->findBy(['programmer' => $programmer]); | |
return $this->createApiResponse($battles); | |
} | |
} |
Right? Is it really that simple?
Well, let's find out! Go back to ProgrammerControllerTest
, copy the new method name and run:
./vendor/bin/phpunit --filter testFollowProgrammerBattlesLink
OK, cool - check out how this looks: it's a big JSON array that holds a bunch of JSON battle objects. At first glance, it's great! But there's a problem? It's totally inconsistent with our other endpoint that returns a collection of programmers.
Scroll down a little to testProgrammersCollection()
. Here: we expect an items
key with the resources inside of it:
... lines 1 - 7 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 10 - 105 | |
public function testGETProgrammersCollection() | |
{ | |
... lines 108 - 120 | |
$this->asserter()->assertResponsePropertyIsArray($response, 'items'); | |
$this->asserter()->assertResponsePropertyCount($response, 'items', 2); | |
$this->asserter()->assertResponsePropertyEquals($response, 'items[1].nickname', 'CowboyCoder'); | |
} | |
... lines 125 - 315 | |
} |
We're also missing the pagination fields, making it harder for our API clients to guess how our responses will look.
Nope, we can do better, guys.
"Houston: no signs of life"
Start the conversation!
// 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
}
}