November 10, 2012 . . Comments
Tags:


Getting Started with REST and Zend Framework 2


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 updated module.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 the AlbumTable 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.

 

Comments

Please feel free to leave any comments as long as they're related to the topic.