Tag Archives: silex

Adding PHPDocumentor (Sami) via Composer

In a previous article I demonstrated setting up a Silex project via composer. In addition to setting up PHPUnit I also mentioned how to get Codesniffer working to PSR2 coding standards. I figured a nice addition to that would be setting up PHPDocumentor.

Note: Using a document generator infers that you need documentation. If you’ve already got code and no documentation then it’s probably worth highlighting that some would say you are doing something wrong. Arguably, you should always have some documentation explaining what the API endpoints need to be, and what they should return. For the purposes of this article I shall assume that you’re like me – and want to create some pretty doucmentation automagically.

I’ve actually opted to use Sami over phpDocumentor2 – for reasons that phpDocumentor seemed to not fully support composer when Silex was installed (conflicting dependencies). There is also the addition that Sami is from Fabien Potencier of Sensio Labs, who wrote Silex.

Getting Started

Okay, so let’s dive right in – I’m assuming you’ve already got the application running from my previous article mentioned above.

Edit your composer.json:

{
    "minimum-stability": "dev",
    "require": {
        "silex/silex": "1.0.*@dev"
    },
    "autoload": {
        "psr-0": {"DVO": "src/"}
    },
    "require-dev": {
        "phpunit/phpunit": "3.7.*",
        "squizlabs/php_codesniffer": "1.*",
        "sami/sami" : "*",
    }
}

Now give composer a little nudge:

$ composer update --dev

Very quickly you can verify that Sami has been installed correctly:

$ ./vendor/bin/sami.php

If that spits out some useful help information then you’re onto a winner. The next step is to create a config file; I’ve gone for a basic implemtation just to get it up and running.

Create a config directory and create a new file called sami.php with the following:

<?php

return new Sami\Sami(__DIR__.'/../src/', array(
    'build_dir' => __DIR__.'/../build/sami/documentation',
    'cache_dir' => __DIR__.'/../build/sami/cache',
));

Give it another whirl:

$ ./vendor/bin/sami.php update config/sami.php

How easy was that? You will see it has dumped a bunch of stuff in the build/sami folder. You can easily browse the documentation and/or setup a vhost to share.

Nice and simple. Let me know if you have any thoughts/feedback.

Bobby (@bobbyjason)

Share

Creating a RESTful API with Silex using Composer and PHPUnit

Recently I’ve been playing around with Silex largely due to wanting to experiment with the various frameworks out there but also because I wanted to have a play around with composer. Previously I’ve used either Zend Framework or Kohana for my projects and whilst they don’t prevent you from using composer, they weren’t built with it in mind. For dependancy injection I’ve been using Sensio Labs’ Pimple – which I find to be awesome. Soon, I want to look at Symfony2 so I figured playing around with Silex (also by Sensio Labs) would be a great per-cursor; it uses all the same mod-cons and introduces the Symfony framework gradually.

So, where do we begin? Well I want to code from my MacBook to begin with – write some code and some tests. If I get that far, we can look at using My dev server to run Apache!

I’m assuming you’ve got a fresh install of Mountain Lion – so open up the terminal and smash in the following command:

sudo php /usr/lib/php/install-pear-nozlib.phar

Now, edit your php.ini and add the following:

zend_extension=/usr/lib/php/extensions/no-debug-non-zts-20090626/xdebug.so
include_path=.:/usr/lib/php/pear
memory_limit=512M
date.timezone=UTC
detect_unicode = Off

Hey presto, you now have PHP setup on your Mac. Does it work?

$ php -v

All ok? Good. Now lets install composer; the following is a good guide:

http://getcomposer.org/download/

…or just run this in your terminal to get the latest Composer version:

$ curl -s https://getcomposer.org/installer | php
$ sudo mv composer.phar /usr/local/bin/composer

Go to your working directory (/code/projectname), create composer.json and add the following:

{
    "minimum-stability": "dev",
    "require": {
        "silex/silex": "1.0.*@dev"
    },
    "autoload": {
        "psr-0": {"DVO": "src/"}
    },
    "require-dev": {
        "phpunit/phpunit": "3.7.*",
        "squizlabs/php_codesniffer": "1.*"
    }
}

Here we are specifying that phpunit and Codesniffer should only be installed hen the –dev flag is specified. So let’s go ahead and do just that:

$ composer install --dev

First things first you will notice we now have a vendor folder – this is where composer puts all our external libraries. If you want to add this project to a Git repository, make sure you add the vendor folder to the .gitignore file. You can see below I’ve added a few other items too.

.gitignore:

*.lock
vendor/
build/
.DS_Store

Okay, let’s create our directory structure:

/code
    /projectname
        /app
        /src
        /vendor
        /web

Open up web/index.php with the following:

<?php

use Symfony\Component\HttpFoundation\Request;

$app = require_once __DIR__.'/../app/app.php';

if (count($argv) > 0) {
    list($_, $method, $path) = $argv;
    $request = Request::create($path, $method);
    $app->run($request);    
} else {
    $app->run();    
}

This allows us to run our code via the CLI – we don’t want to have to get Apache involved just yet.

Now create app.php and bootstrap.php and put these in the app directory.

app/app.php:

<?php

require_once __DIR__.'/bootstrap.php';

use Silex\Application;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$app = new Application();
$app['debug'] = true;

// example route - kinda obvious what this does :)
$app->get('/hello/{name}', function ($name) use ($app) {
    return 'Hello '.$app->escape($name);
});

return $app;

bootstrap.php:

<?php

require_once __DIR__.'/../vendor/autoload.php';

Done! You can now go to your console and type the following command:

$ php web/index.php GET /hello/yourname

That’s all we need to call Silex, but as you can see it doesn’t do a great deal. To sort out our API and pull back some data we will now call some of our own library code. In here we will have the controllers and entities. The controllers are to handle our CRUD requests and the Entity stuff is where I’ve used a Factory and Gateway to access the data.

src/DVO/Controller/VoucherController.php:

<?php

namespace DVO\Controller;

use DVO\Entity\Voucher;
use DVO\Entity\Voucher\VoucherFactory;
use Symfony\Component\HttpFoundation\JsonResponse;

class VoucherController
{
    protected $_factory;

    /**
     * VoucherController constructor
     *
     * @param VoucherFactory $factory The voucher factory
     * 
     * @return void
     * @author
     **/
    public function __construct(VoucherFactory $factory)
    {
        $this->_factory = $factory;
    }

    public function indexJsonAction()
    {
        $vouchers = $this->_factory->getVouchers();
        $vouchers = array_map(function($voucher) {
            $vc = array();
            $vc['code'] = $voucher->getCode();

            return $vc;
        }, $vouchers);
        return new JsonResponse($vouchers);
    }

    public function createJsonAction()
    {
    }

    public function updateJsonAction()
    {
    }

    public function deleteJsonAction()
    {
    }
}

src/DVO/Entity/Voucher/VoucherFactory.php:

<?php

namespace DVO\Entity\Voucher;

use DVO\Cache;


class VoucherFactory
{
    protected $_gateway;
    protected $_cache;

    /**
     * VoucherFactory constructor
     *
     * @param VoucherGateway $gateway The voucher gateway
     * @param Cache          $cache   The cache
     * 
     * @return void
     * @author
     **/
    public function __construct(VoucherGateway $gateway, Cache $cache)
    {   
        $this->_gateway = $gateway;
        $this->_cache   = $cache;
    }

    /**
     * Creates the Voucher
     *
     * @return void
     * @author 
     **/
    public static function create()
    {
        return new \DVO\Entity\Voucher;
    }

    /**
     * Gets the vouchers
     *
     * @return void
     * @author 
     **/
    public function getVouchers()
    {
        $vouchers = array_map(function($voucher) {
            $vc = VoucherFactory::create();
            foreach ($voucher as $key => $value) {
                $vc->$key = $value;
            }

            return $vc;
        }, $this->_gateway->getAllVouchers());

        return $vouchers;
    }
}

src/DVO/Entity/Voucher/VoucherGateway.php:

<?php

namespace DVO\Entity\Voucher;

class VoucherGateway
{   
    /**
     * Get vouchers
     *
     * @return void
     * @author 
     **/
    public function getAllVouchers()
    {
        return array(array('code' => 'OFFER999'));
    }
}

src/DVO/Entity/Voucher.php:

<?php

namespace DVO\Entity;

/**
 * Voucher
 *
 * @package default
 * @author 
 **/
class Voucher extends EntityAbstract
{
    public function __construct()
    {
        $this->_data = array(
            'id'   => '',
            'code' => ''
            );
    }
}

src/DVO/Entity/EntityAbstract.php:

<?php

namespace DVO\Entity;

/**
 * Abstract Entity
 *
 * @package default
 * @author 
 **/
abstract class EntityAbstract
{
    protected $_data;
    /**
     * Magic function to capture getters & setters
     *
     * @param string $name      the name of the function
     * @param array  $arguments an array of arguments
     *
     * @return void
     */
    public function __call($name, array $arguments)
    {
        $type     = substr($name, 0, 3);
        $variable = strtolower(substr($name, 3));
        switch ($type) {
            case 'get':
                return $this->$variable;
            break;
            case 'set':
                $this->$variable = $arguments[0];
            break;
            default:
                return $this->invalid($type);
            break;
        }
    }

    public function getData()
    {
        return $this->_data;
    }

    /**
     * Magic function to capture getters
     *
     * @param string $name name of the variable
     *
     * @return mixed
     */
    public function __get($name)
    {   
        if (true === array_key_exists($name, $this->_data)) {
            return $this->_data[$name];
        } else {
            throw new Exception('Param ' . $name . ' not found in ' . get_called_class());
        }
    }

    /**
     * Magic function to capture setters
     *
     * @param string $name  the name of the var
     * @param string $value the value for the var
     *
     * @return void
     */
    public function __set($name, $value)
    {
        if (true === array_key_exists($name, $this->_data)) {
            $this->_data[$name] = $value;
        } else {
            throw new Exception('Param ' . $name . ' not found in ' . get_called_class());
        }
    }

    /**
     * called when invalid function is called
     *
     * @return boolean
     **/
    public function invalid($type)
    {
        throw new Exception('Error: Invalid handler in ' . get_called_class());
    }
}

src/DVO/Entity/Exception.php:

<?php

namespace DVO\Entity;

class Exception extends \Exception
{

}

src/DVO/Cache.php:

<?php

namespace DVO;

/**
 * Cache Class
 *
 * @package Cache Class
 * @author 
 **/
class Cache
{
}

Now we’ve added those lovely additional files, you can modify app/app.php:

<?php

require_once __DIR__.'/bootstrap.php';

use Silex\Application;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$app = new Application();
$app['debug'] = true;

// example route - kinda obvious what this does :)
$app->get('/hello/{name}', function ($name) use ($app) {
    return 'Hello '.$app->escape($name);
});

// setup the app cache
$app['cache'] = $app->share(function(){
    return new DVO\Cache;
});

// setup the voucher gateway
$app['vouchers.gateway'] = $app->share(function() {
    return new DVO\Entity\Voucher\VoucherGateway;
});

// setup the voucher factory
$app['vouchers.factory'] = $app->share(function() use($app) {
    return new DVO\Entity\Voucher\VoucherFactory($app['vouchers.gateway'], $app['cache']);
});

// setup the voucher controller
$app['vouchers.controller'] = $app->share(function() use ($app) {
    return new DVO\Controller\VoucherController($app['vouchers.factory']);
});

$app->register(new Silex\Provider\ServiceControllerServiceProvider());

$app->get('/vouchers', "vouchers.controller:indexJsonAction");
$app->post('/vouchers', "vouchers.controller:createJsonAction");
$app->put('/vouchers', "vouchers.controller:updateJsonAction");
$app->delete('/vouchers', "vouchers.controller:deleteJsonAction");

return $app;

Give it a whirl:

$ php web/index.php GET /vouchers

I’m not going to go into explaining about Gateway and Factory design patterns but I’m sure the code above is in a very simple enough form for you to understand. Some folks absoutely hate magic methods – but don’t forget that the EntityAbstract class is just what is says it is, an Abstract class. If a particular entity requires it’s own getter then go ahead and create it.

If you’ve gotten this far and it’s working then you’ve done well – good work! The next step is to use PHPUnit to test all of our beautiful code. For this we can setup PHPUnit.

Copy the below and put it in app/phpunit.xml:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false"
         syntaxCheck="false"
         bootstrap="bootstrap.php">
    <testsuites>
        <testsuite name="YourApp Test Suite">
            <directory>../tests/</directory>
        </testsuite>
    </testsuites>

    <logging>
        <log type="coverage-html" target="../build/coverage" title="PHP_CodeCoverage"
        charset="UTF-8" yui="true" highlight="true"
        lowUpperBound="35" highLowerBound="70"/>
        <log type="coverage-clover" target="../build/logs/clover.xml"/>
        <log type="junit" target="../build/logs/junit.xml" logIncompleteSkipped="false"/>
    </logging>

    <filter>
        <whitelist addUncoveredFilesFromWhitelist="true">
        <directory suffix=".php">../src</directory>
        <exclude>
            <file>../src/DVO/Entity/Voucher/VoucherGateway.php</file>
        </exclude>
        </whitelist>
    </filter>

    <php>
        <server name="APP_DIR" value="/code/projectname/app" />
        <env name="env" value="test" />
    </php>
</phpunit>

I’ve set the phpunit.xml file to dump the code coverage logs to the build directory. It’s good to set up a vhost to point to this folder so you can view your code coverage.

Here, I’m just doing a very basic test just to show you how you can get one running.

tests/DVO/CacheTest.php:

<?php

/**
 * Cache Test
 *
 * @package DVO
 * @author 
 **/
class CacheTest extends \PHPUnit_Framework_TestCase
{
    /**
     * Cache is just an empty class atm!
     *
     * @return void
     * @author 
     **/
    public function testCache() {
        $cache = new \DVO\Cache;
        $this->assertInstanceOf('\DVO\Cache', $cache);
    }
}

Run the tests:

$ ./vendor/bin/phpunit -c app/phpunit.xml

I’ve not put all the tests in the tutorial – but you can find them in the repo on GitHub (https://github.com/posmena/voucherapi) – feel free to follow Posmena on GitHub!

So, you’ve used the above code and your tests pass? Maybe now you want to make sure you code is PSR2 compliant – a very popular coding standard. We already installed Codesniffer via composer earlier, so all you need to do to check your code against code sniffer:

$ ./vendor/bin/phpcs --config-set default_standard PSR2
$ ./vendor/bin/phpcs src

There’s a very good chance (certainty) that the above code will not pass PSR2 standards. I figured I’d leave a few little things for you to research. I won’t be too cruel though; PHP Closures don’t seem to be PSR2 compliant so you need to wrap them with the @codingStandardsIgnoreStart and @codingStandardsIgnoreEnd tags. Once your code is passing Codesniffer, you will simply not get any errors. Checkout the CodeSniffer options page for config options.

I hope you’ve enjoyed this article. I’m open to any comments/suggestions you may have!

Bobby (@bobbyjason)

Thanks to:

Share