Jan 15 2012

Integrating Zend Framework 1 and Pimple

This post will describe a way to integrate Zend Framework 1 and Pimple. A complete working version of the code is available on github.

Thankfully, Zend Framework 2 features its own Dependency Injection Container. Happy days. Still, if you're not prepared to wait, you may find this useful.

What is Pimple?

Pimple is a small Dependency Injection Container for PHP 5.3 that consists of just one file and one class (about 50 lines of code).

If you aren't too familiar with Dependency Injection and why it's useful, check out these slides by Fabien Potencier.

Why bother?

It allows you to mock dependencies in your controllers within your controller tests. In this example, the Doctrine 2 entity manager is added into the container. If you're just after Zend Framework 1 and Doctrine 2 integration, you will certainly find this post from the Mayflower blog helpful.

Add a bootstrap resource plugin for the container

The bootstrap resource plugin in this example adds the entity manager closure into Pimple. The init method returns the container, thus it can be retrieved from within a controller.

More information on resource plugins

// library/Example/Application/Resource/Container.php
namespace Example\Application\Resource;

use Zend_Application_Resource_ResourceAbstract,
    Doctrine\ORM\EntityManager,
    Doctrine\ORM\Tools\Setup;

class Container extends Zend_Application_Resource_ResourceAbstract
{
    public $_explicitType = 'container';

    protected $container;

    public function __construct($options = null, $container = null)
    {
        parent::__construct($options);

        $this->container = $container;
    }

    public function init() 
    {
        $options = $this->getBootstrap()->getOptions();
        $db = $options['db'];

        $this->container['entityManager'] = $this->container->share(function ($c) use ($db) {
            $isDevMode = APPLICATION_ENV == 'development';

            $config = Setup::createAnnotationMetadataConfiguration(array(APPLICATION_PATH . '/models'), $isDevMode);

            return EntityManager::create($db, $config);
        });

        return $this->container;
    }
}

Resource plugins can be added from the application configuration. However, it's important that it is instantiated outside of the configuration to allow injection of the Pimple container. This is an extremely important step for testing.

// public/index.php

// ...

$resource = new \Example\Application\Resource\Container(null, new Pimple);
$application->getBootstrap()->registerPluginResource($resource);

$application->bootstrap()
            ->run();

Create the controller

Within the action, the container is retrieved from the bootstrap.

// application/controllers/IndexController.php
class IndexController extends Zend_Controller_Action
{
    public function indexAction()
    {
        $bootstrap = $this->getInvokeArg('bootstrap');
        $container = $bootstrap->getResource('container');

        $em = $container['entityManager'];

        $this->view->items = $em->getRepository('Application_Model_Item')->findAll();
    }
}

Add a test for the controller

I've used the PHPUnit mock library for the example. Mockery is better though. Didn't want to throw another library into the mix!

// tests/application/controllers/IndexController.php
class IndexControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{
    protected $container;

    protected function setUp()
    {
        $this->bootstrap = new Zend_Application(APPLICATION_ENV, APPLICATION_PATH . '/configs/application.ini');
        $this->container = new Pimple;
        $resource = new Example\Application\Resource\Container(null, $this->container);
        $this->bootstrap->getBootstrap()->registerPluginResource($resource);
        parent::setUp();
    }

    public function testIndexAction()
    {
        $items = array(
            $this->createItem('test1', 'test1value'),
            $this->createItem('test2', 'test2value'),
        );

        // mock the entity repository
        $repo = $this->getMockBuilder('Doctrine\ORM\EntityRepository')
            ->disableOriginalConstructor()
            ->getMock();

        $repo->expects($this->once())
            ->method('findAll')
            ->will($this->returnValue($items));

        // mock the entity manager
        $em = $this->getMockBuilder('Doctrine\ORM\EntityManager')
            ->disableOriginalConstructor()
            ->getMock();

        $em->expects($this->once())
            ->method('getRepository')
            ->with($this->equalTo('Application_Model_Item'))
            ->will($this->returnValue($repo));

        // replace the entity manager closure in the container
        $this->container['entityManager'] = $em;

        $this->dispatch('/');

        $this->assertQueryContentContains("h1", "Items");
        $this->assertQueryContentContains('ul li', 'test1: test1value');
        $this->assertQueryContentContains('ul li', 'test2: test2value');
    }

    protected function createItem($name, $value)
    {
        $item = new Application_Model_Item;
        $item->setName($name);
        $item->setValue($value);
        return $item;
    }
}

Improvements

  • Populate Pimple outside of the bootstrap resource plugin. This step allows you to have an empty container during testing (no forgetting to replace objects in the container).
  • Allow easier access to the container via an action helper or by extending Zend_Controller_Action.
Feb 05 2011

Redirecting from an old route in Zend Framework

Firstly ensure your old route has been created:

routes.new.route = "new"
routes.new.defaults.controller = index
routes.new.defaults.action = new
routes.new.defaults.module = default
routes.new.defaults.id = ""
routes.new.reqs.id = \d+

routes.old.route = "old"
routes.old.defaults.controller = index
routes.old.defaults.action = old
routes.old.defaults.module = default
routes.old.defaults.id = ""
routes.old.reqs.id = \d+

In the controller:

class IndexController extends Zend_Controller_Action 
{
    ...

    public function oldAction()
    {
        $redirector = $this->_helper->getHelper('Redirector');
        $redirector->setCode(301);
        return $redirector->gotoRoute($this->_getAllParams(), 'new');
    }
}

For reference, examples developed with Zend Framework 1.11.2

Feb 05 2011

Setting a default page title in Zend Framework and modifying per action

There may be a better way of doing this, I'm open to suggestions. Still, my goal was to have a default page title, manipulating when required in an action.

I initially tried to just use $this->headTitle('foobar'); in my layout and prepend in the controller action. This didn't work out too well, due to the order in which they're executed - append and prepend are reversed. Not too intuitive.

Therefore, I created an _init method in the Bootstrap class:

public function _initTitle()
{
    $view = $this->bootstrap('view')->getResource('view'); 
    $view->headTitle('foobar');
}

Modifying the title in the action is very simple:

public function fooAction()
{
    $this->view->headTitle()->prepend('foo | ');
}

Accessing the foo action will yield:

<title>foo | foobar</title>

For reference example was written with Zend Framework 1.11.2.

Jan 19 2011

Using Zend_Form_Decorators to compose a form as table rows

Ahh Zend form decorators, they certainly have a reputation of being quite difficult to pick up. I suffered similar problems but persevered to get the table layout I wanted.

$form->setElementDecorators(array(
    'ViewHelper',
    array('Description', array('tag' => 'span')),
    array('Errors'),
    array('HtmlTag', array('tag' => 'td')),
    array('Label', array('tag' => 'th', 'requiredSuffix' => ' *')),
    array(array('row' => 'HtmlTag'), array('tag' => 'tr')),
));

The above code will result in the following (albeit not quite as neatly):

<tr>
    <th>Label *</th>
    <td>
        <input type="text" /><span class="hint">Description</span>
        <ul class="errors"><li>Error</li></ul>
    </td>
</tr>

For reference example was written with Zend Framework 1.11.2.

Info

I'm Ade Slade, a PHP web developer.