263 lines
8.2 KiB
PHP
263 lines
8.2 KiB
PHP
|
<?php
|
||
|
namespace nur\v\base;
|
||
|
|
||
|
use nur\A;
|
||
|
use nur\b\ValueException;
|
||
|
use nur\func;
|
||
|
use nur\md;
|
||
|
use nur\str;
|
||
|
use nur\v\html5\Html5BasicErrorPage;
|
||
|
use nur\v\model\IPage;
|
||
|
use nur\v\model\IRouteManager;
|
||
|
|
||
|
class RouteManager implements IRouteManager {
|
||
|
const DEFAULT_PAGE = Html5BasicErrorPage::class;
|
||
|
|
||
|
function __construct(?array $routes=null) {
|
||
|
$this->eroutes = [];
|
||
|
$this->proutes = [];
|
||
|
if ($routes !== null) $this->addRoute(...$routes);
|
||
|
}
|
||
|
|
||
|
private static function check_page_type($page): array {
|
||
|
if (is_array($page) && array_key_exists(0, $page) && is_string($page[0])) {
|
||
|
return [$page[0], $page];
|
||
|
} elseif ($page instanceof IPage) {
|
||
|
return [$page, $page];
|
||
|
} elseif (is_string($page)) {
|
||
|
return [$page, [$page]];
|
||
|
}
|
||
|
throw ValueException::unexpected_type(IPage::class, $page);
|
||
|
}
|
||
|
|
||
|
/** @param $args IPage|array */
|
||
|
private static function get_page($args): IPage {
|
||
|
if ($args instanceof IPage) return $args;
|
||
|
else return func::cons(...$args);
|
||
|
}
|
||
|
|
||
|
/** @var IPage|array */
|
||
|
protected $errorPage;
|
||
|
|
||
|
function setErrorPage($page) {
|
||
|
$this->errorPage = self::check_page_type($page)[1];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @var array routes pour des chemins exacts. elles ont la priorité sur les
|
||
|
* préfixes
|
||
|
*/
|
||
|
protected $eroutes;
|
||
|
|
||
|
/** @var array routes pour des préfixes de chemin */
|
||
|
protected $proutes;
|
||
|
|
||
|
private static final function get_package(?string $class): ?string {
|
||
|
if ($class === null) return null;
|
||
|
str::del_prefix($class, "\\");
|
||
|
if (($pos = strrpos($class, "\\")) === false) return "";
|
||
|
else return substr($class, 0, $pos);
|
||
|
}
|
||
|
|
||
|
function addRoute(array ...$routes): void {
|
||
|
$eroutes =& $this->eroutes;
|
||
|
$proutes =& $this->proutes;
|
||
|
foreach ($routes as $route) {
|
||
|
md::ensure_schema($route, self::ROUTE_SCHEMA, null, false);
|
||
|
str::del_prefix($route["path"], "/");
|
||
|
$path = $route["path"];
|
||
|
[$page, $cons_args] = self::check_page_type($route["page"]);
|
||
|
$route["page"] = $page;
|
||
|
$route["cons_args"] = $cons_args;
|
||
|
$route["package"] = self::get_package($route["page"]);
|
||
|
A::ensure_array($route["aliases"]);
|
||
|
switch ($route["mode"]) {
|
||
|
case self::MODE_EXACT:
|
||
|
# route exacte
|
||
|
$eroutes[$path] = $route;
|
||
|
break;
|
||
|
case self::MODE_PREFIX:
|
||
|
case self::MODE_PACKAGE:
|
||
|
case self::MODE_PACKAGE2:
|
||
|
if ($path && !str::ends_with("/", $path)) {
|
||
|
# chemin ne se terminant pas par '/': faire aussi la correspondance
|
||
|
# exacte
|
||
|
$eroutes[$path] = $route;
|
||
|
}
|
||
|
$proutes[$page] = $route;
|
||
|
break;
|
||
|
}
|
||
|
# faire les aliases
|
||
|
foreach ($route["aliases"] as $alias) {
|
||
|
str::del_prefix($alias, "/");
|
||
|
$route["path"] = $alias;
|
||
|
$route["mode"] = self::MODE_EXACT;
|
||
|
$eroutes[$alias] = $route;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Calculer $page à partir de $path avec les règles suivantes:
|
||
|
* - supprimer le suffixe '.php'
|
||
|
* - remplacer '/' et '--' par '\\'
|
||
|
* - pour chaque élément, transformer camel-case en CamelCase
|
||
|
* - rajouter le suffixe Page
|
||
|
*
|
||
|
* voici comment est calculée la valeur de départ:
|
||
|
* - si $path se termine par '.php', prendre le chemin SANS le suffixe '.php'
|
||
|
* - si $path est la forme '*.php/SUFFIX', prendre SUFFIX comme valeur de
|
||
|
* départ
|
||
|
*/
|
||
|
private function computePackagePage(?string $path, string $package): string {
|
||
|
str::del_prefix($path, "/");
|
||
|
if (str::ends_with(".php", $path)) {
|
||
|
str::del_suffix($path, ".php");
|
||
|
} elseif (($pos = strpos($path, ".php/")) !== false) {
|
||
|
$path = substr($path, $pos + 5);
|
||
|
}
|
||
|
$path = str_replace("--", "/", $path);
|
||
|
|
||
|
$parts = explode("/", "$path-page");
|
||
|
$last = count($parts) - 1;
|
||
|
for ($i = 0; $i <= $last; $i++) {
|
||
|
$parts[$i] = str::us2camel($parts[$i]);
|
||
|
if ($i == $last) $parts[$i] = str::upper1($parts[$i]);
|
||
|
}
|
||
|
str::add_suffix($package, "\\", true);
|
||
|
return $package.implode("\\", $parts);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* sur la base des routes définies, résoudre la classe à instancier pour
|
||
|
* traiter le chemin spécifié
|
||
|
*
|
||
|
* @return IPage|array
|
||
|
*/
|
||
|
function resolvePage(?string $path) {
|
||
|
if ($path === null) $path = $_SERVER["PHP_SELF"];
|
||
|
str::del_prefix($path, "/");
|
||
|
|
||
|
# essayer d'abord des routes exactes
|
||
|
$route = A::get($this->eroutes, $path);
|
||
|
if ($route !== null) return $route["cons_args"];
|
||
|
# puis, essayer dans l'ordre les routes basées sur des préfixes
|
||
|
foreach ($this->proutes as $route) {
|
||
|
switch ($route["mode"]) {
|
||
|
case self::MODE_PREFIX:
|
||
|
$prefix = $route["path"];
|
||
|
if ($prefix) str::add_suffix($prefix, "/", true);
|
||
|
if (str::starts_with($prefix, $path)) return $route["cons_args"];
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
# chemin non trouvé, essayer de calculer à partir de $path
|
||
|
$page = false;
|
||
|
foreach ($this->proutes as $route) {
|
||
|
switch ($route["mode"]) {
|
||
|
case self::MODE_PACKAGE:
|
||
|
case self::MODE_PACKAGE2:
|
||
|
$prefix = $route["path"];
|
||
|
if ($prefix) str::add_suffix($prefix, "/", true);
|
||
|
if (str::starts_with($prefix, $path)) {
|
||
|
$page = $this->computePackagePage(
|
||
|
str::without_prefix($prefix, $path),
|
||
|
$route["package"]);
|
||
|
if (class_exists($page)) return [$page];
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
# sinon prendre la page par défaut (qui est en fait la page d'erreur)
|
||
|
$error = $this->errorPage;
|
||
|
if ($error === null) $error = [static::DEFAULT_PAGE];
|
||
|
if (is_array($error)) {
|
||
|
if ($page) {
|
||
|
A::merge($error, ["$path: unable to find page class $page"]);
|
||
|
} else {
|
||
|
A::merge($error, ["$path: unable to find page class"]);
|
||
|
}
|
||
|
}
|
||
|
return $error;
|
||
|
}
|
||
|
|
||
|
function getPage(?string $path=null): IPage {
|
||
|
$page = $this->resolvePage($path);
|
||
|
if ($page instanceof IPage) return $page;
|
||
|
else return func::cons(...$page);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Calculer $path à partir de $page avec les règles suivantes:
|
||
|
* - supprimer le suffixe Page
|
||
|
* - pour chaque élément, transformer CamelCase en camel-case
|
||
|
* - remplacer '\\' par '/'
|
||
|
* - rajouter le suffixe '.php'
|
||
|
*
|
||
|
* NB: cette fonction n'est pas la symétrique de {@link computePackagePage()}.
|
||
|
* par exemple:
|
||
|
* - computePackagePage() transforme 'p--do-it.php' en 'p\\DoItPage'
|
||
|
* - computePackagePath() transforme 'p\\DoItPage' en 'p/do-it.php'
|
||
|
* pour les cas particulier, il vaut donc mieux faire des routes exactes
|
||
|
*/
|
||
|
private function computePackagePath(string $prefix, string $page, string $package, string $sep="/"): string {
|
||
|
# préfixe
|
||
|
str::add_suffix($prefix, "/", true);
|
||
|
# classe
|
||
|
if ($package) str::del_prefix($page, "$package\\");
|
||
|
str::del_suffix($page, "Page");
|
||
|
$parts = explode("\\", $page);
|
||
|
$last = count($parts) - 1;
|
||
|
for ($i = 0; $i <= $last; $i++) {
|
||
|
if ($i == $last) $parts[$i] = str::lower1($parts[$i]);
|
||
|
$parts[$i] = str::camel2us($parts[$i], false, "-");
|
||
|
}
|
||
|
$path = implode($sep, $parts);
|
||
|
return "$prefix$path.php";
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* obtenir le chemin correspondant à l'instance de {@link IPage} ou à la
|
||
|
* classe spécifiée.
|
||
|
*
|
||
|
* @param string|IPage $page instance ou classe de la page dont on veut le
|
||
|
* chemin depuis la racine de l'application
|
||
|
*/
|
||
|
function getPath($page): string {
|
||
|
if ($page instanceof IPage) {
|
||
|
$page = get_class($page);
|
||
|
} elseif (!is_string($page)) {
|
||
|
throw ValueException::unexpected_type(["string", IPage::class], $page);
|
||
|
}
|
||
|
# d'abord les chemins exacts
|
||
|
foreach ($this->eroutes as $route) {
|
||
|
if ($page === $route["page"]) return $route["path"];
|
||
|
}
|
||
|
# puis les préfixes
|
||
|
foreach ($this->proutes as $route) {
|
||
|
switch ($route["mode"]) {
|
||
|
case self::MODE_PREFIX:
|
||
|
if ($page === $route["page"]) return $route["path"];
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
# puis les packages
|
||
|
foreach ($this->proutes as $route) {
|
||
|
$mode = $route["mode"];
|
||
|
switch ($mode) {
|
||
|
case self::MODE_PACKAGE:
|
||
|
case self::MODE_PACKAGE2:
|
||
|
$package = $route["package"];
|
||
|
if (str::starts_with($package, $page)) {
|
||
|
if ($mode == self::MODE_PACKAGE) $sep = "/";
|
||
|
else $sep = "--";
|
||
|
return $this->computePackagePath($route["path"], $page, $package, $sep);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
# pas trouvé
|
||
|
throw new ValueException(": $page: unable to find path");
|
||
|
}
|
||
|
}
|