nur-sery/nur_src/v/base/RouteManager.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 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");
}
}