Zend Framework route: unknown number of params
Asked Answered
G

3

6

I'm trying to write a route for an level N category depth. So an usual category URL would look like this:

http://website/my-category/my-subcategory/my-subcategory-level3/my-subcategory-level4

It has an unknown depth and my route has to match all possible levels. I made a route for this, but I can't get all the params from my controller.

$routeCategory = new Zend_Controller_Router_Route_Regex(
    '(([a-z0-9-]+)/?){1,}',
        array(
            'module' => 'default',
            'controller' => 'index',
            'action' => 'index'
        ),
        array( 1 => 'path'),
        '%s'
);
$router->addRoute('category', $routeCategory);

I can't seem to find a way to send the route matched params to the controller. If you have a better solution, I'm open to suggestions!

Guatemala answered 22/3, 2011 at 8:37 Comment(0)
G
4

I found a solution that I think fits my needs. I'll post it here for people who will end up in the same thing I got into.

Problem:

  • need custom route for level N categories like category/subcategory/subsubcategory/...
  • custom route for N categories + object like category/subcategory/../page.html
  • preserve Zend Framework's default routing (for other modules, admin for example)
  • URL assembling with URL helper

Solution:

  • create custom route class (I used Zend_Controller_Router_Route_Regex as a starting point so I can benefit from the assemble() method)

Actual code:

<?php

class App_Controller_Router_Route_Category extends Zend_Controller_Router_Route_Regex
{
    public function match($path, $partial = false)
    {
        if (!$partial) {
            $path = trim(urldecode($path), '/');
        }

        $values = explode('/', $path);
        $res = (count($values) > 0) ? 1 : 0;
        if ($res === 0) {
            return false;
        }

        /**
         * Check if first param is an actual module
         * If it's a module, let the default routing take place
         */
        $modules = array();
        $frontController = Zend_Controller_Front::getInstance();
        foreach ($frontController->getControllerDirectory() as $module => $path) {
            array_push($modules, $module);
        }

        if(in_array($values[0], $modules)) {
            return false;
        }

        if ($partial) {
            $this->setMatchedPath($values[0]);
        }

        $myValues = array();
        $myValues['cmsCategory'] = array();

        // array_filter_key()? Why isn't this in a standard PHP function set yet? :)
        foreach ($values as $i => $value) {
            if (!is_int($i)) {
                unset($values[$i]);
            } else {
                if(preg_match('/.html/', $value)) {
                    $myValues['cmsObject'] = $value;
                } else {
                    array_push($myValues['cmsCategory'], $value);
                }
            }
        }

        $values = $myValues;
        $this->_values = $values;

        $values   = $this->_getMappedValues($values);
        $defaults = $this->_getMappedValues($this->_defaults, false, true);

        $return   = $values + $defaults;

        return $return;
    }

    public function assemble($data = array(), $reset = false, $encode = false, $partial = false)
    {
        if ($this->_reverse === null) {
            require_once 'Zend/Controller/Router/Exception.php';
            throw new Zend_Controller_Router_Exception('Cannot assemble. Reversed route is not specified.');
        }

        $defaultValuesMapped  = $this->_getMappedValues($this->_defaults, true, false);
        $matchedValuesMapped  = $this->_getMappedValues($this->_values, true, false);
        $dataValuesMapped     = $this->_getMappedValues($data, true, false);

        // handle resets, if so requested (By null value) to do so
        if (($resetKeys = array_search(null, $dataValuesMapped, true)) !== false) {
            foreach ((array) $resetKeys as $resetKey) {
                if (isset($matchedValuesMapped[$resetKey])) {
                    unset($matchedValuesMapped[$resetKey]);
                    unset($dataValuesMapped[$resetKey]);
                }
            }
        }

        // merge all the data together, first defaults, then values matched, then supplied
        $mergedData = $defaultValuesMapped;
        $mergedData = $this->_arrayMergeNumericKeys($mergedData, $matchedValuesMapped);
        $mergedData = $this->_arrayMergeNumericKeys($mergedData, $dataValuesMapped);

        /**
         * Default Zend_Controller_Router_Route_Regex foreach insufficient
         * I need to urlencode values if I bump into an array
         */
        if ($encode) {
            foreach ($mergedData as $key => &$value) {
                if(is_array($value)) {
                    foreach($value as $myKey => &$myValue) {
                        $myValue = urlencode($myValue);
                    }
                } else {
                    $value = urlencode($value);
                }
            }
        }

        ksort($mergedData);

        $reverse = array();
        for($i = 0; $i < count($mergedData['cmsCategory']); $i++) {
            array_push($reverse, "%s");
        }
        if(!empty($mergedData['cmsObject'])) {
            array_push($reverse, "%s");
            $mergedData['cmsCategory'][] = $mergedData['cmsObject'];
        }

        $reverse = implode("/", $reverse);
        $return = @vsprintf($reverse, $mergedData['cmsCategory']);

        if ($return === false) {
            require_once 'Zend/Controller/Router/Exception.php';
            throw new Zend_Controller_Router_Exception('Cannot assemble. Too few arguments?');
        }

        return $return;

    }
}

Usage:

Route:

$routeCategory = new App_Controller_Router_Route_Category(
        '',
        array(
            'module' => 'default',
            'controller' => 'index',
            'action' => 'index'
        ),
        array(),
        '%s'
);
$router->addRoute('category', $routeCategory);

URL Helper:

echo "<br>Url: " . $this->_helper->url->url(array(
                            'module' => 'default',
                            'controller' => 'index',
                            'action' => 'index',
                            'cmsCategory' => array(
                                'first-category',
                                'subcategory',
                                'subsubcategory')
                            ), 'category');

Sample output in controller with getAllParams()

["cmsCategory"]=>
  array(3) {
    [0]=>
    string(15) "first-category"
    [1]=>
    string(16) "subcategory"
    [2]=>
    string(17) "subsubcategory"
  }
  ["cmsObject"]=>
  string(15) "my-page.html"
  ["module"]=>
  string(7) "default"
  ["controller"]=>
  string(5) "index"
  ["action"]=>
  string(5) "index"
  • Note the cmsObject is set only when the URL contains something like category/subcategory/subsubcategory/my-page.html
Guatemala answered 22/3, 2011 at 13:4 Comment(1)
Thanks, MiPnamic! I really did some of thinking for this one :)Guatemala
S
2

I've done it without routes... I've routed only the first parameter and then route the others getting all the params inside the controller

Route:

resources.router.routes.catalog-display.route = /catalog/item/:id
resources.router.routes.catalog-display.defaults.module = catalog
resources.router.routes.catalog-display.defaults.controller = item
resources.router.routes.catalog-display.defaults.action = display

as example: I use this for the catalog, then into the itemController into the displayAction I check for $this->getRequest()->getParams(), the point is that you can (but I think that you know it) read all the params passed in the way key/value, as example: "site.com/catalog/item/15/kind/hat/color/red/size/M" will produce an array as: $params['controller'=>'catalog','action'=>'display','id'=>'15','kind'=>'hat','color'=>'red','size'=>'M'];

Seism answered 22/3, 2011 at 8:42 Comment(3)
Can you give an example of that, please?Guatemala
I'm not so sure this is what I want. This way I'll end up with many variables in the controller scattered around. Anyway, I found a solution so I'll post it below.Guatemala
How does site.com/catalog/item/15/kind/hat/color/red/size/M match the route /catalog/item/:id? Unless I am missing something, I don't believe it matches.Ethical
E
1

For anyone stumbling across this question using Zend Framework 2 or Zend Framework 3, there is a regex route type which can (and probably should) be used for the OP's route in which there is an unknown number of parameters dependent on the number of child categories. To use, add the following line to the top of your router config:

use Zend\Router\Http\Regex;

Then, you can use a route such as the following to match an unknown number of categories:

'categories' => [
    'type' => Regex::class,
        'options' => [
        'regex'    => '/categories(?<sequence>(/[\w\-]+)+)',
        'defaults' => [
            'controller' => ApplicationController\Categories::class,
            'action'     => 'view',
        ],  
        'spec' => '%sequence',
    ],  
],  

The above route will match the following routes:

/categories/parent-cat
/categories/parent-cat/child-cat
/categories/parent-cat/sub-child-cat
/categories/parent-cat/sub-sub-child-cat
/categories/parent-cat-2
/categories/parent-cat-2/child-cat

... and so on. The sequence of categories is passed to the controller in the sequence parameter. You can process this parameter as desired in your controller.

Ethical answered 8/2, 2019 at 3:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.