The TryingToScale PHP framework.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
tts_framework/src/classes/router.php

655 lines
20 KiB

<?php
declare(strict_types=1);
/**
* @license MIT
* @copyright (c) 2018, Patrik Mokrý
* @author Patrik Mokrý
* @link https://github.com/MokryPatrik/PHP-Router
*/
namespace tts;
class router
{
/**
* @var self $instance
*/
private static $instance = null;
/**
* @var null $URL
*/
public static $URL = null;
/**
* @var null $REQUEST
*/
public static $REQUEST = null;
/**
* @var array $params
*/
public static $params = [];
/**
* @var array $queryParams
*/
private static $queryParams = [];
/**
* This static property holds all routes
*
* @var array $routes
*/
private static $routes = [];
/**
* Return current route
*
* @var string
*/
public static $route = "";
/**
* Last route added
*
* @var string
*/
private static $last = null;
/**
* @var string
*/
private static $prefix = null;
/**
* @var string
*/
private static $name = null;
/**
* @var array $doNotIncludeInParams
*/
private static $doNotIncludeInParams = [];
/**
* @var array
*/
private static $shortcuts = [
'i' => '(\d+)',
's' => '(\w+)',
'locale' => '(sk|en)->en'
];
/**
* Init new self instance
*/
public static function init() {
self::$instance = new self();
}
/**
* Assign name to route
*
* @param $name
*/
public function name($name)
{
$route = self::$routes[self::$last];
unset(self::$routes[self::$last]);
self::$routes[self::$name . $name] = $route;
}
/**
* Add custom shortcut
* shortcut with default value -> $regex = (val1|val2)->val1;
*
* @param $shortcut
* @param $regex
*/
public static function addShortcut($shortcut, $regex)
{
self::$shortcuts[$shortcut] = $regex;
}
/**
* Get link from route name and params
*
* @param $route
* @param array $params
* @param $absolute
* @return null
*/
public static function link($route, $params = [], $absolute = true)
{
if (!isset(self::$routes[$route])) return null;
$route = self::$routes[$route];
$link = "";
foreach ($route['params'] as $key => $param) {
if (isset($param['real'])) {
$link .= $param['pattern'] . '/';
} else if (isset($params[$param['name']])) {
// Chcek if param has default
if (isset($param['default']) && $param['default'] !== $params[$param['name']]) {
$link .= $params[$param['name']] . '/';
}
}
}
// Add absolute path
if ($absolute) {
$link = self::$URL . $link;
}
// Cut slash at the end
return rtrim($link, '/');
}
/**
* Redirict to specific route or defined location
*
* @param $route
* @param array $params
*/
public static function redirect($route, $params = [])
{
if (isset(self::$routes[$route])) {
$route = self::link($route, $params);
}
header('Location: ' . $route, true);
die();
}
/**
* Create prefixed routes
*
* @param $prefix
* @param $callback
* @param string $name
* @param array $doNotIncludeInParams
*/
public static function prefix($prefix, $callback, $name = '', $doNotIncludeInParams = [])
{
self::$prefix = $prefix;
self::$name = $name;
self::$doNotIncludeInParams = $doNotIncludeInParams;
call_user_func($callback);
self::$prefix = null;
self::$name = null;
self::$doNotIncludeInParams = [];
}
/**
* Create route
*
* @param $route
* @param $action
* @param array $method
* @return Router
*/
public static function route($route, $action, $method = ["POST", "GET"])
{
$prefix = self::$prefix;
if (empty($route) && !empty(self::$prefix)) {
$prefix = rtrim(self::$prefix, '/');
}
$explodedRoute = explode("/", $prefix . $route);
$params = [];
$pattern = "";
// create route
foreach ($explodedRoute as $key => $r) {
if (strpos($r, '}?') !== false) {
$r = self::dynamic($r, $params, true);
$pattern = substr($pattern, 0, -1);
$dyn = true;
} else if (strpos($r, '}') !== false) {
$r = self::dynamic($r, $params, false);
$pattern = substr($pattern, 0, -1);
$dyn = true;
} else {
$params[] = [
'pattern' => $r,
'real' => false
];
}
$pattern .= ($key == 0 && !isset($dyn) ? '/' : '') . $r . '/';
}
// Create pattern
$pattern = substr($pattern, 0, -1) . '/?';
$pattern = '~^' . str_replace('/', '\/', $pattern) . '$~';
// Save data to static property
$name = uniqid();
self::$routes[$name] = [
'route' => $route,
'pattern' => $pattern,
'params' => $params,
'action' => $action,
'method' => $method,
'doNotIncludeInParams' => self::$doNotIncludeInParams
];
// Set last added
self::$last = $name;
// return self instance
if (self::$instance == null) {
self::init();
}
return self::$instance;
}
/**
* Helper for route with GET method
*
* @param $route
* @param $action
* @return self
*/
public static function get($route, $action)
{
return self::route($route, $action, ['GET']);
}
/**
* Helper for route with POST method
*
* @param $route
* @param $action
* @return self
*/
public static function post($route, $action)
{
return self::route($route, $action, ['POST']);
}
/**
* Helper for route with PUT method
*
* @param $route
* @param $action
* @return self
*/
public static function put($route, $action)
{
return self::route($route, $action, ['PUT']);
}
/**
* Helper for route with DELETE method
*
* @param $route
* @param $action
* @return self
*/
public static function delete($route, $action)
{
return self::route($route, $action, ['DELETE']);
}
/**
* Helper for route with GET, POST, PUT, DELETE method
*
* @param $route
* @param $action
* @return self
*/
public static function any($route, $action)
{
return self::route($route, $action, ['GET', 'POST', 'PUT', 'DELETE']);
}
/**
* Create all routes for resource (CRUD)
*
* @param $route
* @param $controller
* @param string $name
* @param string $shortcut
*/
public static function resource($route, $controller, $name = null, $shortcut = 's')
{
self::$name = self::$name . $name;
self::get($route . '/{page::paginator}?', $controller . "@index")->name('index');
self::get($route . "/create", $controller . "@create")->name('create');
self::post($route, $controller . "@store")->name('store');
self::get($route . "/{id::" . $shortcut . "}", $controller . "@show")->name('show');
self::get($route . "/{id::" . $shortcut . "}/edit", $controller . "@edit")->name('edit');
self::put($route . "/{id::" . $shortcut . "}", $controller . "@update")->name('update');
self::delete($route . "/{id::" . $shortcut . "}", $controller . "@destroy")->name('destroy');
self::get($route . "/{id::" . $shortcut . "}/delete", $controller . "@destroy")->name('destroy_');
self::$name = str_replace($name, '', self::$name);
}
/**
* Handle dynamic parameter in route
*
* @param string $route
* @param array $params
* @param boolean $optional
* @return mixed
*/
private static function dynamic($route, &$params, $optional = false)
{
$shortcut = self::rules($route);
$name = str_replace('{', '', $route);
if (!$optional) $name = str_replace('}', '', $name);
else $name = str_replace('}?', '', $name);
if (strpos($name, '::')) {
$name = substr($name, 0, strpos($name, "::"));
}
if (array_search($name, array_column($params, 'name')) === false) {
$pattern = str_replace('(', '(/', $shortcut['shortcut']);
$pattern = str_replace('|', '|/', $pattern);
// If is optional add ? at the end of pattern
if ($optional) {
$pattern .= '?';
}
$params[] = [
'name' => $name,
'pattern' => $pattern,
'default' => $shortcut['default']
];
} else {
die('Parameter with name ' . $name . ' has been already defined');
}
return $pattern;
}
/**
* Dynamic route rules
*
* @param $route
* @return mixed
*/
private static function rules($route)
{
if (preg_match('~::(.*?)}~', $route, $match)) {
list(, $shortcut) = $match;
if (isset(self::$shortcuts[$shortcut])) {
// Try to get default value from shortcut
$default = '';
$shortcut = self::$shortcuts[$shortcut];
if (preg_match('~->(.*?)$~', $shortcut, $match)) {
$default = $match[1];
$shortcut = str_replace($match[0], '', $shortcut);
}
// Return shortcut and default value
return [
'shortcut' => $shortcut,
'default' => $default
];
}
}
return [
'shortcut' => self::$shortcuts['s'],
'default' => ''
];
}
/**
* get_all_routes -> Added by Robert Strutts to auto load routes
* Namespace must start with prj
* Requires a Route to have an initial Projects Folder
* followed by a routes folder, then
* either an routes.php or test_routes.php files must exists
* with a method called get!
*/
private static function get_all_routes(bool $testing = false) {
$prj = rtrim(\bs_tts\site_helper::get_project(), '/');
if (!empty($prj)) {
$route_name = (\tts\console_app::is_cli()) ? "cli_routes" : "routes";
$routes = ($testing) ? "test_routes" : $route_name;
$routes_class = "\\prj\\{$prj}\\routes\\{$routes}";
if (class_exists($routes_class)) {
if (method_exists($routes_class, "get")) {
$callback = "{$routes_class}::get";
call_user_func($callback);
}
}
}
}
/**
* Execute router
*/
public static function execute() {
$ROOT = \bs_tts\site_helper::get_root();
$request_uri = \bs_tts\site_helper::get_uri();
$request_method = \bs_tts\site_helper::get_method();
$testing = \bs_tts\site_helper::get_testing();
// Generate request and absolute path
self::generateURL($ROOT, $request_uri);
// Fecth all Routes from the Project
self::get_all_routes($testing);
// Get query string
$request = explode('?', $request_uri);
$queryParams = [];
if (isset($request[1])) {
$queryParams = $request[1];
parse_str($queryParams, $queryParams);
self::$queryParams = $queryParams;
}
// Modify request
$request = '/' . trim(self::$REQUEST, '/');
// Find route
foreach (self::$routes as $routeKey => $route) {
$post_method = \tts\misc::post_var("_method");
$matchMethod = in_array($request_method, $route['method']) || ($post_method !== null
&& in_array($post_method, $route['method']));
if (preg_match($route['pattern'], $request, $match) && $matchMethod) {
// Default variables
$explodedRequest = explode('/', ltrim($request, '/'));
$routeParams = $route['params'];
$params = [];
// Match request params with params in array - static params
foreach ($explodedRequest as $key => $value) {
foreach ($routeParams as $k => $routeParam) {
// Go to the next request part
if (isset($routeParam['real']) && $routeParam['pattern'] == $value) {
unset($routeParams[$k]);
unset($explodedRequest[$key]);
break;
}
}
}
// Match request params with params in array - dynamic params
foreach ($explodedRequest as $key => $value) {
foreach ($routeParams as $k => $routeParam) {
if ($k >= $key && ($k - $key) < 2) {
if (preg_match('~' . $routeParam['pattern'] . '~', '/' . $value, $match)) {
$params[$routeParam['name']] = $value;
unset($routeParams[$k]);
}
}
}
}
// Last try to assign params - only with default values
foreach ($routeParams as $k => $routeParam) {
if (!isset($routeParam['default'])) continue;
$params[$routeParam['name']] = $routeParam['default'];
}
// Resort params to default order
$resortedParams = [];
foreach ($route['params'] as $k => $routeParam) {
if (isset($routeParam['name']) && isset($params[$routeParam['name']])) {
$resortedParams[$routeParam['name']] = $params[$routeParam['name']];
}
}
// Merge with query params
$resortedParams = array_merge($resortedParams, $queryParams);
self::$params = $resortedParams;
// Check if can redirect to some defaults
$link = self::link($routeKey, $resortedParams, false);
if (trim($request, '/') !== $link) {
header('Location: ' . self::$URL . $link . (!empty(self::$queryParams) ? '?' . http_build_query(self::$queryParams) : ''));
}
// Setup default route and url
self::$route = $route;
// If is post request
if (in_array('POST', $route['method']) || in_array('PUT', $route['method']) || in_array('DELETE', $route['method'])) {
$contentType = isset($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : '';
if (strcasecmp($contentType, 'application/json') !== false) {
$post = json_decode(file_get_contents('php://input'), true);
} else {
$post = $_POST;
}
array_unshift($resortedParams, $post);
}
// Check if there are some unwanted params
$params = $resortedParams;
foreach ($resortedParams as $key => $value) {
if (in_array($key, $route['doNotIncludeInParams']) && !is_numeric($key)) {
unset($resortedParams[$key]);
}
}
// Call action
if (is_callable($route['action'])) {
$returned = call_user_func_array($route['action'], $resortedParams);
return ["found"=> true, "returned"=> $returned];
} else if (strpos($route['action'], '@') !== false) {
// call controller
list($controller, $method) = explode('@', $route['action']);
// init new controller
$controller = new $controller;
// Check if class has parent
$parentControllers = class_parents($controller);
if (!empty($parentControllers)) {
end($parentControllers);
$parentController = $parentControllers[key($parentControllers)];
$parentController = new $parentController;
// Add properties to parent class
foreach ($params as $key => $value) {
$parentController::$params[$key] = $value;
}
}
//Call method
if (method_exists($controller, $method)) {
$returned = call_user_func_array([$controller, $method], $resortedParams);
return ["found"=> true, "returned"=> $returned];
}
}
}
}
return ["found"=>false];
}
/**
* Generate URL
*
* @param $ROOT
*/
private static function generateURL(string $ROOT, string $request_uri)
{
$https = \bs_tts\safer_io::get_clean_server_var('HTTPS');
$baseLink = ($https === 'on') ? "https" : "http";
$server_name = \bs_tts\safer_io::get_clean_server_var('SERVER_NAME');
$baseLink .= "://" . $server_name;
$port = \bs_tts\safer_io::get_clean_server_var('SERVER_PORT');
$baseLink .= ($port !== '80') ? ':' . $port : ':';
$baseRequest = '';
$request = $request_uri;
foreach (explode('/', $ROOT) as $key => $value) {
if ($value == '') continue;
if (preg_match('~/' . $value . '~', $request)) {
$baseRequest .= $value . '/';
}
$request = preg_replace('~/' . $value . '~', '', $request, 1);
}
self::$URL = $baseLink . '/' . $baseRequest;
self::$REQUEST = explode('?', $request)[0];
}
}
/**
* Useage
*
\tts\router::get('example/{id}?', "mockup\controllers\app\home_ctrl@test");
Router::get('example', function() {
return 'This route responds to requests with the GET method at the path /example';
});
Router::get('example/{id}', function($id) {
return 'This route responds to requests with the GET method at the path /example/<anything>';
});
Router::get('example/{id}?', function() {
return 'This route responds to requests with the GET method at the path /example/[optional]';
});
Router::post('example', function() {
return 'This route responds to requests with the POST method at the path /example';
});
Router::execute(__DIR__);
*/
/**
Regex Shortcuts
:i => (\d+) # numbers only
:s => (\w+) # any word, character
use in routes:
'/user/{name::i}'
'/user/{name::s}'
Custom shortcuts
Router::addShortcut(name, regex)
// create shortcut with default value
Router::addShortcut('locale', '(sk|en)->sk)')
Named Routes for Reverse Routing
Router::get('example/{id}', function() {
return 'example';
})->name('example');
echo Router::link('example', ['id' => 48]) // example/48
Prefix Groups
// If you need some prefix e.g. admin, api, locale, ...
Router::prefix('admin/', function() {
Router::get('example/{id}', function() {
return 'example';
});
});
*/