Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WiP] NestedMatcher support #30

Merged
merged 3 commits into from
Dec 7, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions NestedMatcher/FinalMatcherInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Symfony\Cmf\Component\Routing\NestedMatcher;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;

/**
* A FinalMatcher returns only one route from a collection of candidate routes.
*/
interface FinalMatcherInterface extends RequestMatcherInterface {

/**
* Sets the route collection this matcher should use.
*
* @param \Symfony\Component\Routing\RouteCollection $collection
* The collection against which to match.
*
* @return \Drupal\Core\Routing\FinalMatcherInterface
* The current matcher.
*/
public function setCollection(RouteCollection $collection);
}
76 changes: 76 additions & 0 deletions NestedMatcher/FirstEntryFinalMatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace Symfony\Cmf\Component\Routing\NestedMatcher;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

/**
* Final matcher that simply returns the first item in the remaining routes.
*
* This class simply matches the first remaining route.
*/
class FirstEntryFinalMatcher implements FinalMatcherInterface {

/**
* The RouteCollection this matcher should match against.
*
* @var RouteCollection
*/
protected $routes;

/**
* Sets the route collection this matcher should use.
*
* @param \Symfony\Component\Routing\RouteCollection $collection
* The collection against which to match.
*
* @return \Drupal\Core\Routing\FinalMatcherInterface
* The current matcher.
*/
public function setCollection(RouteCollection $collection) {
$this->routes = $collection;

return $this;
}

/**
* Implements Drupal\Core\Routing\FinalMatcherInterface::matchRequest().
*/
public function matchRequest(Request $request) {
// Return whatever the first route in the collection is.
foreach ($this->routes as $name => $route) {
$path = $request->getPathInfo();

$compiled = $route->compile();

preg_match($compiled->getRegex(), $path, $matches);

return array_merge($this->mergeDefaults($matches, $route->getDefaults()), array('_route' => $name));
}
}

/**
* Get merged default parameters.
*
* @param array $params
* The parameters.
* @param array $defaults
* The defaults.
*
* @return array
* Merged default parameters.
*/
protected function mergeDefaults($params, $defaults) {
$parameters = $defaults;
foreach ($params as $key => $value) {
if (!is_int($key)) {
$parameters[$key] = $value;
}
}

return $parameters;
}

}
53 changes: 53 additions & 0 deletions NestedMatcher/HttpMethodMatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Symfony\Cmf\Component\Routing\NestedMatcher;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;

/**
* This class filters routes based on their HTTP Method.
*/
class HttpMethodMatcher extends PartialMatcher {

/**
* Matches a request against multiple routes.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* A Request object against which to match.
*
* @return \Symfony\Component\Routing\RouteCollection
* A RouteCollection of matched routes.
*/
public function matchRequestPartial(Request $request) {
$possible_methods = array();

$method = $request->getMethod();

$collection = new RouteCollection();

foreach ($this->routes->all() as $name => $route) {
// _method could be a |-delimited list of allowed methods, or null. If
// null, we accept any method.
$allowed_methods = array_filter(explode('|', strtoupper($route->getRequirement('_method'))));
if (empty($allowed_methods) || in_array($method, $allowed_methods)) {
$collection->add($name, $route);
}
else {
// Build a list of methods that would have matched. Note that we only
// need to do this if a route doesn't match, because if even one route
// passes then we'll never throw the exception that needs this array.
$possible_methods += $allowed_methods;
}
}

if (!count($collection->all())) {
throw new MethodNotAllowedException(array_unique($possible_methods));
}

return $collection;
}

}

22 changes: 22 additions & 0 deletions NestedMatcher/InitialMatcherInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Symfony\Cmf\Component\Routing\NestedMatcher;

use Symfony\Component\HttpFoundation\Request;

/**
* A PartialMatcher works like a UrlMatcher, but will return multiple candidate routes.
*/
interface InitialMatcherInterface {

/**
* Matches a request against multiple routes.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* A Request object against which to match.
*
* @return \Symfony\Component\Routing\RouteCollection
* A RouteCollection of matched routes.
*/
public function matchRequestPartial(Request $request);
}
194 changes: 194 additions & 0 deletions NestedMatcher/NestedMatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
<?php

namespace Symfony\Cmf\Component\Routing\NestedMatcher;

use Symfony\Component\Routing\RequestContext;
use Symfony\Component\HttpFoundation\Request;

/**
* The nested matcher layers multiple partial matchers together.
*/
class NestedMatcher implements NestedMatcherInterface {

/**
* The final matcher.
*
* @var Symfony\Component\Routing\Matcher\RequestMatcherInterface
*/
protected $finalMatcher;

/**
* An array of PartialMatchers.
*
* @var array
*/
protected $partialMatchers = array();

/**
* Array of PartialMatcherInterface objects, sorted.
*
* @var type
*/
protected $sortedMatchers = array();

/**
* The initial matcher to match against.
*
* @var Drupal\core\Routing\InitialMatcherInterface
*/
protected $initialMatcher;

/**
* The request context.
*
* @var Symfony\Component\Routing\RequestContext
*/
protected $context;

/**
* Adds a partial matcher to the matching plan.
*
* Partial matchers will be run in the order in which they are added.
*
* @param \Drupal\Core\Routing\PartialMatcherInterface $matcher
* A partial matcher.
* @param int $priority
* (optional) The priority of the matcher. Higher number matchers will be checked
* first. Default to 0.
*
* @return NestedMatcherInterface
* The current matcher.
*/
public function addPartialMatcher(PartialMatcherInterface $matcher, $priority = 0) {
if (empty($this->matchers[$priority])) {
$this->matchers[$priority] = array();
}

$this->matchers[$priority][] = $matcher;
$this->sortedMatchers = array();
}

/**
* Sets the final matcher for the matching plan.
*
* @param \Drupal\Core\Routing\FinalMatcherInterface $final
* The matcher that will be called last to ensure only a single route is
* found.
*
* @return \Drupal\Core\Routing\NestedMatcherInterface
* The current matcher.
*/
public function setFinalMatcher(FinalMatcherInterface $final) {
$this->finalMatcher = $final;

return $this;
}

/**
* Sets the first matcher for the matching plan.
*
* Partial matchers will be run in the order in which they are added.
*
* @param \Drupal\Core\Routing\InitialMatcherInterface $matcher
* An initial matcher. It is responsible for its own configuration and
* initial route collection
*
* @return \Drupal\Core\Routing\NestedMatcherInterface
* The current matcher.
*/
public function setInitialMatcher(InitialMatcherInterface $initial) {
$this->initialMatcher = $initial;

return $this;
}

/**
* Tries to match a request with a set of routes.
*
* If the matcher can not find information, it must throw one of the
* exceptions documented below.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request to match.
*
* @return array
* An array of parameters.
*
* @throws ResourceNotFoundException
* If no matching resource could be found.
* @throws MethodNotAllowedException
* If a matching resource was found but the request method is not allowed.
*/
public function matchRequest(Request $request) {
$collection = $this->initialMatcher->matchRequestPartial($request);

foreach ($this->getPartialMatchers() as $matcher) {
if ($collection) {
$matcher->setCollection($collection);
}
$collection = $matcher->matchRequestPartial($request);
}

$attributes = $this->finalMatcher->setCollection($collection)->matchRequest($request);

return $attributes;
}

/**
* Sorts the matchers and flattens them.
*
* @return array
* An array of RequestMatcherInterface objects.
*/
public function getPartialMatchers() {
if (empty($this->sortedMatchers)) {
$this->sortedMatchers = $this->sortMatchers();
}

return $this->sortedMatchers;
}

/**
* Sort matchers by priority.
*
* The highest priority number is the highest priority (reverse sorting).
*
* @return \Symfony\Component\Routing\RequestMatcherInterface[]
* An array of Matcher objects in the order they should be used.
*/
protected function sortMatchers() {
$sortedMatchers = array();
krsort($this->matchers);

foreach ($this->matchers as $matchers) {
$sortedMatchers = array_merge($sortedMatchers, $matchers);
}

return $sortedMatchers;
}

/**
* Sets the request context.
*
* This method is unused. It is here only to satisfy the interface.
*
* @param \Symfony\Component\Routing\RequestContext $context
* The context
*/
public function setContext(RequestContext $context) {
$this->context = $context;
}

/**
* Gets the request context.
*
* This method is unused. It is here only to satisfy the interface.
*
* @return \Symfony\Component\Routing\RequestContext
* The context
*/
public function getContext() {
return $this->context;
}

}
Loading