Issues

ZF-7848: Empty static route (empty string) will NEVER match (sample from docs, Route_Hostname, Route_Static)

Description

The following is suggested in docs:


$hostnameRoute = new Zend_Controller_Router_Route_Hostname(
    ':username.users.example.com',
    array(
        'controller' => 'profile',
        'action'     => 'userinfo'
    )
);

$plainPathRoute = new Zend_Controller_Router_Route_Static('');

$router->addRoute('user', $hostnameRoute->chain($plainPathRoute);

The purpose of $plainPathRoute: to create a default route for users visiting the hostname .users.example.com. Unfortunatelly this will never work, because static route of [empty string] *will never match(). Unfortunatelly, leaving only the Route_Hostname is not an option - because as stated in the manual, lone Route_Hostname will catch each and every request (?).

The bug is here: (line 78 in Zend/Controller/Router/Route/Static.php)


    public function match($path, $partial = false)
    {
        if ($partial) {
            if (substr($path, 0, strlen($this->_route)) === $this->_route) {
                $this->setMatchedPath($this->_route);
                return $this->_defaults;
            }
        } else {
            if (trim($path, '/') == $this->_route) {
                return $this->_defaults;
            }
        }

Why? substr() of empty string always returns false - thus this route will never match and the whole chain is ommited.

Fix:


    public function match($path, $partial = false)
    {
        if ($partial) {
            if (($this->_route === '' && $path === '') || substr($path, 0, strlen($this->_route)) === $this->_route) {
                $this->setMatchedPath($this->_route);
                return $this->_defaults;
            }
        } else {
            if (($this->_route === '' && $path === '') || trim($path, '/') == $this->_route) {
                return $this->_defaults;
            }
        }

Comments

Modified fix to work with non-partial matches.

UPDATE

It is also broken for simple chains, like the one in link above:


<?xml version="1.0" encoding="UTF-8"?>
adminadminindexindexloginadminloginindex

Expected: To work for urls /admin and /admin/login.

Actual: It will only match for /admin/login.

Workaround: It's caused by the following snippet in Zend_Controller_Router_Route_Chain:


    /**
     * Matches a user submitted path with a previously defined route.
     * Assigns and returns an array of defaults on a successful match.
     *
     * @param  Zend_Controller_Request_Http $request Request to get the path info from
     * @return array|false An array of assigned values or a false on a mismatch
     */
    public function match($request, $partial = null)
    {
        $path    = trim($request->getPathInfo(), '/');
        $subPath = $path;
        $values  = array();
        
       foreach ($this->_routes as $key => $route) {
            if ($key > 0 && $matchedPath !== null) {
                $separator = substr($subPath, 0, strlen($this->_separators[$key]));
                if ($separator !== $this->_separators[$key]) {
                    return false;
                }
                
                $subPath = substr($subPath, strlen($separator));
            }

Below is a quick fix which takes into account the behaviour of substr() on empty strings, as in this case when chain has already consumed admin and an empty '' $subpath is left for matching.


    /**
     * Matches a user submitted path with a previously defined route.
     * Assigns and returns an array of defaults on a successful match.
     *
     * @param  Zend_Controller_Request_Http $request Request to get the path info from
     * @return array|false An array of assigned values or a false on a mismatch
     */
    public function match($request, $partial = null)
    {
        $path    = trim($request->getPathInfo(), '/');
        $subPath = $path;
        $values  = array();
        
       foreach ($this->_routes as $key => $route) {
            if ($key > 0 && $matchedPath !== null) {
                $separator = substr($subPath, 0, strlen($this->_separators[$key]));
                if (($subPath !== '') && $separator !== $this->_separators[$key]) {
                    return false;
                }
                
                $subPath = (string)substr($subPath, strlen($separator));
            }

What happens is that we check for empty string '' and then force (string) as a result of substr(), because other routes would fail to match agains false.

This allows "default" routes to work with simple non-host-based chains!

Cheers!

In 1.10 alpha this patch does not give the required result.

Thank you for info.

I am willing to analyze it and contribute a new patch as long as there is any chance of (finally) commiting and fixing it permanently!

Artur it would be great if you could look into this problem, because it is a real blocker (for me).

I need hostname based routing for my modules and the only "half"-workaround Ive managed to produce is following...

$hostnameRoute = new Zend_Controller_Router_Route_Hostname('admin.test.local',array('module' => 'admin'));

// Instead of empty static route...
// $oRoute = new Zend_Controller_Router_Route_Static('');

$oRoute = new Zend_Controller_Router_Route('/:controller/:action/*', array('module' => 'admin', 'controller' => 'index', 'action' => 'index'));

$router->addRoute('admin', $hostnameRoute->chain($oRoute));

With this Ive managed the routing part... but in the routing process the path information is build without "/" at the beginning. $this->_request->getPathInfo() returns "controller/action" instead of "/controller/action" This breaks the Zend_Navigation component !!

Maybe there is another workaround I am not aware of?

Hey Edvin!

Which version are you using? Have you patched your ZF with the snippets I provided?

I use these routes (suggested by docs, sic!) every day and they work fine.

Same issue here. I really don't want to patch ZF. I can probably override match() and the method in the standard router to use the child class instead of Zend_Controller_Router_Route_Chain on "chain" in the config, but obviously that's hardly ideal.

I fixed this by overriding some methods in the involved route classes (thanks Artur), but one issue remains: it never produces a 404. It always goes to the index action of the default module. This is probably a separate related issue, but just wanted to check if people experiencing the issue in this ticket are also having this issue..

I've just come up against the same issue.

Is there a reason Artur's patches can not be commited in the trunk? I'm surprised more people aren't complaining about this problem.

I actually get the exact opposite of what the reporter and several commenters have discovered (when testing against current trunk, which is 1.11.0beta1): I can match the /admin route, but not the /admin/login route. I'm attempting to fix this issue now.

Additionally, the behavior does not change with the "patch" applied.

Matthew: check if this is related to ZF-8812.

Kim -- nope. (I've applied your patches locally; doesn't change anything in regards to the environment and expectations presented here.)

Fixed in trunk and release branch. Patch had to change due to changes that have already been introduced; basic gist was that a check for (empty($path) && empty($this->_route)) had to ORd to the existing partial conditional in the Static route.