Skip to content

Commit

Permalink
Major routing logic changes with wildcard routes implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
cpt-erwin committed Jan 26, 2021
1 parent 5870343 commit d362c7f
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 8 deletions.
142 changes: 142 additions & 0 deletions Route.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php

namespace Okami\Core;

use LogicException;

/**
* Class Route
*
* @author Michal Tuček <[email protected]>
* @package Okami\Core
*/
class Route
{
private array $paths;

/** @var array|callable|string $callback */
private $callback;

private array $params = [];

private array $patterns = [
'any' => '.*', // Any
'num' => '[0-9]+', // Numbers
'alpha' => '[a-zA-Z]+', // Letters
'alnum' => '[a-zA-Z0-9]+', // Letters & numbers
'slug' => '[a-zA-Z0-9\-\_]+', // Letters & numbers with dash & underscore signs as dividers
'id' => '[0-9]+', // Same as :num // FIXME: Is there a way to remove this duplicity?
];

/**
* Route constructor.
*
* @param string $path
* @param string|callable|array $callback
*/
public function __construct(string $path, $callback)
{
$this->paths = array_reverse($this->analyzePath($path));
$this->callback = $callback;
}

public function analyzePath(string $path, string $root = ''): array
{
// paths example
// ''
// '/'
// '/posts'
// '/posts/{id}'
// '/posts/{id:id}'
// '/posts[/{id:id}]'
// '/gallery/{galleryID:id}/image/{imageID:id}'
// '/gallery/{galleryID:id}[/image/{imageID:id}]'
// '/stock/{stockID:id}[/supplier/{supplierID:id}[/product/{productID:id}]]'

$matches = preg_split('/(\[.*?\]$)|(\{.*?\})/', $path, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
if($matches === false) {
throw new LogicException("Unexpected exception occurred while testing the path against the REGEX.");
}

$paths = [];

if (sizeof($matches) === 1) {
/** SIMPLE PATH **/
$paths[] = $matches[0];
} else {
/** WILDCARD PATH **/

// When the route like this occurs
// '/gallery/{galleryID:id}[/image/{imageID:id}]'
// we should generate two routes starting from the last optional one to first mandatory
// 1) '/gallery/{galleryID:id}/image/{imageID:id}'
// 2) '/gallery/{galleryID:id}'
// for the second case we still want imageID to be set with value null


foreach ($matches as $match) {
// If $match starts with { and ends with } then remove those signs
// and split string by : where first element of the returned array
// will be the name of the param and the second element will be its
// regex pattern to be matched.
// If the array has only one element the :any pattern will be selected.
if (preg_match('/^\{.*?\}$/', $match)) {
$param = explode(':', str_replace(['{', '}'], '', $match));
if(sizeof($param) === 1) {
$param[1] = 'any';
}
$this->params[] = $param[0];
$paths[array_key_last($paths)] .= '(' . $this->getPattern($param[1]) . ')';
continue;
}
if (preg_match('/^\[.*?\]$/', $match)) {
$root = $paths[array_key_last($paths)];
$subPaths = $this->analyzePath(substr($match, 1, -1), $root);
foreach ($subPaths as $subPath) {
$paths[] = $root . $subPath;
}
continue;
}

$paths[] = $match;
}
}
return $paths;
}

public function match(string $pathToMatch): bool
{
foreach($this->paths as $path) {
$pattern = '/^' . str_replace('/', '\/', $path) . '$/';
if(preg_match($pattern, $pathToMatch)) {
// FIXME: Make a constant for replacement delimiter @&#&@
$arguments = explode('@&#&@', preg_replace($pattern, '$1@&#&@$2@&#&@$3@&#&@$4', $pathToMatch));
$this->params = array_combine($this->params, array_slice($arguments, 0, sizeof($this->params))); // initialize params
array_walk($this->params, function(&$value) { $value = $value ?: null; }); // set empty params to null value
return true;
}
}
return false;
}

/**
* @return array|callable|string
*/
public function getCallback()
{
return $this->callback;
}

public function getParams(): array
{
return $this->params;
}

private function getPattern(string $pattern): string
{
if(!array_key_exists($pattern, $this->patterns)) {
throw new LogicException('Unknown pattern \'' . $pattern . '\' used!');
}
return $this->patterns[$pattern];
}
}
61 changes: 53 additions & 8 deletions Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Okami\Core;

use LogicException;
use Okami\Core\Exceptions\NotFoundException;

/**
Expand All @@ -28,36 +29,80 @@ public function __construct(Request $request, Response $response)
$this->response = $response;
}

/**
* @param string $path
* @param string|callable|array $callback
*/
public function get(string $path, $callback)
{
$this->routes['get'][$path] = $callback;
$this->addRoute('get', $path, $callback);
}

/**
* @param string $path
* @param string|callable|array $callback
*/
public function post(string $path, $callback)
{
$this->routes['post'][$path] = $callback;
$this->addRoute('post', $path, $callback);
}

/**
* @param string $method
* @param string $path
* @param string|callable|array $callback
*/
private function addRoute(string $method, string $path, $callback)
{
$route = new Route($path, $callback);
$this->routes[$method][] = $route;
}

public function resolve()
{
$path = $this->request->getPath();
$method = $this->request->method();
$callback = $this->routes[$method][$path] ?? false;
if ($callback === false) {

$route = $this->getRoute($method, $path);
if (is_null($route)) {
throw new NotFoundException();
}
if (is_string($callback)) {
return App::$app->view->renderView($callback);

/** RENDER TEMPLATE **/
if (is_string($route->getCallback())) {
return App::$app->view->renderView($route->getCallback());
}
if (is_array($callback)) {

/** CALL CONTROLLER **/
if (is_array($route->getCallback())) {
$callback = $route->getCallback();
App::$app->setController(new $callback[0]()); // create instance of passed controller
App::$app->controller->action = $callback[1];
$callback[0] = App::$app->getController();

foreach (App::$app->controller->getMiddlewares() as $middleware) {
$middleware->execute();
}
return call_user_func($callback, $this->request, $this->response, $route->getParams());
}

/** EXECUTE FUNCTION **/
if (is_callable($route->getCallback())) {
return call_user_func($route->getCallback(), $this->request, $this->response, $route->getParams());
}

// Shouldn't ever reach this statement but just to be sure...
throw new LogicException('Requires callback of type string|callable|array but callback with type ' . gettype($route->getCallback()) . ' passed instead!');
}

private function getRoute(string $method, string $path): ?Route
{
/** @var Route $route */
foreach ($this->routes[$method] as $route) {
if($route->match($path)) {
return $route;
}
}
return call_user_func($callback, $this->request, $this->response);
return null;
}
}

0 comments on commit d362c7f

Please sign in to comment.