Skip to content

Commit

Permalink
[Routing] added support for expression conditions in routes
Browse files Browse the repository at this point in the history
  • Loading branch information
fabpot committed Sep 19, 2013
1 parent 86ac8d7 commit d477f15
Show file tree
Hide file tree
Showing 28 changed files with 255 additions and 28 deletions.
11 changes: 11 additions & 0 deletions src/Symfony/Component/Routing/Annotation/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Route
private $host;
private $methods;
private $schemes;
private $condition;

/**
* Constructor.
Expand Down Expand Up @@ -153,4 +154,14 @@ public function getMethods()
{
return $this->methods;
}

public function setCondition($condition)
{
$this->condition = $condition;
}

public function getCondition()
{
return $this->condition;
}
}
12 changes: 11 additions & 1 deletion src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ public function load($class, $type = null)
'schemes' => array(),
'methods' => array(),
'host' => '',
'condition' => '',
);

$class = new \ReflectionClass($class);
Expand Down Expand Up @@ -154,6 +155,10 @@ public function load($class, $type = null)
if (null !== $annot->getHost()) {
$globals['host'] = $annot->getHost();
}

if (null !== $annot->getCondition()) {
$globals['condition'] = $annot->getCondition();
}
}

$collection = new RouteCollection();
Expand Down Expand Up @@ -194,7 +199,12 @@ protected function addRoute(RouteCollection $collection, $annot, $globals, \Refl
$host = $globals['host'];
}

$route = new Route($globals['path'].$annot->getPath(), $defaults, $requirements, $options, $host, $schemes, $methods);
$condition = $annot->getCondition();
if (null === $condition) {
$condition = $globals['condition'];
}

$route = new Route($globals['path'].$annot->getPath(), $defaults, $requirements, $options, $host, $schemes, $methods, $condition);

$this->configureRoute($route, $class, $method, $annot);

Expand Down
12 changes: 8 additions & 4 deletions src/Symfony/Component/Routing/Loader/XmlFileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ protected function parseRoute(RouteCollection $collection, \DOMElement $node, $p
$schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY);
$methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY);

list($defaults, $requirements, $options) = $this->parseConfigs($node, $path);
list($defaults, $requirements, $options, $condition) = $this->parseConfigs($node, $path);

$route = new Route($node->getAttribute('path'), $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods);
$route = new Route($node->getAttribute('path'), $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition);
$collection->add($id, $route);
}

Expand All @@ -157,7 +157,7 @@ protected function parseImport(RouteCollection $collection, \DOMElement $node, $
$schemes = $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY) : null;
$methods = $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY) : null;

list($defaults, $requirements, $options) = $this->parseConfigs($node, $path);
list($defaults, $requirements, $options, $condition) = $this->parseConfigs($node, $path);

$this->setCurrentDir(dirname($path));

Expand Down Expand Up @@ -211,6 +211,7 @@ private function parseConfigs(\DOMElement $node, $path)
$defaults = array();
$requirements = array();
$options = array();
$condition = null;

foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) {
switch ($n->localName) {
Expand All @@ -228,11 +229,14 @@ private function parseConfigs(\DOMElement $node, $path)
case 'option':
$options[$n->getAttribute('key')] = trim($n->textContent);
break;
case 'condition':
$condition = trim($n->textContent);
break;
default:
throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement" or "option".', $n->localName, $path));
}
}

return array($defaults, $requirements, $options);
return array($defaults, $requirements, $options, $condition);
}
}
5 changes: 3 additions & 2 deletions src/Symfony/Component/Routing/Loader/YamlFileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
class YamlFileLoader extends FileLoader
{
private static $availableKeys = array(
'resource', 'type', 'prefix', 'pattern', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options',
'resource', 'type', 'prefix', 'pattern', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition'
);
private $yamlParser;

Expand Down Expand Up @@ -123,8 +123,9 @@ protected function parseRoute(RouteCollection $collection, $name, array $config,
$host = isset($config['host']) ? $config['host'] : '';
$schemes = isset($config['schemes']) ? $config['schemes'] : array();
$methods = isset($config['methods']) ? $config['methods'] : array();
$condition = isset($config['condition']) ? $config['condition'] : null;

$route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods);
$route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition);

$collection->add($name, $route);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<xsd:element name="default" nillable="true" type="element" />
<xsd:element name="requirement" type="element" />
<xsd:element name="option" type="element" />
<xsd:element name="condition" type="condition" />
</xsd:choice>
</xsd:group>

Expand Down Expand Up @@ -61,4 +62,9 @@
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>

<xsd:simpleType name="condition">
<xsd:restriction base="xsd:string">
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public function dump(array $options = array())
$prevHostRegex = '';

foreach ($this->getRoutes()->all() as $name => $route) {
if ($route->getCondition()) {
throw new \LogicException(sprintf('Unable to dump the routes for Apache as route "%s" has a condition.', $name));
}

$compiledRoute = $route->compile();
$hostRegex = $compiledRoute->getHostRegex();
Expand Down
24 changes: 22 additions & 2 deletions src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

/**
* PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes.
Expand All @@ -23,6 +24,8 @@
*/
class PhpMatcherDumper extends MatcherDumper
{
private $expressionLanguage;

/**
* Dumps a set of routes to a PHP class.
*
Expand Down Expand Up @@ -91,6 +94,8 @@ public function match(\$pathinfo)
{
\$allow = array();
\$pathinfo = rawurldecode(\$pathinfo);
\$context = \$this->context;
\$request = \$this->request;
$code
Expand Down Expand Up @@ -237,6 +242,10 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
$hostMatches = true;
}

if ($route->getCondition()) {
$conditions[] = $this->getExpressionLanguage()->compile($route->getCondition(), array('context', 'request'));
}

$conditions = implode(' && ', $conditions);

$code .= <<<EOF
Expand All @@ -245,9 +254,8 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
EOF;

$gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name);
if ($methods) {
$gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name);

if (1 === count($methods)) {
$code .= <<<EOF
if (\$this->context->getMethod() != '$methods[0]') {
Expand Down Expand Up @@ -375,4 +383,16 @@ private function buildPrefixTree(DumperCollection $collection)

return $tree;
}

private function getExpressionLanguage()
{
if (null === $this->expressionLanguage) {
if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
}
$this->expressionLanguage = new ExpressionLanguage();
}

return $this->expressionLanguage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ public function match($pathinfo)
*/
protected function handleRouteRequirements($pathinfo, $name, Route $route)
{
// expression condition
if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), array('context' => $this->context, 'request' => $this->request))) {
return array(self::REQUIREMENT_MISMATCH, null);
}

// check HTTP scheme requirement
$scheme = $route->getRequirement('_scheme');
if ($scheme && $this->context->getScheme() !== $scheme) {
Expand Down
9 changes: 9 additions & 0 deletions src/Symfony/Component/Routing/Matcher/TraceableUrlMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ protected function matchCollection($pathinfo, RouteCollection $routes)
}
}

// check condition
if ($condition = $route->getCondition()) {
if (!$this->getExpressionLanguage()->evaluate($condition, array('context' => $this->context, 'request' => $this->request))) {
$this->addTrace(sprintf('Condition "%s" does not evaluate to "true"', $condition), self::ROUTE_ALMOST_MATCHES, $name, $route);

continue;
}
}

// check HTTP scheme requirement
if ($scheme = $route->getRequirement('_scheme')) {
if ($this->context->getScheme() !== $scheme) {
Expand Down
44 changes: 41 additions & 3 deletions src/Symfony/Component/Routing/Matcher/UrlMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

/**
* UrlMatcher matches URL based on a set of routes.
Expand All @@ -24,7 +26,7 @@
*
* @api
*/
class UrlMatcher implements UrlMatcherInterface
class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
{
const REQUIREMENT_MATCH = 0;
const REQUIREMENT_MISMATCH = 1;
Expand All @@ -45,6 +47,9 @@ class UrlMatcher implements UrlMatcherInterface
*/
protected $routes;

protected $request;
protected $expressionLanguage;

/**
* Constructor.
*
Expand Down Expand Up @@ -91,6 +96,20 @@ public function match($pathinfo)
: new ResourceNotFoundException();
}

/**
* {@inheritdoc}
*/
public function matchRequest(Request $request)
{
$this->request = $request;

$ret = $this->match($request->getPathInfo());

$this->request = null;

return $ret;
}

/**
* Tries to match a URL with a set of routes.
*
Expand Down Expand Up @@ -180,11 +199,18 @@ protected function getAttributes(Route $route, $name, array $attributes)
*/
protected function handleRouteRequirements($pathinfo, $name, Route $route)
{
// expression condition
if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), array('context' => $this->context, 'request' => $this->request))) {
return array(self::REQUIREMENT_MISMATCH, null);
}

// check HTTP scheme requirement
$scheme = $route->getRequirement('_scheme');
$status = $scheme && $scheme !== $this->context->getScheme() ? self::REQUIREMENT_MISMATCH : self::REQUIREMENT_MATCH;
if ($scheme && $scheme !== $this->context->getScheme()) {
return array(self::REQUIREMENT_MISMATCH, null);
}

return array($status, null);
return array(self::REQUIREMENT_MATCH, null);
}

/**
Expand All @@ -205,4 +231,16 @@ protected function mergeDefaults($params, $defaults)

return $defaults;
}

protected function getExpressionLanguage()
{
if (null === $this->expressionLanguage) {
if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
}
$this->expressionLanguage = new ExpressionLanguage();
}

return $this->expressionLanguage;
}
}
Loading

0 comments on commit d477f15

Please sign in to comment.