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