Custom URL’s in Zend Framework

I am searching for a url solution in Zend Framework for quite a while now, and I hope the solution I came up with helps other people trying to do the same thing.

More important, I hope more experienced (Zend Framework) developers can point out problems or suggest better solutions for this problem.

The CMS of my company makes it possible for users to create a tree structure of products/sections/chapters/etc… with an unlimited depth. This makes the url they ‘generate’ very variable.

I’ve searched the internet for solutions and for quite a while I used the solution of Jani. Because this completely overrides the Router object, I continued my search via the Zend MVC mailing list and had an extensive look at the Zend_Controller_Router manual page.

During the search Ivo Jansch tweeted about ZF URL’s and pointed me towards chaining, but the solution for my specific problem lies in adding Routes to the route stack.

I wanted to override the default route so that a language is required in the url. To accomplisch this I added an new function in the Bootstrap.php file:

protected function _initLanguageRoute()
{
    $this->bootstrap('frontController');
    $fc = $this->frontController;
    $router = $fc->getRouter();
    $route = new Zend_Controller_Router_Route(
        ':language/:controller/:action/*',
        array(
            'module' => 'default',
            'language' => 'nl',
            'controller' => 'index',
            'action' => 'index'
        ),
        array(
            'language' => '[a-z]{2}'
        ),
        $translator
    );
    $router->addRoute('default', $route);
}

To add a variable route (calculated by the CMS), I extended the Zend_Controller_Router_Route_Abstract class so the route could also be used by the Url View helper.

The database I use below, has calculated colums already, but a custom mapping can be calculated in the class as well. As long as the request object is pointed to some controller and action.

class Coudenysj_Controller_Router_Route_Mapping extends Zend_Controller_Router_Route_Abstract
{

    private $_db;


    public function __construct($db)
    {
        $this->_db = $db;
    }

    /**
     * A method to publish the way the route operates.
     *
     * @see Zend/Controller/Router/Rewrite.php:392
     *
     * @return int
     */
    public function getVersion()
    {
        return 1;
    }

    /**
     * The required functions (required by Interface).
     *
     * @param Zend_Config $config The config object with defaults.
     *
     * @return void
     */
    public static function getInstance(Zend_Config $config)
    {
        return;
    }

    /**
     * Matches a user submitted path with a previously defined route.
     * Assigns and returns an array of defaults on a successful match.
     *
     * @param string $path 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)
    {
        $path = trim($path, '/');

        return $this->_db->fetchRow(
            'SELECT lang, controller, action, id
            FROM mapping
            WHERE url = ?;',
            array($path)
        );
    }

    /**
     * Assembles a URL path defined by this route
     *
     * @param array   $data    An array of variable and value pairs used as parameters
     * @param boolean $reset   Not used (required by interface)
     * @param boolean $encode  Not used (required by interface)
     * @param boolean $partial Not used (required by interface)
     *
     * @return string|false Route path with user submitted parameters
     */
    public function assemble($data = array(), $reset = false, $encode = false, $partial = false)
    {
        if (!isset($data['language'])) {
            if (Zend_Registry::isRegistered('Zend_Locale')) {
                $locale = Zend_Registry::get('Zend_Locale');
                $data['language'] = $locale->getLanguage();
            } else {
                return false;
            }
        }
        if (   !isset($data['controller'])
            || !isset($data['action'])
            || !isset($data['id'])
        ) {
            return false;
        }

        $result = $this->_db->fetchRow(
            'SELECT url
            FROM mapping
            WHERE lang = ? AND controller = ? AND action = ? AND id = ?;',
            array(
                $data['language'],
                $data['controller'],
                $data['action'],
                $data['id'],
            )
        );
        return $result['url'];
    }
}

Now I can add a new ‘cms’ route to the Router object in the Bootstrap.php file:

protected function _initMappingRoute()
{
    $db = new Zend_Db::factory(...);
    $this->bootstrap('frontController');
    $fc = $this->frontController;
    $router = $fc->getRouter();
    $router->addRoute(
        'cms', new Coudenysj_Controller_Router_Route_Mapping($db)
    );
}

Remember that the route stack is processed backwards, so the ‘cms’ route is the first one to try and route the url.

This route can now also be used in the view:

echo $this->url(
    array(
        'id' => $id,
        'controller' => $controller,
        'action' => $action
    ),
    'cms' // the name of our route
);

This solution works for me (for now) and suggestions to improve it are very welcome!

  • Thanks, very clear.

    This actually helps me better understand routing and the url view helper. đŸ˜‰ Well done!

  • Pingback: Zend Framework News » Blog Archive » Individuelle URLs mit dem Zend Framework()

  • Hello,

    this is a very good explained article. It was really useful for me.

    greetings
    René

  • Oliver

    Really helpful but I am running into one problem. If you pass an empty request String (so the Domain only), it seems like no dispatching happens and no controller is set:

    Zend_Controller_Request_Http Object
    [_requestUri:protected] => /
    [_baseUrl:protected] => http://www.mydomain.de
    [_basePath:protected] => [_pathInfo:protected] => /
    [_dispatched:protected] =>
    [_module:protected] =>
    [_moduleKey:protected] => module
    [_controller:protected] =>
    [_controllerKey:protected] => controller
    [_action:protected] => [_actionKey:protected] => action

    Any hints?

  • Make sure you pass the ‘default’ route first (or just leave the standard one) and return false in the match() method if no result is found.
    That way the controller will pass the request to the next registered route (‘default’).