Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

427 lines (364 sloc) 13.144 kb
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
* @package Zend_Mvc
*/
namespace Zend\Mvc\Router\Http;
use Traversable;
use Zend\Mvc\Router\Exception;
use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\RequestInterface as Request;
/**
* Segment route.
*
* @package Zend_Mvc_Router
* @subpackage Http
* @see http://manuals.rubyonrails.com/read/chapter/65
*/
class Segment implements RouteInterface
{
/**
* Map of allowed special chars in path segments.
*
* http://tools.ietf.org/html/rfc3986#appendix-A
* segement = *pchar
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
* / "*" / "+" / "," / ";" / "="
*
* @var array
*/
private static $urlencodeCorrectionMap = array(
'%21' => "!", // sub-delims
'%24' => "$", // sub-delims
'%26' => "&", // sub-delims
'%27' => "'", // sub-delims
'%28' => "(", // sub-delims
'%29' => ")", // sub-delims
'%2A' => "*", // sub-delims
// '%2B' => "+", // sub-delims - special value for php/urlencode
'%2C' => ",", // sub-delims
// '%2D' => "-", // unreserved - not touched by urlencode
// '%2E' => ".", // unreserved - not touched by urlencode
'%3A' => ":", // pchar
'%3B' => ";", // sub-delims
'%3D' => "=", // sub-delims
'%40' => "@", // pchar
// '%5F' => "_", // unreserved - not touched by urlencode
'%7E' => "~", // unreserved
);
/**
* Parts of the route.
*
* @var array
*/
protected $parts;
/**
* Regex used for matching the route.
*
* @var string
*/
protected $regex;
/**
* Map from regex groups to parameter names.
*
* @var array
*/
protected $paramMap = array();
/**
* Default values.
*
* @var array
*/
protected $defaults;
/**
* List of assembled parameters.
*
* @var array
*/
protected $assembledParams = array();
/**
* Create a new regex route.
*
* @param string $route
* @param array $constraints
* @param array $defaults
*/
public function __construct($route, array $constraints = array(), array $defaults = array())
{
$this->defaults = $defaults;
$this->parts = $this->parseRouteDefinition($route);
$this->regex = $this->buildRegex($this->parts, $constraints);
}
/**
* factory(): defined by RouteInterface interface.
*
* @see Route::factory()
* @param array|Traversable $options
* @throws \Zend\Mvc\Router\Exception\InvalidArgumentException
* @return Segment
*/
public static function factory($options = array())
{
if ($options instanceof Traversable) {
$options = ArrayUtils::iteratorToArray($options);
} elseif (!is_array($options)) {
throw new Exception\InvalidArgumentException(__METHOD__ . ' expects an array or Traversable set of options');
}
if (!isset($options['route'])) {
throw new Exception\InvalidArgumentException('Missing "route" in options array');
}
if (!isset($options['constraints'])) {
$options['constraints'] = array();
}
if (!isset($options['defaults'])) {
$options['defaults'] = array();
}
return new static($options['route'], $options['constraints'], $options['defaults']);
}
/**
* Parse a route definition.
*
* @param string $def
* @return array
* @throws Exception\RuntimeException
*/
protected function parseRouteDefinition($def)
{
$currentPos = 0;
$length = strlen($def);
$parts = array();
$levelParts = array(&$parts);
$level = 0;
while ($currentPos < $length) {
preg_match('(\G(?P<literal>[^:{\[\]]*)(?P<token>[:{\[\]]|$))', $def, $matches, 0, $currentPos);
$currentPos += strlen($matches[0]);
if (!empty($matches['literal'])) {
$levelParts[$level][] = array('literal', $matches['literal']);
}
if ($matches['token'] === ':') {
if (isset($def[$currentPos]) && $def[$currentPos] === '{') {
if (!preg_match('(\G\{(?P<name>[^}]+)\}:?)', $def, $matches, 0, $currentPos)) {
throw new Exception\RuntimeException('Translated parameter missing closing bracket');
}
$levelParts[$level][] = array('translated-parameter', $matches['name']);
} else {
if (!preg_match('(\G(?P<name>[^:/{\[\]]+)(?:{(?P<delimiters>[^}]+)})?:?)', $def, $matches, 0, $currentPos)) {
throw new Exception\RuntimeException('Found empty parameter name');
}
$levelParts[$level][] = array('parameter', $matches['name'], isset($matches['delimiters']) ? $matches['delimiters'] : null);
}
$currentPos += strlen($matches[0]);
} elseif ($matches['token'] === '{') {
if (!preg_match('(\G(?P<literal>[^}]+)\})', $def, $matches, 0, $currentPos)) {
throw new Exception\RuntimeException('Translated literal missing closing bracket');
}
$currentPos += strlen($matches[0]);
$levelParts[$level][] = array('translated-literal', $matches['literal']);
} elseif ($matches['token'] === '[') {
$levelParts[$level][] = array('optional', array());
$levelParts[$level + 1] = &$levelParts[$level][count($levelParts[$level]) - 1][1];
$level++;
} elseif ($matches['token'] === ']') {
unset($levelParts[$level]);
$level--;
if ($level < 0) {
throw new Exception\RuntimeException('Found closing bracket without matching opening bracket');
}
} else {
break;
}
}
if ($level > 0) {
throw new Exception\RuntimeException('Found unbalanced brackets');
}
return $parts;
}
/**
* Build the matching regex from parsed parts.
*
* @param array $parts
* @param array $constraints
* @param integer $groupIndex
* @return string
* @throws Exception\RuntimeException
*/
protected function buildRegex(array $parts, array $constraints, &$groupIndex = 1)
{
$regex = '';
foreach ($parts as $part) {
switch ($part[0]) {
case 'literal':
$regex .= preg_quote($part[1]);
break;
case 'parameter':
$groupName = '?P<param' . $groupIndex . '>';
if (isset($constraints[$part[1]])) {
$regex .= '(' . $groupName . $constraints[$part[1]] . ')';
} elseif ($part[2] === null) {
$regex .= '(' . $groupName . '[^/]+)';
} else {
$regex .= '(' . $groupName . '[^' . $part[2] . ']+)';
}
$this->paramMap['param' . $groupIndex++] = $part[1];
break;
case 'optional':
$regex .= '(?:' . $this->buildRegex($part[1], $constraints, $groupIndex) . ')?';
break;
// @codeCoverageIgnoreStart
case 'translated-literal':
throw new Exception\RuntimeException('Translated literals are not implemented yet');
break;
case 'translated-parameter':
throw new Exception\RuntimeException('Translated parameters are not implemented yet');
break;
// @codeCoverageIgnoreEnd
}
}
return $regex;
}
/**
* Build a path.
*
* @param array $parts
* @param array $mergedParams
* @param boolean $isOptional
* @param boolean $hasChild
* @return string
* @throws Exception\RuntimeException
* @throws Exception\InvalidArgumentException
*/
protected function buildPath(array $parts, array $mergedParams, $isOptional, $hasChild)
{
$path = '';
$skip = true;
$skippable = false;
foreach ($parts as $part) {
switch ($part[0]) {
case 'literal':
$path .= $part[1];
break;
case 'parameter':
$skippable = true;
if (!isset($mergedParams[$part[1]])) {
if (!$isOptional || $hasChild) {
throw new Exception\InvalidArgumentException(sprintf('Missing parameter "%s"', $part[1]));
}
return '';
} elseif (!$isOptional || $hasChild || !isset($this->defaults[$part[1]]) || $this->defaults[$part[1]] !== $mergedParams[$part[1]]) {
$skip = false;
}
$path .= $this->encode($mergedParams[$part[1]]);
$this->assembledParams[] = $part[1];
break;
case 'optional':
$skippable = true;
$optionalPart = $this->buildPath($part[1], $mergedParams, true, $hasChild);
if ($optionalPart !== '') {
$path .= $optionalPart;
$skip = false;
}
break;
// @codeCoverageIgnoreStart
case 'translated-literal':
throw new Exception\RuntimeException('Translated literals are not implemented yet');
break;
case 'translated-parameter':
throw new Exception\RuntimeException('Translated parameters are not implemented yet');
break;
// @codeCoverageIgnoreEnd
}
}
if ($isOptional && $skippable && $skip) {
return '';
}
return $path;
}
/**
* match(): defined by RouteInterface interface.
*
* @see Route::match()
* @param Request $request
* @param string|null $pathOffset
* @return RouteMatch
*/
public function match(Request $request, $pathOffset = null)
{
if (!method_exists($request, 'getUri')) {
return null;
}
$uri = $request->getUri();
$path = $uri->getPath();
if ($pathOffset !== null) {
$result = preg_match('(\G' . $this->regex . ')', $path, $matches, null, $pathOffset);
} else {
$result = preg_match('(^' . $this->regex . '$)', $path, $matches);
}
if (!$result) {
return null;
}
$matchedLength = strlen($matches[0]);
$params = array();
foreach ($this->paramMap as $index => $name) {
if (isset($matches[$index]) && $matches[$index] !== '') {
$params[$name] = $this->decode($matches[$index]);
}
}
return new RouteMatch(array_merge($this->defaults, $params), $matchedLength);
}
/**
* assemble(): Defined by RouteInterface interface.
*
* @see Route::assemble()
* @param array $params
* @param array $options
* @return mixed
*/
public function assemble(array $params = array(), array $options = array())
{
$this->assembledParams = array();
return $this->buildPath(
$this->parts,
array_merge($this->defaults, $params),
false,
(isset($options['has_child']) ? $options['has_child'] : false)
);
}
/**
* getAssembledParams(): defined by RouteInterface interface.
*
* @see Route::getAssembledParams
* @return array
*/
public function getAssembledParams()
{
return $this->assembledParams;
}
/**
* Encode a path segment.
*
* @param string $value
* @return string
*/
private function encode($value)
{
$encoded = urlencode($value);
$encoded = strtr($encoded, self::$urlencodeCorrectionMap);
return $encoded;
}
/**
* Decode a path segment.
*
* @param string $value
* @return string
*/
private function decode($value)
{
return urldecode($value);
}
}
Jump to Line
Something went wrong with that request. Please try again.