Today i want to show you how to build a rest application. This tutorials assume you have completed the Getting Started. I will be repeating lot of the steps allready explained in there. There is also a sample Album module which you can install from here.
Setting up the AlbumRest module
Start by creating a directory called AlbumRest
under module with the following subdirectories to hold the module’s files:
zf2-tutorial/ /module /AlbumRest /config /src /AlbumRest /Controller /test
Create Module.php
in the AlbumRest module at zf2-tutorial/module/AlbumRest
:
<?php namespace AlbumRest; class Module { public function getAutoloaderConfig() { return array( 'Zend\Loader\ClassMapAutoloader' => array( __DIR__ . '/autoload_classmap.php', ), 'Zend\Loader\StandardAutoloader' => array( 'namespaces' => array( __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__, ), ), ); } public function getConfig() { return include __DIR__ . '/config/module.config.php'; } }
Configuration
Create a file called module.config.php
under zf2-tutorial/module/AlbumRest/config
:
<?php return array( 'controllers' => array( 'invokables' => array( 'AlbumRest\Controller\AlbumRest' => 'AlbumRest\Controller\AlbumRestController', ), ), 'view_manager' => array( 'template_path_stack' => array( 'album-rest' => __DIR__ . '/../view', ), ), );
As we are in development, we don’t need to load files via the classmap, so we provide an empty array for the classmap autoloader. Create a file called autoload_classmap.php
under zf2-tutorial/module/AlbumRest
:
<?php return array();
Informing the application about our new module
We now need to tell the ModuleManager that this new module exists. This is done in the application’s config/application.config.php
file which is provided by the skeleton application. Update this file so that its modules section contains the AlbumRest module as well, so the file now looks like this:
(Changes required are highlighted using comments.)
<?php return array( 'modules' => array( 'Application', 'Album', 'AlbumRest', // <-- Add this line ), 'module_listener_options' => array( 'config_glob_paths' => array( 'config/autoload/{,*.}{global,local}.php', ), 'module_paths' => array( './module', './vendor', ), ), );
As you can see, we have added our AlbumRest
module into the list of modules after the Album
module.
We have now set up the module ready for putting our custom code into it.
Setup Rest Routing
We need to first add our custom REST routing so we are able to call the RestController. This is the updatedmodule.config.php
with the new code highlighted.
<?php return array( 'controllers' => array( 'invokables' => array( 'AlbumRest\Controller\AlbumRest' => 'AlbumRest\Controller\AlbumRestController', ), ), // The following section is new and should be added to your file 'router' => array( 'routes' => array( 'album-rest' => array( 'type' => 'segment', 'options' => array( 'route' => '/album-rest[/:id]', 'constraints' => array( 'id' => '[0-9]+', ), 'defaults' => array( 'controller' => 'AlbumRest\Controller\AlbumRest', ), ), ), ), ), 'view_manager' => array( 'template_path_stack' => array( 'album-rest' => __DIR__ . '/../view', ), ), );
The name of the route is album-rest
and has a type of segment
. For a RestController we must provide a placeholder in this case the route is /album-rest/id
which will match any URL that starts with /album-rest
. The next segment will be an optional id which is required for the RestController The constraints section allows us to ensure that the characters within a segment are as expected.
Setup View Strategy
We add the view strategy to our config at zf2-tutorial/module/Albumrest/config/module.config.php
<?php return array( 'controllers' => array( 'invokables' => array( 'AlbumRest\Controller\AlbumRest' => 'AlbumRest\Controller\AlbumRestController', ), ), // The following section is new` and should be added to your file 'router' => array( 'routes' => array( 'album-rest' => array( 'type' => 'Segment', 'options' => array( 'route' => '/album-rest[/:id]', 'constraints' => array( 'id' => '[0-9]+', ), 'defaults' => array( 'controller' => 'AlbumRest\Controller\AlbumRest', ), ), ), ), ), 'view_manager' => array( //Add this config 'strategies' => array( 'ViewJsonStrategy', ), ),
Create the controller
Let’s go ahead and create our controller class AlbumRestController.php
at zf2-tutorials/module/AlbumRest/src/AlbumRest/Controller
:
<?php namespace AlbumRest\Controller; use Zend\Mvc\Controller\AbstractRestfulController; use Album\Model\Album; use Album\Form\AlbumForm; use Album\Model\AlbumTable; use Zend\View\Model\JsonModel; class AlbumRestController extends AbstractRestfulController { public function getList() { # code... } public function get($id) { # code... } public function create($data) { # code... } public function update($id, $data) { # code... } public function delete($id) { # code... } }
We have now set up the controller methods to map the HTTP request methods. You can find a detailed explanation of the methods in the Manual
Write the tests
Our AlbumRest controller doesn’t do much yet, so it should be easy to test.
Create the follwing subdirectories:
zf2-tutorial/ /module /AlbumRest /test /AlbumRestTest /Controller
Add the 3 files as described in Unit Testing to module/AlbumRest/test
- Bootstrap.php
- phpunit.xml.dist
- TestConfig.php.dist
Remember here to change the namespace
in Bootstrap.php
and change the the TestConfig.php.dist
to following:
<?php return array( 'modules' => array( 'Album', 'AlbumRest' ), 'module_listener_options' => array( 'config_glob_paths' => array( '../../../config/autoload/{,*.}{global,local}.php', ), 'module_paths' => array( 'module', 'vendor', ), ), );
In phpunit.xml
change the directory to point at AlbumRestTest
Create zf2-tutorial/Album/module/AlbumRest/test/AlbumRestTest/Controller/AlbumRestControllerTest.php
with the following contents:
<?php namespace AlbumRestTest\Controller; use AlbumRestTest\Bootstrap; use AlbumRest\Controller\AlbumRestController; use Zend\Http\Request; use Zend\Http\Response; use Zend\Mvc\MvcEvent; use Zend\Mvc\Router\RouteMatch; use Zend\Mvc\Router\Http\TreeRouteStack as HttpRouter; use PHPUnit_Framework_TestCase; class AlbumRestControllerTest extends PHPUnit_Framework_TestCase { protected $controller; protected $request; protected $response; protected $routeMatch; protected $event; protected function setUp() { $serviceManager = Bootstrap::getServiceManager(); $this->controller = new AlbumRestController(); $this->request = new Request(); $this->routeMatch = new RouteMatch(array('controller' => 'index')); $this->event = new MvcEvent(); $config = $serviceManager->get('Config'); $routerConfig = isset($config['router']) ? $config['router'] : array(); $router = HttpRouter::factory($routerConfig); $this->event->setRouter($router); $this->event->setRouteMatch($this->routeMatch); $this->controller->setEvent($this->event); $this->controller->setServiceLocator($serviceManager); } public function testGetListCanBeAccessed() { $result = $this->controller->dispatch($this->request); $response = $this->controller->getResponse(); $this->assertEquals(200, $response->getStatusCode()); } public function testGetCanBeAccessed() { $this->routeMatch->setParam('id', '1'); $result = $this->controller->dispatch($this->request); $response = $this->controller->getResponse(); $this->assertEquals(200, $response->getStatusCode()); } public function testCreateCanBeAccessed() { $this->request->setMethod('post'); $this->request->getPost()->set('artist', 'foo'); $this->request->getPost()->set('title', 'bar'); $result = $this->controller->dispatch($this->request); $response = $this->controller->getResponse(); $this->assertEquals(200, $response->getStatusCode()); } public function testUpdateCanBeAccessed() { $this->routeMatch->setParam('id', '1'); $this->request->setMethod('put'); $result = $this->controller->dispatch($this->request); $response = $this->controller->getResponse(); $this->assertEquals(200, $response->getStatusCode()); } public function testDeleteCanBeAccessed() { $this->routeMatch->setParam('id', '1'); $this->request->setMethod('delete'); $result = $this->controller->dispatch($this->request); $response = $this->controller->getResponse(); $this->assertEquals(200, $response->getStatusCode()); } }
And execute phpunit
from module/AlbumRest/test
.
PHPUnit 3.7.8 by Sebastian Bergmann. ..... Time: 0 seconds, Memory: 5.25Mb OK (5 tests, 5 assertions)
We are going to consume services from the Album module. Let's start adding in some functionality. In our AlbumRestController.php
add:
public function getAlbumTable() { if (!$this->albumTable) { $sm = $this->getServiceLocator(); $this->albumTable = $sm->get('Album\Model\AlbumTable'); } return $this->albumTable; }
You should also add:
protected $albumTable;
Add this test to your AlbumControllerTest.php
:
public function testGetAlbumTableReturnsAnInstanceOfAlbumTable() { $this->assertInstanceOf('Album\Model\AlbumTable', $this->controller->getAlbumTable()); }
Listing albums
In order to list the albums, we need to retrieve them from the model and return a JsonModel. To do this, we fill in getList() within AlbumRestController
. Update the AlbumRestController’s getList() like this:
public function getList() { $results = $this->getAlbumTable()->fetchAll(); $data = array(); foreach($results as $result) { $data[] = $result; } return array('data' => $data); }
As we do not have any views for our Controller we need a method on how to test these. For this example i am using curl
to test the functionality.
$ curl -i -H "Accept: application/json" http://zf2-tutorial.localhost/album-rest HTTP/1.1 200 OK Date: Sat, 10 Nov 2012 19:36:03 GMT Server: Apache/2.2.22 (Ubuntu) X-Powered-By: PHP/5.4.8-1~precise+1 Content-Length: 320 Content-Type: application/json {"content":{"data":[{"id":"1","artist":"The Military Wives","title":"In My Dreams"},{"id":"2","artist":"Adele","title":"21"},{"id":"3","artist":"Bruce Springsteen","title":"Wrecking Ball (Deluxe)"},{"id":"4","artist":"Lana Del Rey","title":"Born To Die"},{"id":"5","artist":"Gotye","title":"Making Mirrors"}]}}
Adding Missing functionality
Let's add the rest of the functionality to AlbumRestController
.
Get Album
public function get($id) { $album = $this->getAlbumTable()->getAlbum($id); return array("data" => $album); }
And run curl to see the output.
$ curl -i -H "Accept: application/json" http://zf2-tutorial.localhost/album-rest/1 HTTP/1.1 200 OK Date: Sat, 10 Nov 2012 19:45:07 GMT Server: Apache/2.2.22 (Ubuntu) X-Powered-By: PHP/5.4.8-1~precise+1 Content-Length: 88 Content-Type: application/json {"content":{"data":{"id":"1","artist":"The Military Wives","title":"In My Dreams"}}}
Add Album
We need to modify theAlbumTable
in module/Album/src/Album/Model
to return the generated id
public function saveAlbum(Album $album) { $data = array( 'artist' => $album->artist, 'title' => $album->title, ); $id = (int)$album->id; if ($id == 0) { $this->tableGateway->insert($data); $id = $this->tableGateway->getLastInsertValue(); //Add this line } else { if ($this->getAlbum($id)) { $this->tableGateway->update($data, array('id' => $id)); } else { throw new \Exception('Form id does not exist'); } } return $id; // Add Return }
Modify the create method in module/AlbumRest/src/AlbumRest/Controller/AlbumRestController
as following:
public function create($data) { $form = new AlbumForm(); $album = new Album(); $form->setInputFilter($album->getInputFilter()); $form->setData($data); if ($form->isValid()) { $album->exchangeArray($form->getData()); $id = $this->getAlbumTable()->saveAlbum($album); } return new JsonModel(array( 'data' => $this->get($id), )); }
$ curl -i -H "Accept: application/json" -X POST -d "artist=AC DC&title=Dirty Deeds" http://zf2-tutorial.localhost/album-rest HTTP/1.1 200 OK Date: Sat, 10 Nov 2012 19:45:07 GMT Server: Apache/2.2.22 (Ubuntu) X-Powered-By: PHP/5.4.8-1~precise+1 Content-Length: 88 Content-Type: application/json {"content":{"data":{"id":"1","artist":"The Military Wives","title":"In My Dreams"}}}
Edit Album
public function update($id, $data) { $data['id'] = $id; $album = $this->getAlbumTable()->getAlbum($id); $form = new AlbumForm(); $form->bind($album); $form->setInputFilter($album->getInputFilter()); $form->setData($data); if ($form->isValid()) { $id = $this->getAlbumTable()->saveAlbum($form->getData()); } return new JsonModel(array( 'data' => $this->get($id), )); }
$ curl -i -H "Accept: application/json" -X PUT -d "artist=Ac-Dc&title=Dirty Deeds" http://zf2-tutorial.localhost/album-rest/1 HTTP/1.1 200 OK Date: Sun, 11 Nov 2012 01:25:11 GMT Server: Apache/2.2.22 (Ubuntu) X-Powered-By: PHP/5.4.8-1~precise+1 Content-Length: 70 Content-Type: application/json {"content":{"data":{"id":"1","artist":"Ac-Dc","title":"Dirty Deeds"}}}
Delete Album
public function delete($id) { $this->getAlbumTable()->deleteAlbum($id); return new JsonModel(array( 'data' => 'deleted', )); }
$ curl -i -H "Accept: application/json" -X DELETE http://modules.zendframework.com.dev/album-rest/7 HTTP/1.1 200 OK Date: Sun, 11 Nov 2012 01:28:43 GMT Server: Apache/2.2.22 (Ubuntu) X-Powered-By: PHP/5.4.8-1~precise+1 Content-Length: 30 Content-Type: application/json {"content":{"data":"deleted"}}
So now we have turned our Album into a Restfull Application. I wanted to implement a jGrid for this tutorial but i belive that would be suited for a new Module. Thanks for all the commments to upgrade/fix this blog.
The sample source code for this Module can be found here.