Skip to content

Commit

Permalink
ControllerHandler and RestHandler refactoring.
Browse files Browse the repository at this point in the history
The ControllerHandler and RestHandler have been refactored to reuse logic from
the PatternMatchhandler to use FastRoute to perform any route matching.
  • Loading branch information
Daniel Bruce committed Dec 31, 2014
1 parent 6541cda commit ed40fca
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 129 deletions.
45 changes: 40 additions & 5 deletions docs/handlers/rest_handler.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

The class `Vectorface\SnappyRouter\Handler\RestHandler` provides a simple
"by convention" api routing handler that builds on top of the
[controller handler](/handlers/controller_handler) by mapping specific route
[controller handler](handlers/controller_handler) by mapping specific route
patterns to controllers and actions.

## Rest Routing

The following route patterns are supported:

```
/(optional/base/path/)v{$apiVersion}/${controller}/${objectId}/${action}(/$additionalParameters...)
/(optional/base/path/)v{$apiVersion}/${controller}/${objectId}/${action}
/(optional/base/path/)v{$apiVersion}/${controller}/${action}/${objectId}
/(optional/base/path/)v{$apiVersion}/${controller}/${action}
/(optional/base/path/)v{$apiVersion}/${controller}/${objectId}
/(optional/base/path/)v{$apiVersion}/${controller}
Expand All @@ -19,8 +20,8 @@ The following route patterns are supported:
Examples:

```
/api/v1.2/users/1234/save/relationships
/api/v1.2/users/1234/save
/api/v1.2/users/1234/details
/api/v1.2/users/details/1234
/api/v1.2/users/search
/api/v1.2/users/1234
/api/v1.2/users
Expand All @@ -38,7 +39,7 @@ Example:
```php
<?php

namespace Vendor/MyNamespace/Handler;
namespace Vendor\MyNamespace\Handler;

use \Exception;
use Vectorface\SnappyRouter\Handler\RestHandler;
Expand All @@ -57,3 +58,37 @@ class MyRestHandler extends RestHandler
}
```

## Writing a Restful Controller

Similar to the [controller handler](handlers/controller_handler), the
controller class should subclass
`Vectorface\SnappyRouter\Controller\AbstractController`. A key difference
between the REST handler and the controller handler is that the route
parameters will always have the API version as the first element. If present
in the route, the `${objectId}` will be second element of the route parameters.

Note that the return value of the action will be encoded as a JSON string
automatically.

Example controller:
```php
<?php

namespace Vendor\MyNamespace\Controller;

use \Exception;
use Vectorface\SnappyRouter\Controller\AbstractController;

class RestUsersController extends AbstractController
{
public function detailsAction($routeParams)
{
$apiVersion = array_pop($routeParams);
if (empty($routeParams)) {
throw new Exception('Missing user ID');
}
$user = ModelLayer::getUser(array_pop($routeParams));
return $user->getDetails();
}
}
```
138 changes: 82 additions & 56 deletions src/Vectorface/SnappyRouter/Handler/ControllerHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
namespace Vectorface\SnappyRouter\Handler;

use \Exception;
use FastRoute\Dispatcher;
use Vectorface\SnappyRouter\Controller\AbstractController;
use Vectorface\SnappyRouter\Di\Di;
use Vectorface\SnappyRouter\Encoder\EncoderInterface;
use Vectorface\SnappyRouter\Encoder\NullEncoder;
use Vectorface\SnappyRouter\Encoder\TwigViewEncoder;
use Vectorface\SnappyRouter\Exception\HandlerException;
use Vectorface\SnappyRouter\Exception\ResourceNotFoundException;
use Vectorface\SnappyRouter\Request\HttpRequest;
use Vectorface\SnappyRouter\Response\AbstractResponse;
Expand All @@ -20,7 +19,7 @@
* @copyright Copyright (c) 2014, VectorFace, Inc.
* @author Dan Bruce <dbruce@vectorface.com>
*/
class ControllerHandler extends AbstractRequestHandler
class ControllerHandler extends PatternMatchHandler
{
/** Options key for the base path */
const KEY_BASE_PATH = 'basePath';
Expand All @@ -36,6 +35,14 @@ class ControllerHandler extends AbstractRequestHandler
/** The current route parameters */
protected $routeParams;

/** Constants indicating the type of route */
const MATCHES_NOTHING = 0;
const MATCHES_CONTROLLER = 1;
const MATCHES_ACTION = 2;
const MATCHES_CONTROLLER_AND_ACTION = 3;
const MATCHES_PARAMS = 4;
const MATCHES_CONTROLLER_ACTION_AND_PARAMS = 7;

/**
* Returns true if the handler determines it should handle this request and false otherwise.
* @param string $path The URL path for the request.
Expand All @@ -48,63 +55,38 @@ public function isAppropriate($path, $query, $post, $verb)
{
// remove the leading base path option if present
$options = $this->getOptions();
if (isset($options[self::KEY_BASE_PATH])) {
$path = $this->extractPathFromBasePath($path, $options[self::KEY_BASE_PATH]);
}
$path = $this->extractPathFromBasePath($path, $options);

// remove the leading /
if (0 === strpos($path, '/')) {
$path = substr($path, 1);
// extract the controller, action and route parameters if present
// and fall back to defaults when not present
$controller = 'index';
$action = 'index';
$this->routeParams = array();
$routeInfo = $this->getRouteInfo($verb, $path);
// ensure the path matches at least one of the routes
if (Dispatcher::FOUND !== $routeInfo[0]) {
return false;
}

// split the path components to find the controller, action and route parameters
$pathComponents = array_filter(array_map('trim', explode('/', $path)), 'strlen');
$pathComponentsCount = count($pathComponents);

// default values if not present
$controllerClass = 'index';
$actionName = 'index';
$this->routeParams = array();
switch ($pathComponentsCount) {
case 0:
break;
case 2:
$actionName = $pathComponents[1];
// fall through is intentional
case 1:
$controllerClass = $pathComponents[0];
break;
default:
$controllerClass = $pathComponents[0];
$actionName = $pathComponents[1];
$this->routeParams = array_slice($pathComponents, 2);
if ($routeInfo[1] & self::MATCHES_CONTROLLER) {
$controller = strtolower($routeInfo[2]['controller']);
if ($routeInfo[1] & self::MATCHES_ACTION) {
$action = strtolower($routeInfo[2]['action']);
if ($routeInfo[1] & self::MATCHES_PARAMS) {
$this->routeParams = explode('/', $routeInfo[2]['params']);
}
}
}
$controllerClass = strtolower($controllerClass);
$actionName = strtolower($actionName);
$defaultView = sprintf(
'%s/%s.twig',
$controllerClass,
$actionName
);
$controllerClass = ucfirst($controllerClass).'Controller';
$actionName = $actionName.'Action';

$controllerClass = ucfirst($controller).'Controller';
// ensure we actually handle the controller for this application
try {
$this->getServiceProvider()->getServiceInstance($controllerClass);
} catch (Exception $e) {
return false;
}

// configure the view encoder if they specify a view option
if (isset($options[self::KEY_VIEWS])) {
$this->encoder = new TwigViewEncoder(
$options[self::KEY_VIEWS],
$defaultView
);
} else {
$this->encoder = new NullEncoder();
}
$actionName = $action.'Action';
$this->configureViewEncoder($options, $controller, $action);

// configure the request object
$this->request = new HttpRequest(
Expand All @@ -121,7 +103,7 @@ public function isAppropriate($path, $query, $post, $verb)

/**
* Performs the actual routing.
* @return mixed Returns the result of the route.
* @return string Returns the result of the route.
*/
public function performRoute()
{
Expand All @@ -136,7 +118,7 @@ public function performRoute()
/**
* Returns a request object extracted from the request details (path, query, etc). The method
* isAppropriate() must have returned true, otherwise this method should return null.
* @return Returns a Request object or null if this handler is not appropriate.
* @return HttpRequest Returns a Request object or null if this handler is not appropriate.
*/
public function getRequest()
{
Expand All @@ -155,7 +137,7 @@ public function getEncoder()
/**
* Sets the encoder to be used by this handler (overriding the default).
* @param EncoderInterface $encoder The encoder to be used.
* @return Returns $this.
* @return ControllerHandler Returns $this.
*/
public function setEncoder(EncoderInterface $encoder)
{
Expand All @@ -166,13 +148,22 @@ public function setEncoder(EncoderInterface $encoder)
/**
* Returns the new path with the base path extracted.
* @param string $path The full path.
* @param string $basePath The base path to extract.
* @param array $options The array of options.
* @return string Returns the new path with the base path removed.
*/
protected function extractPathFromBasePath($path, $basePath)
protected function extractPathFromBasePath($path, $options)
{
$pos = strpos($path, $basePath);
return (false === $pos) ? $path : substr($path, $pos + strlen($basePath));
if (isset($options[self::KEY_BASE_PATH])) {
$pos = strpos($path, $options[self::KEY_BASE_PATH]);
if (false !== $pos) {
$path = substr($path, $pos + strlen($options[self::KEY_BASE_PATH]));
}
}
// ensure the path has a leading slash
if (empty($path) || $path[0] !== '/') {
$path = '/'.$path;
}
return $path;
}

/**
Expand Down Expand Up @@ -244,4 +235,39 @@ protected function invokeControllerAction(AbstractController $controller, $actio
);
return $response;
}

/**
* Configures the view encoder based on the current options.
* @param array $options The current options.
* @param string $controller The controller to use for the default view.
* @param string $action The action to use for the default view.
*/
private function configureViewEncoder($options, $controller, $action)
{
// configure the view encoder if they specify a view option
if (isset($options[self::KEY_VIEWS])) {
$this->encoder = new TwigViewEncoder(
$options[self::KEY_VIEWS],
sprintf('%s/%s.twig', $controller, $action)
);
} else {
$this->encoder = new NullEncoder();
}
}

/**
* Returns the array of routes.
* @return array The array of routes.
*/
protected function getRoutes()
{
return array(
'/' => self::MATCHES_NOTHING,
'/{controller}' => self::MATCHES_CONTROLLER,
'/{controller}/' => self::MATCHES_CONTROLLER,
'/{controller}/{action}' => self::MATCHES_CONTROLLER_AND_ACTION,
'/{controller}/{action}/' => self::MATCHES_CONTROLLER_AND_ACTION,
'/{controller}/{action}/{params:.+}' => self::MATCHES_CONTROLLER_ACTION_AND_PARAMS
);
}
}
41 changes: 33 additions & 8 deletions src/Vectorface/SnappyRouter/Handler/PatternMatchHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class PatternMatchHandler extends AbstractRequestHandler
'OPTIONS'
);

/** The route information from FastRoute */
private $routeInfo;

/**
* Returns true if the handler determines it should handle this request and false otherwise.
* @param string $path The URL path for the request.
Expand All @@ -40,20 +43,42 @@ class PatternMatchHandler extends AbstractRequestHandler
*/
public function isAppropriate($path, $query, $post, $verb)
{
$options = $this->getOptions();
$dispatcher = $this->getDispatcher($options['routes']);
$routeInfo = $dispatcher->dispatch(strtoupper($verb), $path);
switch ($routeInfo[0]) {
case Dispatcher::FOUND:
break;
default:
return false;
$routeInfo = $this->getRouteInfo($verb, $path);
if (Dispatcher::FOUND !== $routeInfo[0]) {
return false;
}
$this->callback = $routeInfo[1];
$this->routeParams = isset($routeInfo[2]) ? $routeInfo[2] : array();
return true;
}

/**
* Returns the array of route info from the routing library.
* @param string $verb The HTTP verb used in the request.
* @param string $path The path to match against the patterns.
* @param boolean $useCache (optional) An optional flag whether to use the
* cached route info or not. Defaults to false.
* @return array Returns the route info as an array.
*/
protected function getRouteInfo($verb, $path, $useCache = false)
{
if (!$useCache || !isset($this->routeInfo)) {
$dispatcher = $this->getDispatcher($this->getRoutes());
$this->routeInfo = $dispatcher->dispatch(strtoupper($verb), $path);
}
return $this->routeInfo;
}

/**
* Returns the array of routes.
* @return array The array of routes.
*/
protected function getRoutes()
{
$options = $this->getOptions();
return isset($options[self::KEY_ROUTES]) ? $options[self::KEY_ROUTES] : array();
}

/**
* Performs the actual routing.
* @return mixed Returns the result of the route.
Expand Down
Loading

0 comments on commit ed40fca

Please sign in to comment.