Zend Framework URLs without mod_rewrite

3rd June 2008

Some of our Zend Framework applications have to run on IIS without ISAPI_Rewrite installed. In these cases we need urls of the form http://www.example.com/index.php?module=mod&controller=con&action=act. I couldn't get this to work out of the box with Zend Framework 1.5, so wrote my own router called App_Controller_Router_Route_RequestVars.

This code obviously only supports what I needed and I've only tested it on IIS for Windows 2003 Server, so you may need to tweak to make it do what you want! Feel free to share any fixes :)

This is the code:

<?php

/** Zend_Controller_Router_Exception */
require_once 'Zend/Controller/Router/Exception.php';

/** Zend_Controller_Router_Route_Interface */
require_once 'Zend/Controller/Router/Route/Interface.php';

/**
 * Route
 *
 * @package    App_Controller
 * @subpackage Router
 * @copyright  Copyright (c) 2008 Rob Allen (rob@akrabat.com)
 */
class App_Controller_Router_Route_RequestVars implements Zend_Controller_Router_Route_Interface
{
    protected $_current = array();

    /**
     * Instantiates route based on passed Zend_Config structure
     */
    public static function getInstance(Zend_Config $config)
    {
        return new self();
    }

    /**
     * Matches a user submitted path with a previously defined route.
     * Assigns and returns an array of defaults on a successful match.
     *
     * @param string Path used to match against this routing map
     * @return array|false An array of assigned values or a false on a mismatch
     */
    public function match($path)
    {
        $frontController Zend_Controller_Front::getInstance();
        $request $frontController->getRequest();
        /* @var $request Zend_Controller_Request_Http */
        
        $baseUrl $request->getBaseUrl();
        if (strpos($baseUrl'index.php') !== false) {
            $url str_replace('index.php'''$baseUrl);
            $request->setBaseUrl($url);
        }
        
        $params $request->getParams();
        
        if (array_key_exists('module'$params)
                || array_key_exists('controller'$params)
                || array_key_exists('action'$params)) {
            
            $module $request->getParam('module'$frontController->getDefaultModule());
            $controller $request->getParam('controller'$frontController->getDefaultControllerName());
            $action $request->getParam('action'$frontController->getDefaultAction());

            $result = array('module' => $module, 
                'controller' => $controller, 
                'action' => $action, 
                );
            $this->_current $result;
            return $result;
        }
        return false;
    }

    /**
     * Assembles a URL path defined by this route
     *
     * @param array An array of variable and value pairs used as parameters
     * @return string Route path with user submitted parameters
     */
    public function assemble($data = array(), $reset=false)
    {
        $frontController Zend_Controller_Front::getInstance();
        
        if(!array_key_exists('module'$data) && !$reset 
            && array_key_exists('module'$this->_current)
            && $this->_current['module'] != $frontController->getDefaultModule()) {
            $data array_merge(array('module'=>$this->_current['module']), $data);
        }
        if(!array_key_exists('controller'$data) && !$reset 
            && array_key_exists('controller'$this->_current) 
            && $this->_current['controller'] != $frontController->getDefaultControllerName()) {
            $data array_merge(array('controller'=>$this->_current['controller']), $data);
        }
        if(!array_key_exists('action'$data) && !$reset 
            && array_key_exists('action'$this->_current)
            && $this->_current['action'] != $frontController->getDefaultAction()) {
            $data array_merge(array('action'=>$this->_current['action']), $data);
        }
        
        $url '';
        if(!empty($data)) {
            $urlParts = array();
            foreach($data as $key=>$value) {
                $urlParts[] = $key '=' $value;
            }
            $url '?' implode('&'$urlParts);
        }

        return $url;
    }
}

This route is then added to the Front Controller's router in my bootstrap like this:


$frontController Zend_Controller_Front::getInstance();
$router $frontController->getRouter();
$router->addRoute('requestVars', new App_Controller_Router_Route_RequestVars());

Hopefully this is a useful starting point for others who can't use mod_rewrite with Zend Framework.

Top Tip: XHTML with Zend Form Elements

29th May 2008

When you render a Zend_Form, the elements will render to HTML compliance rather than XHTML compliance, even if you have < ?php echo $this->doctype('XHTML1_STRICT');?> at the top of your layout script. Practically, this means that all the input elements do not end in "/>".

To resolve this, you need to call the doctype() view helper prior to rendering your form.

Within my projects, I do this within a front controller plug-in called ViewSetup that looks a little like this:


class App_Controller_Plugin_ViewSetup extends Zend_Controller_Plugin_Abstract
{
    public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
    {
        // setup the layout
        Zend_Layout::startMvc(); 
        
        $viewRenderer Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer');
        $viewRenderer->init();
        
        $view $viewRenderer->view;
        $view->doctype('XHTML1_TRANSITIONAL');

        $view->headMeta()->appendHttpEquiv('Content-Type''text/html;charset=utf-8');
    }
}

Obviously, this class lives in the file library/App/Controller/Plugin/ViewSetup.php.

As you can see, I also set up the meta tag for the content-type to UTF-8 ready for rendering later.

Simple Zend_Form File Upload Example Revisited

16th May 2008

I've been thinking about the Simple Zend_Form File Upload Example that I discussed last month.

To recap, if you haven't read the comments, if the form fails to validate for some reason then you get a nasty error:

Warning: htmlspecialchars() expects parameter 1 to be string, object given in /Users/rob/Sites/akrabat/Zend_Form_FileUpload_Example/lib/Zend/View/Abstract.php on line 786

Essentially, what is happening is that the App_Form_Element_File class that we wrote assigns the $_FILES array to the $value parameter for the form element. On redisplay of the form, the formFile view helper then calls the escape() view helper passing in the $value when rendering the <input> element. The escape() view helper calls htmlspecialchars() which throws the warning about $value not being a string.

*whew!*

What we need is something that's an array when the data is valid, but can also look like a string to htmlspecialchars(). This got me thinking about the SPL and creating an object for the data from the $_FILES array.

Let's call this object App_Form_Element_FileValue and store it in lib/App/Form/Element/FileValue.php:

<?php

class App_Form_Element_FileValue extends ArrayObject
{
    public function __toString()
    {
        $result '';
        if(isset($this->name)) {
            $result $this->name;
        }
        return $result;
    }
}

The ArrayObject class is part of the SPL and handily provides a set of functions that enables the object to work with most functions that we like to use with an array including the ability to access the data using array notation. We implement the PHP5 magic function __toString() so that htmlspecialchars() will get a string from the object when it asks for one which nicely knocks that problem on the head.

To integrate it into the code, we need to modify App_Form_Element_File::isValid() from:


    public function isValid($value$context null)
    {
        // for a file upload, the value is not in the POST array, it's in $_FILES
        $key $this->getName();
        if(null === $value) {
            if(isset($_FILES[$key])) {
                $value $_FILES[$key];
            }
        }
        // continues...

to


    public function isValid($value$context null)
    {
        // for a file upload, the value is not in the POST array, it's in $_FILES
        $key $this->getName();
        if(null === $value) {
            if(isset($_FILES[$key])) {
                $value = new App_Form_Element_FileValue($_FILES[$key]);
            }
        }
        // continues...

We also need to modify the validator App_Validate_ValidFile::isValid() function as it's rather too rigourous in its checking. We currently check that $value is an array using is_array():


    public function isValid($value)
    {
        // default value and error is "no file uploaded"
        $valueString '';
        $error UPLOAD_ERR_NO_FILE;
        
        if(is_array($value) && array_key_exists('error'$value)) {
            // set the error to the correct value
            $error $value['error'];
            
            // set the %value% placeholder to the uplaoded filename
            $valueString $value['name'];
        }
        // continues...

As $value is now an object of type App_Form_Element_FileValue, we need to change the test in the if statement to:


    public function isValid($value)
    {
        // default value and error is "no file uploaded"
        $valueString '';
        $error UPLOAD_ERR_NO_FILE;
        
        if((is_array($value) || $value instanceof ArrayObject) 
            && array_key_exists('error'$value)) {
            // set the error to the correct value
            $error $value['error'];
            
            // set the %value% placeholder to the uplaoded filename
            $valueString $value['name'];
        }
        // continues...

Note that we test for an instance of ArrayObject as that is where the functionality of array behaviour is implemented and is more generic in case we need to reuse this code with another object that behaves like an array.

Those are the only changes needed to elegantly remove the error message.

Here's a zip file of this project with the above changes: Zend_Form_FileUpload_Example_Revisited.zip (It includes Zend Framework 1.5.2 which is why it's 3.9MB big).

Test it out and see if it works for you as well as it works for me !

Simple Zend_Form File Upload Example

7th April 2008

Zend Framework 1.5's Zend_Form component is missing support for the file input element as it is waiting on a file upload component to build upon. We're busy people, so we'll fake it...

This is a super simple example showing how to do file uploads with Zend_Form in Zend Framework 1.5.

I'm building on the Simple Zend_Form Example, so make sure you have read that and that it works before you start this one.

This is what the form looks like:

SimpleFileUploadScreenshot.jpg

(Unstyled, as usual!)

This is what we need to do:

The form

The form is an extension of Zend_Form and is stored in app/forms/UploadForm.php and so the class name is forms_UploadForm:

<?php

class forms_UploadForm extends Zend_Form 
{ 
    public function __construct($options null) 
    { 
        $this->addElementPrefixPath('App''App/');
        parent::__construct($options);
        $this->setName('upload');
        $this->setAttrib('enctype''multipart/form-data');
        
        $description = new Zend_Form_Element_Text('description');
        $description->setLabel('Description')
                  ->setRequired(true)
                  ->addValidator('NotEmpty');

        $file = new App_Form_Element_File('file');
        $file->setLabel('File')
                 ->setRequired(true)
                 ->addValidator('NotEmpty');
             

        $submit = new Zend_Form_Element_Submit('submit');
        $submit->setLabel('Upload');
        
        $this->addElements(array($description$file$submit));
            

    } 
} 

We are going to create our own validator class within our own library namespace (lib/App) and so we need to tell the form about this using addElementPath(). We then set the name and enctype attribute of the form to allow for files to be uploaded.

The form has two fields: a text field called 'description' and the file upload field called 'file', along with a submit button. As Zend Framework doesn't have it's own file element, we will create our own called App_Form_Element_File.

The file form element

The file element, App_Form_Element_File, is stored in lib/App/Form/Element/File.php and looks like this:

<?php
require_once 'Zend/Form/Element/Xhtml.php';

class App_Form_Element_File extends Zend_Form_Element_Xhtml
{
    /**
     * Flag indicating whether or not to insert ValidFile validator when element is required
     * @var bool
     */
    protected $_autoInsertValidFileValidator true;

    /**
     * Default view helper to use
     * @var string
     */
    public $helper 'formFile';
    
    /**
     * Set flag indicating whether a ValidFile validator should be inserted when element is required
     * 
     * @param  bool $flag 
     * @return Zend_Form_Element
     */
    public function setAutoInsertValidFileValidator($flag)
    {
        $this->_autoInsertValidFileValidator = (bool) $flag;
        return $this;
    }

    /**
     * Get flag indicating whether a ValidFile validator should be inserted when element is required
     * 
     * @return bool
     */
    public function autoInsertValidFileValidator()
    {
        return $this->_autoInsertValidFileValidator;
    }
    
    
    public function isValid($value$context null)
    {
        // for a file upload, the value is not in the POST array, it's in $_FILES
        $key $this->getName();
        if(null === $value) {
            if(isset($_FILES[$key])) {
                $value $_FILES[$key];
            }
        }
        
        // auto insert ValidFile validator
        if ($this->isRequired()
            && $this->autoInsertValidFileValidator()
            && !$this->getValidator('ValidFile'))
        {
            $validators $this->getValidators();
            $validFile   = array('validator' => 'ValidFile''breakChainOnFailure' => true);
            array_unshift($validators$validFile);
            $this->setValidators($validators);
            
            // do not use the automatic NotEmpty Validator as ValidFile replaces it 
            $this->setAutoInsertNotEmptyValidator(false);
        }

        return parent::isValid($value$context);
    }

}

Zend Framework already provides a view helper for displaying a file element, formFile, so we set the $helper member variable to 'formFile' so that the correct element is rendered when the form is displayed. We then need to ensure that validation is handled correctly. For all other form elements the value of the field is returned in the POST array. For a file, this is not true as the data is with the $_FILES global array. We could handle this in the controller, but by putting it in the element class, we never have to think about it again. The isValid() member function is used to set the value for an element and also run the validator chain to determine if the value is valid or not.

We override isValid() for the file element to provide two functionalities:

* Set the value to the contents of correct sub-array of the $_FILES array .
* Automatically turn on a custom validator called ValidFile which will check if the upload succeeded.

The file element also has some helper functions (setAutoInsertValidFileValidator and getAutoInsertValidFileValidator) to control the auto-insertion of the ValidFile validator. Note that if we do automatically insert the ValidFile validator, then we turn off Zend_Form_Element's automatic NotEmpty validator as it is redundant.

Once we have set the $value variable correctly and inserted our validator, we call up to the parent's isValid() function which will run the validation chain for us.

The ValidFile validator

The ValidFile validator's full class name is App_Validate_ValidFile and so it's filename is lib/App/Validate/ValidFile.php. This class handles validating the 'error' field within the $value array (which as you'll recall came from $_FILES). This is the code:

<?php

class App_Validate_ValidFile extends Zend_Validate_Abstract
{

    const INI_SIZE 'iniSize';     // Value: 1; The uploaded file exceeds the upload_max_filesize directive in php.ini
    const FORM_SIZE 'formSize';   // Value: 2; The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form. 
    const PARTIAL 'partial';      // Value: 3; The uploaded file was only partially uploaded. 
    const NO_FILE 'noFile';       // Value: 4; No file was uploaded. 
    const NO_TMP_DIR 'noTmpDir';  // Value: 6; Missing a temporary folder.
    const CANT_WRITE 'cantWrite'// Value: 7; Failed to write file to disk.
    const EXTENSION 'extension';  // Value: 8; File upload stopped by extension. Introduced in PHP 5.2.0. 
    const ERROR 'error';          // General error for future proofing against new PHP versions

    /**
     * @var array
     */
    protected $_messageTemplates = array(
        self::INI_SIZE => "The uploaded file exceeds the upload_max_filesize directive in php.ini",
        self::FORM_SIZE => "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form",
        self::PARTIAL => "The uploaded file was only partially uploaded",
        self::NO_FILE => "No file was uploaded",
        self::NO_TMP_DIR => "Missing a temporary folder",
        self::CANT_WRITE => "Failed to write file to disk",
        self::EXTENSION => "File upload stopped by extension",
        self::ERROR => "Unknown upload error"
    );

    /**
     * Defined by Zend_Validate_Interface
     *
     * Returns true if and only if $value[error] is equal to UPLOAD_ERR_OK.
     * 
     * @note: This validator expects $value to be the array from $_FILES
     *
     * @param array $value
     * @return boolean
     */
    public function isValid($value)
    {
        // default value and error is "no file uploaded"
        $valueString '';

        $error UPLOAD_ERR_NO_FILE;
        
        if(is_array($value) && array_key_exists('error'$value)) {
            // set the error to the correct value
            $error $value['error'];
            
            // set the %value% placeholder to the uplaoded filename
            $valueString $value['name'];
        }
        
        $this->_setValue($valueString);

        $result false;
        switch($error) {
            case UPLOAD_ERR_OK:
                $result true;
                break;
                
            case UPLOAD_ERR_INI_SIZE:
                $this->_error(self::INI_SIZE);
                break;
                
            case UPLOAD_ERR_FORM_SIZE:
                $this->_error(self::FORM_SIZE);
                break;
                
            case UPLOAD_ERR_PARTIAL:
                $this->_error(self::PARTIAL);
                break;
                
            case UPLOAD_ERR_NO_FILE:
                $this->_error(self::NO_FILE);
                break;
                
            case UPLOAD_ERR_NO_TMP_DIR:
                $this->_error(self::NO_TMP_DIR);
                break;
                
            case UPLOAD_ERR_CANT_WRITE:
                $this->_error(self::CANT_WRITE);
                break;
                
            case 8// UPLOAD_ERR_EXTENSION isn't defined in PHP 5.1.4, so use the value
                $this->_error(self::EXTENSION);
                break;
                
            default:
                $this->_error(self::ERROR);
                break;
        }

        return $result;
    }

}

Although this class is quite long, not much happens! All the work happens in isValid() where we set $error to the 'error' value originally from the $_FILES array and then set our error message based on which error PHP has reported to us. If PHP reported no error (UPLOAD_ERR_OK), then we return true, otherwise we return false after having set the error value.

The controller

Lastly, the controller is essentially identical to what we did in the Simple Zend_Form Example:

<?php

class IndexController extends Zend_Controller_Action
{
    function indexAction()
    {
        $this->view->pageTitle "Zend_Form File Upload Example";
        $this->view->bodyCopy "<p>Please fill out this form.</p>";

        $form = new forms_UploadForm();

        if ($this->_request->isPost()) {
            $formData $this->_request->getPost();
            if ($form->isValid($formData)) {
                
                // success - do something with the uploaded file
                $uploadedData $form->getValues();
                Zend_Debug::dump($uploadedData'$uploadedData');
                exit;
                
            } else {
                $form->populate($formData);
            }
        }

        $this->view->form $form;
    }
}

If it all validates correctly, then you need to process the uploaded data. For this example we just display it using Zend_Debug::dump(), so you can see what it looks like:

SimpleFileUploadScreenshot2.jpg

Conclusion

As you can see, although Zend Framework 1.5 doesn't have file upload element built in, adding one is not especially hard. In fact, we have more code used to validate that and provide an error message on failure than we do for the upload itself!

As usual, here's a zip file of this project: Zend_Form_FileUpload_Example.zip (It includes Zend Framework 1.5.1 which is why it's 3.9MB big).

Test it out and maybe use it as the basis of your file uploading needs with Zend_Form.

Update: There's a follow up to this post here: Simple Zend_Form File Upload Example Revisited.

A View Stream with Zend_View

5th February 2008

One of my biggest issues with using PHP as the templating engine in View scripts is that the easiest way to echo a variable is the least secure.

Consider:

<?= $this->var ?>

Perfectly legal, dead easy to understand, but doesn't escape $var which is what you want more often than not. To resolve this you need something like:

<?= $this->escape($this->var?>

But who remembers to do that?!

I don't and I have short-open-tags turned off too!

So, I decided to leverage a post by Mike Naberezny from a while ago about streams. The idea is all his; I just modified it to work with Zend Framework's Zend_View the way I wanted it to.

This is what I want to happen:

My Code:

<?= @$var?>

is translated to

<?php echo $this->escape($this->var); ?>

As you can see, this significantly cuts down the amount of typing that we need to do and also makes view templates much easier to read!

PHP stream wrappers are a mechanism that allows us to write our own protocol handlers for files. In our case, we want to intercept the view script file and alter the code within short tags to escape the variable for us. We use the short tag so that if we decide that we do not want a variable to be escaped we can use:

<?php echo $this->var;?> 

and it will work as expected.

Let's look at the code!
Read the rest of this entry »

Subversion Externals

27th January 2008

Subversion's svn:externals property is the mechanism that is used to automatically check out data from other repositories into your working directory. This post is to remind me how to do it as each time I need the information, I end up Googling for it.

To start, let's assume you have a lib/ directory and want to put ZF's library/ and incubator/library/ code within it so that the result looks like this:

myApplication/
	lib/
		incubator/Zend/
		Zend/

This is what you do:

$ cd lib/
$ export SVN_EDITOR=vim
$ svn propedit svn:externals .

(Change vim to your own choice of editor as required!)

Vim now starts up and you need the following lines:

incubator http://framework.zend.com/svn/framework/branch/release-1.0/incubator/library
Zend http://framework.zend.com/svn/framework/branch/release-1.0/library/Zend

Note that release-1.0 is the current released version of Zend Framework and release-1.5 will be the next one. The ongoing development work for 1.5 is being done on release-1.5PR.

Save and exit from vim and you will see this message:

Set new value for property 'svn:externals' on '.'

That's it! Don't forget to commit and then you can run svn up to keep your copy of the Zend Framework up to date.

UK PHP Conference

23rd January 2008

I'm not sure how many people follow my the Zend Framework in Action website yet, so I thought I'd advertise here that I've just posted about my upcoming session at the UK PHP Conference.

Go and read it and then subscribe to the RSS feed!

New Zend Framework Blog/Website

3rd January 2008

I thought I'd start a new website to talk about all things Zend Framework, so I'm introducing www.zendframeworkbook.com to the world.

The intention is to highlight interesting things that I see on the Zend Framework mailing lists and also to talk about book stuff periodically as well.

Obviously, if you follow the ZF mailing lists religiously, then you may find that you recognise the content of a lot of the posts, but if not, then it may turn into a useful summary of some of the main issues that are happening in Zend Framework land.

Where to set up your view?

9th December 2007

With Zend_Layout on the horizon, I've been looking at how to use it and it will remove the need for my own front controller plugin which I call SiteTemplate. As a result, I need to find a new home for the view customisation stuff I do in SiteTemplate.

My current plan is to create a new front controller plugin called ViewSetup (for want of a better name!) that contains just the view setup stuff from

<?php
require_once 'Zend/Controller/Plugin/Abstract.php';

/**
 * Front Controller plug in to set up the view with the Akra view helper
 * path and some useful request variables.
 *
 */
class Akra_Controller_Plugin_ViewSetup extends Zend_Controller_Plugin_Abstract
{    
    public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
    {
        $viewRenderer Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer');
        $viewRenderer->setView(new Akra_View());

        // add helper path to View/Helper directory within this library
        $prefix 'Akra_View_Helper';
        $dir dirname(__FILE__) . '/../../View/Helper';
        $viewRenderer->view->addHelperPath($dir$prefix);
    
        // set up variables that the view may want to know    
        $viewRenderer->view->baseUrl $request->getBaseUrl();
        $viewRenderer->view->module $request->getModuleName();
        $viewRenderer->view->controller $request->getControllerName();
        $viewRenderer->view->action $request->getActionName();
        $viewRenderer->view->theme $request->getParam('theme''default');
    }
}

(I'll talk about how cool Akra_View is another time...)

I had a dig into Zend_Layout's source and it collects its view object from the view renderer too, so this works as expected. (*phew*!)

The alternative, as I see it is to put the view creation into my bootstrap and then create a series of view helpers like this one for the base url:

<?php

class Places_View_Helper_BaseUrl
{
    protected $_baseUrl;
    
    function __construct()
    {
        $fc Zend_Controller_Front::getInstance();
        $request $fc->getRequest();
        $this->_baseUrl =  $request->getBaseUrl();
    }
    
    function baseUrl()
    {
        return $this->_baseUrl;
    }
}

How does everyone else do it? Is the bootstrap + many view helpers approach a better one than using a front controller plugin? Have I missed a completely obvious better solution?

Zend_View: Access the view from a view helper

6th December 2007

It's in the manual, but I thought I'd blog about my simple View Helper setup that ensures that I can get at the view with minimal effort.

Firstly we create a parent class:

<?php

abstract class My_View_Helper_Abstract
{
    protected $_view;
    
    public function setView($view)
    {
        $this->_view $view;
    }
}

This class contains the code required by Zend_View to collect an instance of the view and assign it to a protected variable. All my view helpers extend this class and so I can acess the view using $this->_view. For instance:

<?php

require_once 'My/View/Helper/Abstract.php';

class My_View_Helper_TreeUl extends My_View_Helper_Abstract
{
    /**
     * Render a nested array as a set of nested <ul>s.
     *
     * @param array|instanceof Iterator $list
     * @return string
     */
    function treeUl($list)
    {
        $output '';
        if (is_array($list) || $list instanceof Iterator) {
            if (count($list) > 0) {
                $output "<ul>\n";
                foreach ($list as $item) {
                    $output .= "\t<li>";
                    if(is_string($item)) {
                        $output .= $this->_view->escape($item);
                    } else {
                        $output .= $this->treeUl($item);
                    }
                    $output .= "</li>\n";
                }
                $output .= "</ul>\n";
            }
        }
        return $output;
    }
}

Simple, isn't it ?!