Zend\Tool providers in ZF2 (dev1)

27th September 2010

I've started playing with the development versions of ZF 2.0 and one of the first things I thought I'd do was to port Akrabat_Db_Schema_Manager. It turned out to be reasonably easy.

All I needed to do was rework my use of ZF components to use the new ZF2 ones. Whilst I was at it, I also converted it to use namespaces. I also had to reorganise the http://github.com/akrabat/Akrabat library so that I could have ZF1 and ZF2 code in it.

The DatabaseSchemaProvider

Before:

<?php

class Akrabat_Tool_DatabaseSchemaProvider extends Zend_Tool_Project_Provider_Abstract
{
    //etc

After:

<?php

namespace Akrabat\Tool;

class DatabaseSchemaProvider extends \Zend\Tool\Project\Provider\AbstractProvider
{
    //etc

The filename, DatabaseSchemaProvider.php, is the same and the file lives in the Akrabat/Tool directory as before.

You can see the class now extends from \Zend\Tool\Project\Provider\AbstractProvider. This shows one of the consequences of moving to namespaces: with a one to one mapping, we would have ended up with a class called Abstract which isn't allowed, so the classname has been changed to AbstractProvider. There are a fair few class name changes like this throughout ZF2, so expect to do a bit of file browsing :)

The rest of the changes that I had to make are exactly the same type namespace conversions and that was all I had to do. Maybe when the autoloader is updated, then more changes will be required and if so, I'll no doubt write it up!

The new Akrabat\Db\Schema\Manager is available on my github account. The readme contains instructions on how to set it up with ZF2 too.

Setting up ZF2's Zend\Tool side by side with ZF1

20th September 2010

If you want to play with the development versions of Zend Framework 2.0, then it's handy to be able to create ZF2 projects using the Zend\Tool command line tool.

Rather unhelpfully, ZF2's Zend\Tool uses the same ini file (~/.zf.ini) as ZF1's Zend_Tool and the same zf.sh script filename, so you can't just put zf2 on to your path and it'll all just work.

I am assuming that you're like me and have production sites using ZF1, so you probably don't want to mess up your current zf.sh usage. This is how I implemented side-by-side ZF cli scripts.

1. Install ZF2 somewhere

I like to install in /usr/local/include. From the command line type:

cd /usr/local/include
git clone git://git.zendframework.com/zf.git zf2

Don't forget to periodically update it with:

cd /usr/local/include
git pull origin master

(And don't be surprised when it breaks whatever you've already coded in ZF2.dev!)

2. Create .zf2.ini

You need an ini file for ZF2, so call it .zf2.ini and store it in your home directory next to .zf.ini. You need to set the correct include path so that Zend\Tool's zf.php can find your ZF2 installation. From the command line type:

echo 'php.include_path = "/usr/local/include/zf2/library:/usr/local/include/Akrabat/zf2/"' >> ~/.zf2.ini

This creates the .zf2.ini file with the correct include_path set up.

3. Create a zf2 alias

Update your ~/.bash_profile to set up an alias to the ZF2 zf.sh script.

Using a text edit, add this line to the end of ~/.bash_profile:

alias zf2='export ZF_CONFIG_FILE=~/.zf2.ini; /usr/local/include/zf2/bin/zf.sh'

Restart your terminal or type source ~/.bash_profile

Now you can type zf2 to run ZF2's zf.sh and Zend\Tool will run and not be affected by any ZF1 configurations you may have!

ZF1 and ZF2's cli scripts both work!

TweetGT: an example of Zend_Service_Twitter via OAuth

13th September 2010

TweetGT is a simple application that talks to Twitter. I wrote it as I couldn't find another way to send a geotagged tweet sent from an arbitrary location.

Screenshot of tweetgt.funkymongoose.com

Also, my friend Cal Evans says that writing a Twitter app is the new Hello World, so I thought I'd better find out how to do it! Obviously, I used Zend Framework :)

The source is up on github so you can have a look at the entire project. The section I want to concentrate on in this post is the Twitter OAuth integration, which was added to Zend_Service_Twitter in version 1.10.6.

To implement my Twitter integration, I used a model, Application_Model_Twitter, which has a protected member variable to an instance of Zend_Service_Twitter. This means that the rest of the application has to go through the model to get to the service and so in principle at least, I get to control access.

OAuth integration requires that we get an access token from twitter. The basic process is that we hand off from our website to Twitter, who then call back to a URL on our site once the user has logged in.

Login: Redirect to Twitter

The loginAction looks like this:


    public function loginAction()
    {
        $twitter $this->_helper->twitter(); /* @var $twitter Application_Model_Twitter */

        // We need the request token for use in the callback when the user is
        // redirected back here from Twitter after authenticating
        $session = new Zend_Session_Namespace();
        $session->requestToken $twitter->getRequestToken();

        // redirect to the Twitter website
        $twitter->loginViaTwitterSite();
    }

Three things going on here. Firstly, I have set up an action helper to retrieve an instance of my model for me. This is mainly for ease of use as there's a bit of configuration required, so having it centralised saves having to duplicate code. There's other ways of doing this of course, but a action helper suited me this time :)

When we hand over to Twitter, we send a request token over too. We will need the request token in the call back so we store to the session ready for use after the user has authenticated on Twitter's site.

Finally we do the redirect to Twitter's site via a model method, loginViaTwitterSite, which simply proxies to the redirect method within Zend_Service_Twitter's OAuth consumer.

Instantiating the model

The model is instantiated within a controller action helper. To log in to twitter using OAuth we need a consumer key and a consumer secret that are available from Twitter on a per-application basis. I've chosen to store these in the application.ini file. We also need to configure Zend_Service_Twitter with the callback URL to use and, if we are logged in, the username and access token from Twitter. This action helper does all that for us and looks like this:


class Application_Controller_Helper_Twitter extends Zend_Controller_Action_Helper_Abstract
{
    /**
     * @var Application_Model_Twitter
     */
    protected $_twitter;

    public function direct()
    {
        if (!$this->_twitter) {
            $controller $this->getActionController();

            $config = array();

            $session = new Zend_Session_Namespace();
            if ($session->accessToken) {
                $token $session->accessToken;
                $config['username'] = $token->screen_name;
                $config['accessToken'] = $token;
            }
            
            $options $controller->getInvokeArg('bootstrap')->getOptions();
            $config['consumerKey'] = $options['twitter']['consumerKey'];
            $config['consumerSecret'] = $options['twitter']['consumerSecret'];

            $request $controller->getRequest();
            $url $request->getScheme() . '://' .  $request->getHttpHost() . $request->getBaseUrl();
            $config['callbackUrl'] = $url '/callback';

            $this->_twitter = new Application_Model_Twitter($config);
        }

        return $this->_twitter;
    }

}

I didn't want my model interacting with sessions or the bootstrap options, so I used an action controller. I could equally have used a service layer object, or instantiated the model in the bootstrap or in a Front Controller plugin. The most important thing is that I only deal with the config of sorting out the config array that Zend_Service_Twitter needs once.

Callback

I have chosen /callback as the URL for Twitter to use. The easiest way to set this up is to have in indexAction() within a CallbackController class. This code will use the model's twitter service to retrieve the access token and store it to the session.

It looks like this:

class CallbackController extends Zend_Controller_Action
{
    public function indexAction()
    {
        $session = new Zend_Session_Namespace();
        if (!empty($this->getRequest()->getQuery()) && isset($session->requestToken)) {

            // Get the model instance from the action helper
            $twitter $this->_helper->twitter(); /* @var $twitter Application_Model_Twitter */

            // turn the request token into an access token
            $accessToken $twitter->getAccessToken($this->getRequest()->getQuery(),
                    $session->requestToken);

            // store the access token
            $session->accessToken $accessToken;

            // we don't need the request token any more
            unset($session->requestToken);

            // redirect back to home page
            $this->_helper->redirector('index''index');
        } else {
            throw new Zend_Exception('Invalid callback request. Oops. Sorry.');
        }
    }

}

The code should be fairly self-explanatory with the inline comments to help :)

That's it. We are now logged into Twitter and back on our own website able to do whatever we want to do :) Have a look at the source to see the rest of the details of how it all fits together.

Unit testing controller actions with Zend_Test_PHPUnit_ControllerTestCase

6th September 2010

Testing controllers has traditionally been a hassle due to the requirements of setting up the bootstrap, the front controller and initiating the dispatch cycle. In June, Matthew addressed this with the release of Zend_Test_PHPUnit_ControllerTestCase way back in 2008.

Later, Matthew helpfully wrote an article on how to use it and I have used that as a starting point for the information here. (Thanks Matthew!)

The project I'm using is TodoIt, which is a simple ZF demo application, which needs unit tests.

Setting up PHPUnit

All your unit tests will live in the /tests folder. The ZF cli tool will create a phpunit.xml file for you, but you'll discover that it's empty! This is what it should look like:


<phpunit colors="true" bootstrap="./TestHelper.php">
    <testsuite name="TodoIt Test Suite">
        <directory>./</directory>
    </testsuite>

    <filter>
        <whitelist>
            <directory suffix=".php">../library/</directory>
            <directory suffix=".php">../application/</directory>
            <exclude>
                <directory suffix=".phtml">../application/</directory>
            </exclude>
        </whitelist>
    </filter>

    <logging>
        <log highlowerbound="80" lowupperbound="50" highlight="true" yui="true" charset="UTF-8" target="./log/report" type="coverage-html"></log>
    </logging>
 
</phpunit>

This file is used to configure phpunit itself and saves having to use command line options. As it's XML, it's fairly easy to read. The testsuites element is used to specify the testsuite we're going to test. In principle you can have many test suites; in this case, one is enough! The filter section is used to specify which files to use for code coverage reporting and the logging section is used to configure the reports.

We also specify TestHelper.php as the bootstrap. This mean that it is called for us and contains the necessary PHP setup we need to do in order to load and use Zend Framework. In effect TestHelper.php acts like public/index.php does for your web application. TestHelper.php looks like this:

<?php
// Based on http://weierophinney.net/matthew/archives/190-Setting-up-your-Zend_Test-test-suites.html

// PHP settings
error_reporting(E_ALL E_STRICT);
date_default_timezone_set('Europe/London');

define('APPLICATION_ENV''unittesting');
define('APPLICATION_PATH'realpath(dirname(__FILE__) . '/../application'));

// Directories for include path
$root realpath(dirname(__FILE__) . '/../');
$library $root '/library';
$models $root '/application/models';

$path = array(
    $library,
    $models,
    get_include_path()
);
set_include_path(implode(PATH_SEPARATOR$path));

require_once 'Zend/Loader/Autoloader.php';
Zend_Loader_Autoloader::getInstance();

// Unset global variables
unset($root$library$models$path);

As with public/index.php, we set APPLICATION_ENV and APPLICATION_PATH, update the include_path and then set up the autoloader. Now we're all ready to write some tests!

A controller test class

I place my controller test classes in tests/application/controllers to make them easy to find. (Model tests go in tests/application/models!). The TodoIt application has a login form in AuthController::indexAction() which is accessed via the /auth URL. We'll start by testing this form is displayed.

The controller's test class is called AuthControllerTest and lives in tests/application/controllers/AuthControllerTest.php:

<?php

// Call AuthControllerTest::main() if this source file is executed directly.
if (!defined("PHPUnit_MAIN_METHOD")) {
    define("PHPUnit_MAIN_METHOD""AuthControllerTest::main");
}

require_once 'PHPUnit/Framework/TestCase.php';

/**
 * @group Controllers
 */
class AuthControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{
    public static function main()
    {
        $suite  = new PHPUnit_Framework_TestSuite(get_class($this));
        $result PHPUnit_TextUI_TestRunner::run($suite);
    }
    
    public function setUp()
    {
        $application = new Zend_Application(
            APPLICATION_ENV,
            APPLICATION_PATH '/configs/application.ini'
        );

        $this->bootstrap $application;
        return parent::setUp();
    }

    public function tearDown()
    {
        /* Tear Down Routine */
    }

    public function testLoginDisplaysAForm()
    {
        $this->dispatch('/auth/index');
        $this->assertQueryContentContains('h1''Login');
        $this->assertQuery('form#login'); // id of form
    }
}

There's three things going on in here, so let's look at each in turn.

Firstly we set up the file to allow PHPUnit to run this file on it's own using the command line:

phpunit tests\application\controllers\AuthControllerTest.php

This is done by setting the PHPUnit_MAIN_METHOD constant to the static method AuthControllerTest::main(). The phpunit cli tool will then run this method which will in turn run this file as a test suite.

The methods setUp() and tearDown() are called before and after every test method and are used to ensure that we have a clean slate for each one. As we extended from Zend_Test_PHPUnit_ControllerTestCase rather than from PHPUnit_Framework_TestCase, we are able to leverage functionality specifically designed to make testing controllers easier. We use this in setUp() to set the property bootstrap to an instance of Zend_Application, which is then used in the tests themselves.

Each test is a method that starts with the word test, like this one:


    public function testLoginDisplaysAForm()
    {
        $this->dispatch('/auth/index');
        $this->assertResponseCode(200);
        $this->assertQueryContentContains('h1''Login');
        $this->assertQuery('form#login'); // id of form
    }

We start by calling dispatch() to run the correct action and then we use the various assert methods to check that the result is what we expect. The assertResponseCode method checks that we didn't error as the errorController will set the code to 500 or 404. We can then use the assertQuery methods to check what has been rendered to the response object. These use DOM paths to select a specific element. The call to assertQueryContentContains allows us to check the text within the H1 element is what we expect and the assertQuery just checks that the element is on the page.

That's it.

This is just the tip of the iceberg and I strongly suggest that you have a read of the documentation to see for yourself how many different assertions you can use to check that your code is performing as expected.

The Redirector action helper

30th August 2010

Following on from the discussion on the FlashMessenger action helper, I thought I'd also cover another supplied helper: Redirector.

Redirector does what it says on the tin and redirects the user to another page. I mostly use this when coming back from filling a form in, so that the user is then redirected to another page. In admin systems, this is usually a list page. On front end websites, this is usually a thank you page. Though for log-in forms, I tend to try and return the user to where they were going!

It's used in a controller action method like this:

$urlOptions = array('controller'=>'index''action'=>'index');
$this->_helper->redirector->gotoRoute($urlOptions);

gotoRoute() takes the same set of parameters are the url() view helper which is not a surprise as they both proxy through to the Front Controller's router object. It's handy though as one you know one, you know the other :)

If you are using the default route, then you can use gotoSimple(). For example to redirect to the news controller's list action, you would do:

$this->_helper->redirector->gotoSimple('list''news');

The gotoSimple() method signature is:

gotoSimple($action$controller null$module null, array $params = array());

As you can see, it provides defaults for the controller and module and params parameters so you only need to set them if you need to. This works well for admin system as I tend to be redirecting within the same controller (from the edit or delete action to index, usually).

You can also use the Redirector with an absolute URL, by using the gotoUrl() method:

$url 'http://www.akrabat.com';
$this->_helper->redirector->gotoUrl($url);

I tend to use this one much less frequently - so infrequently, that I can't think of a use-case off the top of my head :)

By default, Redirector sets a 302 status code, however you can also set a 301 if you want to:

$this->_helpers->redirector->setCode(301);

There are a few other options that can be set like setExit() and setUseAbsoluteUri(), but to be honest, I don't think I've ever used them!

I find that I use Redirector fairly frequently as its gotoRoute() uses the same parameters as url() which makes it easy to remember how to use it. Like url(), it also benefits from remembering which route was used to get you to the current page and reuses that when creating the next one which is handy.