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!
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
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'];
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.