ЧПУ для Zend Framework и кэширование
Задача:
Обрабатывать url'ы вида:
www. example.com/what/about-this-2010-09-08.html
с учетом параметров внутри, например таким шаблоном:
[module]/[controller]-[action]-[year]-[month]-[day].[ext]
Как появилась задача.
ZF, помимо своей универсальности, обладает также приличной тяжеловесностью, и это вполне логично.
Поэтому при генерации какого-либо контента стараются применять кэширование, чтобы уменьшить частоту таких генераций.
Кэшируют блоки, страницы, объекты модели, запросы к БД — всё, что можно.
Страницы, которые не предусматривают активного изменения содержимого лучше кэшировать целиком. И таких страниц, как ни странно, бывает много.
Для этого создан Zend_Cache_Frontend_Page. С первого раза у меня он не заработал, хотя нисколько не сложен. И вместо выяснения ЧЯДНТ, я решил, что такие страницы можно отдавать без участия PHP, просто как .html файл.
Чтобы не перенапрягать .htacess сложными правилами, структура папок будет соответствовать запросам и наоборот.
Также желательно чтобы запросы были очеловеченного вида и не слишком длинные.
url'ы:
файловая структура:
Осталось только задать роуты для таких url'ов, захватить ob_start() вывод для определенных контроллеров-действий и сохранить в папку _cache
Тут Zend предлагает мощнейшее средство Zend_Controller_Router_Route_Regex.
Но средство это чуть более, чем совсем НЕ читабельное. Да еще и reverse и map в придачу.
Написал.
Задаем шаблон url'а через routes.ini
[module]/[controller]/{action}{-page}.htm
этот маршрут подходит для запросов вида:
В квадратных скобках обязательные параметры [param_name],
в фигурных — необязательные {my_param}
Небуквенные символы внутри скобок помимо идентификатора могут понадобиться для:
разделения параметров [action]{-page}, если не уточняется из каких символов* состоят оные
если в url'е нужны эти самые скобки
Для передачи переменного числа параметров я зарезервировал идентификатор {paramstr}
В него включаются не указанные явно параметры.
Например, для маршрута:
[section]/{action}{~paramstr}.html
подойдет запрос
/car/view~year[2001~2003]fuel[diesel]color[blue~red]vol[1000~1600]state[good]place[Москва].html
и в контроллере Test_CatalogController
мы получим все параметры:
И, обратно, если передать помощнику вида эти параметры, будет сформирован соответствующий url.
1. Как и в Zend_Controller_Router_Route_Regex, можно использовать шаблоны .reqs для каждого параметра
2. Как было показано выше, параметры {paramstr} передаются в формате key1[value1~value2]key2[value1~value2~value3]
Этот формат можно поменять в классе
Класс работоспособен, но написан не идеально.
В дальнейшем его можно подправить, избавиться от лишних preg_match, наследовать не от Regex, а от Abstract
Этот класс дополняет зендовские роуты, поэтому для стандартных случаев лучше использовать стандартные средства. А если вы любите сами писать регулярные выражения для url'ов, то вообще лучше не использовать этот класс.
Обрабатывать url'ы вида:
www. example.com/what/about-this-2010-09-08.html
с учетом параметров внутри, например таким шаблоном:
[module]/[controller]-[action]-[year]-[month]-[day].[ext]
Как появилась задача.
1. Понадобилось кэшировать статические и редкоменяющиеся страницы в ZF
ZF, помимо своей универсальности, обладает также приличной тяжеловесностью, и это вполне логично.
Поэтому при генерации какого-либо контента стараются применять кэширование, чтобы уменьшить частоту таких генераций.
Кэшируют блоки, страницы, объекты модели, запросы к БД — всё, что можно.
2. Отдавать генерированные страницы .html без участия PHP
Страницы, которые не предусматривают активного изменения содержимого лучше кэшировать целиком. И таких страниц, как ни странно, бывает много.
Для этого создан Zend_Cache_Frontend_Page. С первого раза у меня он не заработал, хотя нисколько не сложен. И вместо выяснения ЧЯДНТ, я решил, что такие страницы можно отдавать без участия PHP, просто как .html файл.
Чтобы не перенапрягать .htacess сложными правилами, структура папок будет соответствовать запросам и наоборот.
Также желательно чтобы запросы были очеловеченного вида и не слишком длинные.
url'ы:
/what/about/this.html /what/about/other-12.html /it/computing_games~page1.html /it/computing_games~page1.html
файловая структура:
_cache/what/about/this.html
/other-12.html
_cache/it/computing_games~page1.html
/computing_games~page2.html
Осталось только задать роуты для таких url'ов, захватить ob_start() вывод для определенных контроллеров-действий и сохранить в папку _cache
3. Задать для ZF маршруты, отличные от стандартных
Тут Zend предлагает мощнейшее средство Zend_Controller_Router_Route_Regex.
Но средство это чуть более, чем совсем НЕ читабельное. Да еще и reverse и map в придачу.
4. Написать свой класс для обработки
Написал.
class EXT_Controller_Router extends Zend_Controller_Router_Route_Regex
{
protected $_defaultReqs = "\w+";
protected $_paramStrFormat = "name[value]";
protected $_paramStrValueGlue = "~";
//protected $_paramStrFormat = "name-value-";
//protected $_paramStrValueGlue = "!";
protected $_regex = null;
protected $_route = null;
protected $_insides = array();
protected $_quotes = array();
protected $_defaults = array();
protected $_values = array();
protected $_map = array();
protected $_reqs = array();
/**
* @param Zend_Config $config Configuration object
*/
public static function getInstance(Zend_Config $config)
{
$defaults = ($config->defaults instanceof Zend_Config) ? $config->defaults->toArray() : array();
$reqs = ($config->reqs instanceof Zend_Config) ? $config->reqs->toArray() : array();
return new self($config->route, $defaults, $reqs);
}
public function __construct($route, $defaults = array(), $reqs = array())
{
$a = "(\[.*\])";
$b = "(\{.*\})";
$pattern = "#(.*)(" . $a . "|" . $b . ")#Ui";
$this->_quotes = array();
$this->_insides = array();
$map = array();
$regex = '';
if (preg_match_all($pattern, $route, $matches, PREG_SET_ORDER)) {
foreach ($matches as $i => $match) {
$inside = substr($match[2], 1, strlen($match[2])-2);
$key = preg_replace('[\W]', '', $inside);
if (!empty($reqs[$key])) {
$reg = $reqs[$key];
} elseif ('paramstr' == $key) {
$reg = '.*';
} else {
$reg = $this->_defaultReqs;
}
switch ($match[2][0]) {
case '[':
$end = '';
break;
case '{':
$end = '{0,1}';
break;
}
$regex .= preg_quote($match[1], '#') . '(' . str_replace($key, $reg, preg_quote($inside, '#')) . ')' . $end;
$map[$key] = $i+1;
$this->_quotes[$key] = $match[2][0];
$this->_insides[$key] = $inside;
}
}
preg_match("#[^\]\}]*$#i", $route, $lostMatch);
$regex .= preg_quote($lostMatch[0], '#');
$this->_route = $route;
$this->_regex = $regex;
$this->_defaults = (array) $defaults;
$this->_map = $map;
$this->_reqs = $reqs;
}
/**
* 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, $partial = false)
{
$path = trim(urldecode($path), '/');
$regex = '#^' . $this->_regex . '$#Ui';
$res = preg_match($regex, $path, $values);
if ($res === 0) {
return false;
}
foreach ($values as $i => $value) {
if (!is_int($i) || $i === 0 || empty($value)) {
unset($values[$i]);
}
}
$values = $this->_getMappedValues($values);
foreach ($this->_insides as $name => $ins) {
if ($name !== $ins && isset($values[$name])) {
$pattern = '#^' . str_replace($name, '(.*)', preg_quote($ins)) . '$#i';
if (preg_match($pattern, $values[$name], $matches)) {
$values[$name] = $matches[1];
}
}
}
if (!empty($values['paramstr'])) {
$values += $this->explodeParamStr($values['paramstr']);
}
$this->_values = $values;
$defaults = $this->_getMappedValues($this->_defaults, false, true);
$result = $values + $defaults;
return $result;
}
public function explodeParamStr($paramStr)
{
$values = array();
$pattern = str_replace(array('name', 'value'),
array('(\w+)', '(.*)'),
preg_quote($this->_paramStrFormat, '#'));
$pattern = '#(' . $pattern . ')#Uui';
if (preg_match_all($pattern, $paramStr, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
if (!empty($match[3]))
$value = explode($this->_paramStrValueGlue, $match[3]);
if (count($value) > 1) {
$values[$match[2]] = $value;
} else {
$values[$match[2]] = $match[3];
}
}
}
return $values;
}
public function implodeParamStr(array $params)
{
$paramStr = '';
foreach($params as $name => $value) {
if (!in_array($name, array('controller', 'module', 'paramstr', 'action')) && !array_key_exists($name, $this->_map)) {
if (is_array($value)) {
$value = implode($this->_paramStrValueGlue, $value);
}
$paramStr .= str_replace(array('name', 'value'),
array($name, $value),
$this->_paramStrFormat);
}
}
if (empty($paramStr)) {
$paramStr = null;
}
return $paramStr;
}
/**
* Assembles a URL path defined by this route
*
* @param array $data An array of name (or index) and value pairs used as parameters
* @return string Route path with user submitted parameters
*/
public function assemble($data = array(), $reset = false, $encode = false, $partial = false)
{
$replaces = array();
$searches = array();
$paramStr = $this->implodeParamStr($data);
$data = $data + $this->_values;
if (empty($paramStr)) {
unset($data['paramstr']);
} else {
$data['paramstr'] = $paramStr;
}
foreach($this->_map as $key => $i) {
$inside = $this->_insides[$key];
switch ($this->_quotes[$key]) {
case '[':
$searches[$i] = '[' . $inside . ']';
break;
case '{':
$searches[$i] = '{' . $inside . '}';
break;
default:
break;
}
if (!empty($data[$key])) {
$replaces[$i] = str_ireplace($key, $data[$key], $inside);
} else {
$replaces[$i] = '';
}
}
$url = str_replace($searches, $replaces, $this->_route);
return $url;
}
}
Использование
Задаем шаблон url'а через routes.ini
[module]/[controller]/{action}{-page}.htm
my-route.type = "EXT_Controller_Router"
my-route.route = "[module]/[controller]/{action}{-page}.htm"
my-route.defaults.action = index
этот маршрут подходит для запросов вида:
www.notmysite.ru/what/about/this.html www.notmysite.ru/what/about/other-12.html
В квадратных скобках обязательные параметры [param_name],
в фигурных — необязательные {my_param}
Небуквенные символы внутри скобок помимо идентификатора могут понадобиться для:
разделения параметров [action]{-page}, если не уточняется из каких символов* состоят оные
если в url'е нужны эти самые скобки
www.notmysite.ru/dogovor[123e][12].htm, тогда маршрут будет задан так [controller][[tom]][[page]].htm
dogovor.route = "[controller][[tom]][[page]].htm" dogovor.defaults.module = index dogovor.defaults.controller = dogovor dogovor.defaults.action = show
Волшебный параметр [paramstr]
Для передачи переменного числа параметров я зарезервировал идентификатор {paramstr}
В него включаются не указанные явно параметры.
Например, для маршрута:
[section]/{action}{~paramstr}.html
car.type = "EXT_Controller_Router"
car.route = "[section]/{action}{~paramstr}.html"
car.defaults.module = car
car.defaults.controller = catalog
подойдет запрос
/car/view~year[2001~2003]fuel[diesel]color[blue~red]vol[1000~1600]state[good]place[Москва].html
и в контроллере Test_CatalogController
мы получим все параметры:
'section' => 'car'
'action' => 'view'
'paramstr' => 'year[2001~2003]fuel[diesel]color[blue~red]vol[1000~1600]state[good]place[Москва]'
'letter' => 'l'
'year'
0 => '2001'
1 => '2003'
'fuel' => 'diesel'
'color'
0 => 'blue'
1 => 'red'
'vol'
0 => '1000'
1 => '1600'
'state' => 'good'
'place' => 'Москва'
'module' => 'test'
'controller' => 'index'
И, обратно, если передать помощнику вида эти параметры, будет сформирован соответствующий url.
$params = array('action' => 'view',
'step' => 2,
'year' => array('2009', '2010'));
$nextUrl = $this->view->url($params, 'my-route');
Детали
1. Как и в Zend_Controller_Router_Route_Regex, можно использовать шаблоны .reqs для каждого параметра
например, "ограниченный набор значений": my-route.reqs.action = "index|view|print" или "только цифры": my-route.reqs.action = "\d+"
2. Как было показано выше, параметры {paramstr} передаются в формате key1[value1~value2]key2[value1~value2~value3]
Этот формат можно поменять в классе
protected $_paramStrFormat = "name[value]"; protected $_paramStrValueGlue = "~";
Важно
Класс работоспособен, но написан не идеально.
В дальнейшем его можно подправить, избавиться от лишних preg_match, наследовать не от Regex, а от Abstract
Этот класс дополняет зендовские роуты, поэтому для стандартных случаев лучше использовать стандартные средства. А если вы любите сами писать регулярные выражения для url'ов, то вообще лучше не использовать этот класс.

комментарии (3)