Files
2024-07-15 11:28:08 +02:00

356 lines
7.4 KiB
PHP

<?php
/**
* @package awf
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Awf\Router;
use Awf\Application\Application;
use Awf\Container\Container;
use Awf\Uri\Uri;
class Router
{
/**
* The container this router is attached to
*
* @var Container|null
*/
protected $container = null;
/**
* The routing rules for the application
*
* @var array[Rule]
*/
protected $rules = array();
/**
* Public constructor
*
* @param Container $container The container this router is attached to
*
* @return Router
*
* @codeCoverageIgnore
*/
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* Add a routing rule to the stack
*
* @param Rule $rule The routing rule to add
*
* @return void
*
* @codeCoverageIgnore
*/
public function addRule(Rule $rule)
{
$this->rules[] = $rule;
}
/**
* Add a routing rule to the stack from an array definition
*
* @param array $definition The definition of the routing rule to add, @see Route::__construct()
*
* @return void
*
* @codeCoverageIgnore
*/
public function addRuleFromDefinition(array $definition)
{
$rule = new Rule($definition);
$this->addRule($rule);
}
/**
* Add multiple rules in one go. The array can contain either Rule objects or rule definitions in array format
*
* @param array $rules The rules to add
*
* @return void
*
* @codeCoverageIgnore
*/
public function addRules(array $rules)
{
if (!empty($rules))
{
foreach ($rules as $rule)
{
if (is_array($rule))
{
$this->addRuleFromDefinition($rule);
continue;
}
if (!is_object($rule))
{
continue;
}
if (!($rule instanceof Rule))
{
continue;
}
$this->addRule($rule);
}
}
}
/**
* Clear all routing rules
*
* @return void
*
* @codeCoverageIgnore
*/
public function clearRules()
{
$this->rules = array();
}
/**
* Put a URL through the routing rules and return the routed URL.
*
* @param string $url The URL to route
* @param boolean $rebase Should I rebase the resulting URL? False to return a relative URL to the application's
* live_site and base_url, as defined in the application configuration
*
* @return string The routed URL
*/
public function route($url, $rebase = true)
{
// Initialise
$routeResult = null;
// Use the routing rules to produce a list of segments
if (!empty($this->rules))
{
/** @var Rule $rule */
foreach ($this->rules as $rule)
{
$routeResult = $rule->route($url);
if (is_array($routeResult))
{
break;
}
}
}
// Parse the URL into an object
$uri = new Uri($url);
if (!is_null($routeResult))
{
// We'll replace the path and query string parameters with what the routing rule sent us
$uri->setPath(implode('/', $routeResult['segments']));
$uri->setQuery($routeResult['vars']);
}
// Do we have to rebase?
if ($rebase)
{
// Get the base URL
$baseUrl = $this->container->appConfig->get('base_url', '');
if (empty($baseUrl))
{
$baseUrl = '';
}
$baseUrl = rtrim($baseUrl, '/');
if ((strpos($baseUrl, 'http://') === 0) || (strpos($baseUrl, 'https://') === 0))
{
$base = $baseUrl;
}
else
{
$base = Uri::base(false, $this->container);
$base = rtrim($base, '/') . '/' . $baseUrl;
}
$rebaseURI = new Uri($base);
// Merge the paths of the rebase and routed URIs. However if the router URI contains index.php
// and the base URL ends in a .php script do not append index.php to the other .php script
// (this is required for the WP integration to work properly)
if (!(($uri->getPath() == 'index.php') && (substr($rebaseURI->getPath(), -4) == '.php')))
{
$rebaseURI->setPath(rtrim($rebaseURI->getPath(), '/') . '/' . $uri->getPath());
}
// Merge the query string parameters of the rebase and routed URIs
$vars_routed = $uri->getQuery(true);
$vars_rebase = $rebaseURI->getQuery(true);
$rebaseURI->setQuery(array_merge($vars_rebase, $vars_routed));
// We'll return the rebased URI
$uri = $rebaseURI;
}
return $uri->toString();
}
/**
* Parse a routed URL based on the routing rules, setting the input variables of the attached application.
*
* @param string $url The URL to parse. If omitted the current URL will be used.
* @param boolean $rebase Is this a rebased URL? If false we assume we're given a relative URL.
*/
public function parse($url = null, $rebase = true)
{
// If we are not given a URL, use the current URL of the site
if (empty($url))
{
$url = Uri::current();
}
// Initialise
$removePath = null;
$removeVars = null;
if ($rebase)
{
// Get the base URL
$baseUrl = $this->container->appConfig->get('base_url', '');
if (empty($baseUrl))
{
$baseUrl = '';
}
$baseUrl = rtrim($baseUrl, '/');
$base = Uri::base(false, $this->container);
$base = rtrim($base, '/') . '/' . $baseUrl;
$rebaseURI = new Uri($base);
// Get the path and vars to remove from the parsed route
$removePath = $rebaseURI->getPath();
$removePath = trim($removePath, '/');
$removeVars = $rebaseURI->getQuery(true);
}
$uri = new Uri($url);
$path = $uri->getPath();
$path = trim($path, '/');
// Remove the $removePath
if (!empty($removePath))
{
if (strpos($path, $removePath) === 0)
{
$path = substr($path, strlen($removePath));
}
}
// Use the routing rules to parse the URL
$routeVars = null;
if (!empty($this->rules))
{
/** @var Rule $rule */
foreach ($this->rules as $rule)
{
$routeVars = $rule->parse($path);
if (is_array($routeVars))
{
break;
}
}
}
if (is_null($routeVars))
{
$routeVars = array();
}
// Mix route and URI vars
$uriVars = $uri->getQuery(true);
$routeVars = array_merge($routeVars, $uriVars);
// Eliminate $removeVars
if (is_array($removeVars) && !empty($removeVars))
{
foreach ($removeVars as $k => $v)
{
if (isset($routeVars[$k]) && ($routeVars[$k] == $v))
{
unset($routeVars[$k]);
}
}
}
// Set the query vars to the application
if (is_array($routeVars) && !empty($routeVars))
{
foreach ($routeVars as $k => $v)
{
$this->container->input->set($k, $v);
}
}
}
/**
* Exports the routing maps as a JSON string
*
* @return string The routes in JSON format
*/
public function exportRoutes()
{
$maps = array();
if (!empty($this->rules))
{
/** @var Rule $rule */
foreach ($this->rules as $rule)
{
$maps[] = array(
'path' => $rule->getPath(),
'types' => $rule->getTypes(),
'matchVars' => $rule->getMatchVars(),
'pushVars' => $rule->getPushVars(),
'routeCallable' => $rule->getRouteCallable(),
'parseCallable' => $rule->getParseCallable(),
);
}
}
$options = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0;
return json_encode($maps, $options);
}
/**
* Imports routes from a JSON string
*
* @param string $json The JSON string to parse
* @param boolean $replace [optional] Should I replace existing routes?
*
* @return void
*/
public function importRoutes($json, $replace = true)
{
$definitions = json_decode($json, true);
if ($replace)
{
$this->clearRules();
}
$this->addRules($definitions);
}
}