début travail sur renderer
This commit is contained in:
parent
b09d01c37a
commit
bff1953f9e
10
src/web/base/BasicPage.php
Normal file
10
src/web/base/BasicPage.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\web\base;
|
||||||
|
|
||||||
|
use nulib\web\model\IPage;
|
||||||
|
|
||||||
|
class BasicPage implements IPage {
|
||||||
|
use TRenderer;
|
||||||
|
|
||||||
|
const RENDERER = Html5Renderer::class;
|
||||||
|
}
|
||||||
26
src/web/base/Html5Renderer.php
Normal file
26
src/web/base/Html5Renderer.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\web\base;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use nulib\output\log;
|
||||||
|
use nulib\php\func;
|
||||||
|
use nulib\web\model\IPage;
|
||||||
|
use nulib\web\model\IRenderer;
|
||||||
|
use nulib\web\page;
|
||||||
|
use nulib\web\route;
|
||||||
|
|
||||||
|
class Html5Renderer implements IRenderer {
|
||||||
|
function render(IPage $page): void {
|
||||||
|
page::set_current($page);
|
||||||
|
try {
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if (page::is_error()) {
|
||||||
|
log::error($e);
|
||||||
|
} else {
|
||||||
|
page::set_error();
|
||||||
|
$this->render(func::with(route::get_error($e))->invoke());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
285
src/web/base/RouteManager.php
Normal file
285
src/web/base/RouteManager.php
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\web\base;
|
||||||
|
|
||||||
|
use nulib\cl;
|
||||||
|
use nulib\cv;
|
||||||
|
use nulib\exceptions;
|
||||||
|
use nulib\php\func;
|
||||||
|
use nulib\php\types\vint;
|
||||||
|
use nulib\str;
|
||||||
|
use nulib\web\model\IPage;
|
||||||
|
|
||||||
|
class RouteManager {
|
||||||
|
/** @var int indique que la correspondance du chemin doit être exacte */
|
||||||
|
const MODE_EXACT = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int indique que la correspondance du chemin se fait sur le préfixe
|
||||||
|
* par exemple:
|
||||||
|
* - 'index.php' matche 'index.php' et 'index.php/suffix'
|
||||||
|
* - 'path/to/' matche 'path/to/index.php' et 'path/to/menu.php'",
|
||||||
|
* si le chemin ne se termine pas par '/', une correspondance exacte est
|
||||||
|
* automatiquement rajoutée
|
||||||
|
* contraiement à {@link MODE_PACKAGE}, quel que soit le chemin matché, c'est
|
||||||
|
* toujours la même page qui prend en charge le chemin
|
||||||
|
*/
|
||||||
|
const MODE_PREFIX = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int indique que la correspondance du chemin se fait sur le préfixe, et
|
||||||
|
* que la classe associée est calculée automatiquement en cherchant à partir
|
||||||
|
* d'un package spécifié. par exemple:
|
||||||
|
* - si on a [path]="prefix/" et [page]="package\\Class"
|
||||||
|
* - alors 'prefix/index.php' est pris en charge par package\IndexPage
|
||||||
|
* - et 'prefix/sub/do-it.php' est pris en charge par package\sub\DoItPage
|
||||||
|
* si le chemin ne se termine pas par '/', une correspondance exacte est
|
||||||
|
* automatiquement rajoutée
|
||||||
|
*/
|
||||||
|
const MODE_PACKAGE = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int comme {@link MODE_PACKAGE} mais en utilisant '--' au lieu de '/'
|
||||||
|
*/
|
||||||
|
const MODE_PACKAGE2 = 3;
|
||||||
|
|
||||||
|
private const VALID_MODES = [
|
||||||
|
self::MODE_EXACT,
|
||||||
|
self::MODE_PREFIX,
|
||||||
|
self::MODE_PACKAGE,
|
||||||
|
self::MODE_PACKAGE2,
|
||||||
|
];
|
||||||
|
|
||||||
|
function __construct(?array $routes=null) {
|
||||||
|
$this->eroutes = [];
|
||||||
|
$this->proutes = [];
|
||||||
|
$this->errorPage = null; #XXX
|
||||||
|
if ($routes !== null) $this->addRoute(...$routes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** routes pour les chemins exacts. elles ont la priorité sur les préfixes */
|
||||||
|
protected array $eroutes;
|
||||||
|
|
||||||
|
/** routes pour les préfixes de chemin */
|
||||||
|
protected array $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) {
|
||||||
|
$path = cv::not_null($route["path"] ?? null, "route path");
|
||||||
|
str::del_prefix($path, "/");
|
||||||
|
$page = cv::not_null($route["page"] ?? null, "route page");
|
||||||
|
$package = self::get_package($page);
|
||||||
|
$args = cl::with($route["args"] ?? null);
|
||||||
|
$mode = vint::with($route["mode"] ?? null);
|
||||||
|
$aliases = cl::with($route["aliases"] ?? null);
|
||||||
|
$route = [
|
||||||
|
"path" => $path,
|
||||||
|
"page" => $page,
|
||||||
|
"package" => $package,
|
||||||
|
"args" => $args,
|
||||||
|
"mode" => $mode,
|
||||||
|
"aliases" => $aliases,
|
||||||
|
];
|
||||||
|
switch ($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;
|
||||||
|
default:
|
||||||
|
throw exceptions::invalid_value($mode, "route mode");
|
||||||
|
}
|
||||||
|
# faire les aliases
|
||||||
|
foreach ($route["aliases"] as $alias) {
|
||||||
|
str::del_prefix($alias, "/");
|
||||||
|
$route["path"] = $alias;
|
||||||
|
$route["mode"] = self::MODE_EXACT;
|
||||||
|
$eroutes[$alias] = $route;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string $errorPage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* spécifier la page par défaut si aucune route ne correspond
|
||||||
|
*/
|
||||||
|
function setErrorPage(string $page): void {
|
||||||
|
$this->errorPage = $page;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorPage($error): array {
|
||||||
|
return [$this->errorPage, false, $error];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, "\\");
|
||||||
|
return $package.implode("\\", $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* obtenir une class dérivée de {@link IPage} correspondant au chemin spécifié
|
||||||
|
* e.g '/index.php'
|
||||||
|
*
|
||||||
|
* si $path===null, prendre le chemin courant i.e "$php_self.$path_info"
|
||||||
|
*
|
||||||
|
* Retourner `[$class, false, ...$args]` qui est directement utilisable par
|
||||||
|
* {@link func}. Le classe doit être instanciée par l'utilisateur
|
||||||
|
*/
|
||||||
|
function getPage($path=null): array {
|
||||||
|
if ($path === null) $path = $_SERVER["PHP_SELF"];
|
||||||
|
str::del_prefix($path, "/");
|
||||||
|
|
||||||
|
# essayer d'abord des routes exactes
|
||||||
|
$route = cl::get($this->eroutes, $path);
|
||||||
|
if ($route !== null) {
|
||||||
|
return [$route["page"], false, ...$route["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, "/");
|
||||||
|
if (str::starts_with($prefix, $path)) {
|
||||||
|
return [$route["page"], false, ...$route["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, "/");
|
||||||
|
if (str::starts_with($prefix, $path)) {
|
||||||
|
$page = $this->computePackagePage(
|
||||||
|
str::without_prefix($prefix, $path),
|
||||||
|
$route["package"]);
|
||||||
|
if (class_exists($page)) return [$page, false, ...$route["args"]];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# sinon prendre la page par défaut (qui est en fait la page d'erreur)
|
||||||
|
$errorMessage = "$path: unable to find page class";
|
||||||
|
if ($page !== false) $errorMessage .= " $page";
|
||||||
|
return $this->getErrorPage($errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, "/");
|
||||||
|
# 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 à la page spécifiée.
|
||||||
|
*
|
||||||
|
* @param string|IPage $page
|
||||||
|
*/
|
||||||
|
function getPath($page): string {
|
||||||
|
if ($page instanceof IPage) $page = get_class($page);
|
||||||
|
if (!is_string($page)) {
|
||||||
|
throw exceptions::invalid_type($page, "page", ["string", IPage::class]);
|
||||||
|
}
|
||||||
|
# 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 exceptions::invalid_value($page, "page", "chemin non trouvé");
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/web/base/TRenderer.php
Normal file
11
src/web/base/TRenderer.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\web\base;
|
||||||
|
|
||||||
|
use nulib\web\model\IRenderer;
|
||||||
|
|
||||||
|
trait TRenderer {
|
||||||
|
function RENDERER(): IRenderer {
|
||||||
|
$class = static::RENDERER ?? Html5Renderer::class;
|
||||||
|
return new $class();
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/web/model/IPage.php
Normal file
6
src/web/model/IPage.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\web\model;
|
||||||
|
|
||||||
|
interface IPage {
|
||||||
|
function RENDERER(): IRenderer;
|
||||||
|
}
|
||||||
6
src/web/model/IRenderer.php
Normal file
6
src/web/model/IRenderer.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\web\model;
|
||||||
|
|
||||||
|
interface IRenderer {
|
||||||
|
function render(IPage $page): void;
|
||||||
|
}
|
||||||
26
src/web/page.php
Normal file
26
src/web/page.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\web;
|
||||||
|
|
||||||
|
use nulib\app\app;
|
||||||
|
use nulib\app\config;
|
||||||
|
use nulib\exceptions;
|
||||||
|
use nulib\php\func;
|
||||||
|
use nulib\web\model\IPage;
|
||||||
|
|
||||||
|
class page {
|
||||||
|
static function render($page=null): void {
|
||||||
|
app::set_fact(app::FACT_WEB_APP);
|
||||||
|
config::configure(config::CONFIGURE_INITIAL_ONLY);
|
||||||
|
if (!($page instanceof IPage)) {
|
||||||
|
if ($page === null) {
|
||||||
|
config::configure(config::CONFIGURE_ROUTES_ONLY);
|
||||||
|
$page = route::get_page();
|
||||||
|
}
|
||||||
|
if (is_string($page)) $page = new $page();
|
||||||
|
elseif (is_array($page)) $page = func::with($page)->invoke();
|
||||||
|
else throw exceptions::invalid_type($page, "page", ["string", "array"]);
|
||||||
|
}
|
||||||
|
/** @var IPage $page */
|
||||||
|
$page->RENDERER()->render($page);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/web/route.php
Normal file
27
src/web/route.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
namespace nulib\web;
|
||||||
|
|
||||||
|
use nulib\web\base\RouteManager;
|
||||||
|
|
||||||
|
class route {
|
||||||
|
protected static RouteManager $route;
|
||||||
|
|
||||||
|
const MODE_EXACT = RouteManager::MODE_EXACT;
|
||||||
|
const MODE_PREFIX = RouteManager::MODE_PREFIX;
|
||||||
|
const MODE_PACKAGE = RouteManager::MODE_PACKAGE;
|
||||||
|
const MODE_PACKAGE2 = RouteManager::MODE_PACKAGE2;
|
||||||
|
|
||||||
|
static final function add(array ...$routes): void { self::$route->addRoute(...$routes); }
|
||||||
|
|
||||||
|
static final function set_error($page): void { self::$route->setErrorPage($page); }
|
||||||
|
static final function get_error($error): array { return self::$route->getErrorPage($error); }
|
||||||
|
|
||||||
|
static final function get_page(?string $path=null): array { return self::$route->getPage($path); }
|
||||||
|
static final function get_path($page): string { return self::$route->getPath($page); }
|
||||||
|
}
|
||||||
|
|
||||||
|
new class extends route {
|
||||||
|
function __construct() {
|
||||||
|
self::$route = new RouteManager();
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user