src/Library/Router.php
<?php
/**
* This file is part of the Library package.
*
* Copyleft (ↄ) 2013-2016 Pierre Cassat <me@e-piwi.fr> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* The source code of this package is available online at
* <http://github.com/atelierspierrot/library>.
*/
namespace Library;
use \Library\Helper\Url as UrlHelper;
use \Library\Helper\Text as TextHelper;
use \Patterns\Interfaces\RouterInterface;
use \Patterns\Commons\Collection;
/**
* The global router class
*
* @author piwi <me@e-piwi.fr>
*/
class Router
implements RouterInterface
{
/**
* @var string Current URL to work on
*/
protected $url;
/**
* @var string Current route of the handled request
*/
protected $route;
/**
* @var array Current route parsing result
*/
protected $route_parsed;
/**
* @var \Patterns\Commons\Collection A collection of available routes mapping
*/
protected $routes_collection;
/**
* @var \Patterns\Commons\Collection A collection of arguments correspondances
*/
protected $arguments_collection;
/**
* @var \Patterns\Commons\Collection A collection of masks to expand routes
*/
protected $matchers_collection;
/**
* Construction
*
* @param string $route
* @param array|object $routes_table
* @param array|object $arguments_table
* @param array|object $matchers_table
*/
public function __construct(
$route = null, array $routes_table = array(), array $arguments_table = array(), array $matchers_table = array()
) {
if (!empty($routes_table)) {
$this->setRoutes($this->getCollection($routes_table));
}
if (!empty($arguments_table)) {
$this->setArgumentsMap($this->getCollection($arguments_table));
}
if (!empty($matchers_table)) {
$this->setMatchers($this->getCollection($matchers_table));
}
if (!empty($route)) {
$this->setRoute($route);
}
}
/**
* Get a collection object if it was not
*
* @param array|object $collection
* @return \Patterns\Commons\Collection
*/
public function getCollection(array $collection)
{
if ($collection instanceof \Patterns\Commons\Collection) {
return $collection;
} else {
return new Collection($collection);
}
}
// ----------------------
// Setters / Getters
// ----------------------
/**
* Set the current URL
*
* @param string $url
* @return self
*/
public function setUrl($url)
{
$this->url = $url;
return $this;
}
/**
* Get the current URL
*
* @return string
*/
public function getUrl()
{
return $this->url;
}
/**
* Set the current route
*
* @param string $route
* @return self
*/
public function setRoute($route)
{
$this->route = $route;
return $this;
}
/**
* Get the current route
*
* @return string
*/
public function getRoute()
{
return $this->route;
}
/**
* Set the current route parsed infos
*
* @param array $infos
* @return self
*/
public function setRouteParsed($infos)
{
$this->route_parsed = $infos;
return $this;
}
/**
* Get the current route parsed infos
*
* @return array
*/
public function getRouteParsed()
{
return $this->route_parsed;
}
/**
* Set the routes collection
*
* @param \Patterns\Commons\Collection $collection
* @return self
*/
public function setRoutes(Collection $collection)
{
$this->routes_collection = $collection;
return $this;
}
/**
* Get the routes collection
*
* @return \Patterns\Commons\Collection
*/
public function getRoutes()
{
return $this->routes_collection;
}
/**
* Set the arguments correspondances table like ( true arg in URL => true arg name in the app )
*
* @param \Patterns\Commons\Collection $collection
* @return self
*/
public function setArgumentsMap(Collection $collection)
{
$this->arguments_collection = $collection;
return $this;
}
/**
* Get the arguments table
*
* @return \Patterns\Commons\Collection
*/
public function getArgumentsMap()
{
return $this->arguments_collection;
}
/**
* Set a collection of masks to parse and match a route URL
*
* @param \Patterns\Commons\Collection $collection
* @return self
*/
public function setMatchers(Collection $collection)
{
$this->matchers_collection = $collection;
return $this;
}
/**
* Get the route matcher
*
* @return \Patterns\Commons\Collection
*/
public function getMatchers()
{
return $this->matchers_collection;
}
/**
* Check if a route exists
*
* @param string $route The route to test
* @return bool
*/
public function routeExists($route)
{
return is_array($routes = $this->getRoutes()) && isset($routes[$route]);
}
// ----------------------
// Processes & utilities
// ----------------------
/**
* URL parser : load and parse the current URL
*
* The class will pass arguments values to any `$this->fromUrlParam($value)` method for the
* parameter named `param`.
*
*/
protected function _parseUrl()
{
$url_frgts = UrlHelper::parse($this->getUrl());
$route = array('all'=>array());
if (!empty($url_frgts['params'])) {
$frgts = array();
$url_args = $this->getArgumentsMap();
foreach ($url_frgts['params'] as $_var=>$_val) {
$_meth = 'fromUrl'.TextHelper::toCamelCase($_var);
if (method_exists($this, $_meth)) {
$_val = call_user_func_array(array($this, $_meth), array($_val));
}
if (isset($url_args[$_var])) {
$route[$url_args[$_var]] = $_val;
} else {
$route['all'][$_var] = $_val;
}
}
}
$this->setRouteParsed($route);
}
/**
* Route parser : load and parse the current route
*/
protected function _parseRoute()
{
$route_rule = $this->matchUrl($this->getRoute());
if (!empty($route_rule)) {
$this->setRouteParsed($route_rule);
}
}
/**
* Build a new route URL
*
* The class will pass arguments values to any `$this->toUrlParam($value)` method for the
* parameter named `param`.
*
* @param mixed $route_infos The information about the route to analyze, can be a string route or an array
* of arguments like `param => value`
* @param string $base_uri The URI to add the new route to
* @param string $hash A hash tag to add to the generated URL
* @param string $separator The argument/value separator (default is escaped ampersand : '&')
*
* @return string The application valid URL for the route
*
* @todo manage the case of $route_infos = route
*/
public function generateUrl($route_infos, $base_uri = null, $hash = null, $separator = '&')
{
$url_args = $this->getArgumentsMap()->getCollection();
$url = $base_uri;
if (is_array($route_infos)) {
$final_params = array();
foreach ($route_infos as $_var=>$_val) {
if (!empty($_val)) {
$arg = in_array($_var, $url_args) ? array_search($_var, $url_args) : $_var;
$_meth = 'toUrl'.TextHelper::toCamelCase($_var);
if (method_exists($this, $_meth)) {
$_val = call_user_func_array(array($this, $_meth), array($_val));
}
if (is_string($_val)) {
$final_params[$this->urlEncode($arg)] = $this->urlEncode($_val);
} elseif (is_array($_val)) {
foreach($_val as $_j=>$_value) {
$final_params[$this->urlEncode($arg).'['.(is_string($_j) ? $_j : '').']'] = $this->urlEncode($_val);
}
}
}
}
$url .= '?'.http_build_query($final_params, '', $separator);
}
if (!empty($hash)) $url .= '#'.$hash;
return $url;
}
/**
* Test if an URL has a corresponding route
*
* @param mixed $pathinfo
* @return false|mixed
*/
public function matchUrl($pathinfo)
{
$routes = $this->getRoutes();
if (!empty($routes) && isset($routes[$pathinfo])) {
return $routes[$pathinfo];
}
return false;
}
/**
* Actually dispatch the current route
*
* @return self
* @throws \RuntimeException if no route has been found
*/
public function distribute()
{
$route = str_replace($_SERVER['SCRIPT_NAME'], '', $_SERVER['REQUEST_URI']);
if (!empty($route) && in_array($route{0}, array('?', '/', '&'))) {
$route = substr($route, 1);
}
if (!empty($route) && $this->matchUrl($route)) {
$this->setRoute($route)->_parseRoute();
} elseif (!empty($this->url)) {
$this->_parseUrl();
} else {
throw new \RuntimeException('No route or URL to analyze and distribute!');
}
return $this;
}
/**
* Forward the application to a new route (no HTTP redirect)
*
* @param mixed $pathinfo The path information to forward to
* @param string $hash A hash tag to add to the generated URL
* @return void
*/
public function forward($pathinfo, $hash = null)
{
}
/**
* Make a redirection to a new route (HTTP redirect)
*
* @param mixed $pathinfo The path information to redirect to
* @param string $hash A hash tag to add to the generated URL
* @return void
*/
public function redirect($pathinfo, $hash = null)
{
$uri = is_string($pathinfo) ? $pathinfo : $this->generateUrl($pathinfo);
if (!headers_sent()) {
header("Location: $uri");
} else {
echo <<<MESSAGE
<!DOCTYPE HTML>
<head>
<meta http-equiv='Refresh' content='0; url={$uri}'><title>HTTP 302</title>
</head><body>
<h1>HTTP 302</h1>
<p>Your browser will be automatically redirected.
<br />If not, please click on next link: <a href="{$uri}">{$uri}</a>.</p>
</body></html>
MESSAGE;
}
exit;
}
/**
* Special 'urlencode' function to only encode strings and let any "%s" mask not encoded
*
* @param string $str The URL or argument to encode
* @param bool $keep_mask
* @return string The encoded URL if so
*/
public static function urlEncode($str = null, $keep_mask = true)
{
if (
(!empty($str) && is_numeric($str)) ||
(true===$keep_mask && $str==='%s')
) {
return $str;
}
if (empty($str) || !is_string($str)) {
return '';
}
return urlencode($str);
}
}