application web

This commit is contained in:
Jephté Clain 2025-11-11 10:48:04 +04:00
parent 0f6a76e701
commit 944daa8ea4
18 changed files with 706 additions and 93 deletions

236
src/app/web/Application.php Normal file
View File

@ -0,0 +1,236 @@
<?php
namespace nulib\app\web;
use Exception;
use nulib\app\app;
use nulib\app\config;
use nulib\exceptions;
use nulib\ExitError;
use nulib\output\con;
use nulib\output\log;
use nulib\output\msg;
use nulib\output\std\LogMessenger;
use nulib\php\func;
use nulib\web\base\Html5PageRenderer;
use nulib\web\base\JsonRenderer;
use nulib\web\base\TextRenderer;
use nulib\web\model\IComponent;
use nulib\web\model\IPage;
use nulib\web\page;
/**
* Class Application: application de base
*/
abstract class Application {
/** @var string répertoire du projet (celui qui contient composer.json */
const PROJDIR = null;
/**
* @var array répertoires vendor exprimés relativement à PROJDIR
*
* les clés suivantes doivent être présentes dans le tableau:
* - autoload (chemin vers vendor/autoload.php)
* - bindir (chemin vers vendor/bin)
*/
const VENDOR = null;
/**
* @var string code du projet, utilisé pour dériver le noms de certains des
* paramètres extraits de l'environnement, e.g MY_APP_DATADIR si le projet a
* pour code my-app
*
* si non définie, cette valeur est calculée automatiquement à partir de
* self::PROJDIR sans le suffixe "-app"
*/
const PROJCODE = null;
/**
* @var string|null identifiant d'un groupe auquel l'application appartient.
* les applications du même groupe enregistrent leur fichiers de controle au
* même endroit $VARDIR/$APPGROUP
*/
const APPGROUP = null;
/**
* @var string code de l'application, utilisé pour inférer le nom de certains
* fichiers spécifiques à l'application.
*
* si non définie, cette valeur est calculée automatiquement à partir de
* static::class
*/
const NAME = null;
/** @var string description courte de l'application */
const TITLE = null;
const DATADIR = null;
const ETCDIR = null;
const VARDIR = null;
const CACHEDIR = null;
const LOGDIR = null;
const MODE_EXACT = RouteManager::MODE_EXACT;
const MODE_PREFIX = RouteManager::MODE_PREFIX;
const MODE_PACKAGE = RouteManager::MODE_PACKAGE;
const MODE_PACKAGE2 = RouteManager::MODE_PACKAGE2;
const ROUTES = [
];
const PAGE = null;
static function run($page=null, ?Application $app=null): void {
try {
static::_initialize_app();
$app ??= new static();
page::set_app($app);
$app->render($page ?? static::PAGE);
} catch (ExitError $e) {
if ($e->haveUserMessage()) log::error($e->getUserMessage());
exit($e->getCode());
} catch (Exception $e) {
log::error($e);
exit(app::EC_UNEXPECTED);
}
}
protected static function _initialize_app(): void {
app::init(static::class);
app::set_fact(app::FACT_WEB_APP);
$projcode = static::PROJCODE ?? "application";
$log = new LogMessenger([
"output" => "/tmp/$projcode.out",
"min_level" => msg::DEBUG,
]);
con::set_messenger($log);
msg::set_messenger($log);
}
/**
* sortir de l'application avec un code d'erreur, qui est 0 par défaut (i.e
* pas d'erreur)
*
* équivalent à lancer l'exception {@link ExitError}
*/
protected static final function exit(int $exitcode=0, $message=null) {
throw new ExitError($exitcode, $message);
}
/**
* sortir de l'application avec un code d'erreur, qui vaut 1 par défaut (i.e
* une erreur s'est produite)
*
* équivalent à lancer l'exception {@link ExitError}
*/
protected static final function die($message=null, int $exitcode=1) {
throw new ExitError($exitcode, $message);
}
protected RouteManager $routeManager;
function getRouteManager(): RouteManager {
return $this->routeManager;
}
protected function configureRoutes(): void {
$this->routeManager->addRoute(...static::ROUTES);
}
protected function configureLogs(): void {
$log = log::set_messenger(new LogMessenger([
"output" => app::get()->getLogfile(),
"min_level" => msg::MINOR,
]));
msg::set_messenger($log, true);
}
protected function configureNoSession(): void {
config::configure(config::CONFIGURE_NO_SESSION);
}
protected function configureSession(): void {
config::configure();
}
protected function resolvePage($page): IPage {
if ($page instanceof IPage) return $page;
$page ??= $this->routeManager->getPage();
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"]);
return $page;
}
function ensurePhase(IPage $page, int $phase, bool $after=false, &$output=null) {
if (!$page->didComponentPhase($phase)) {
if ($page->beforeComponentPhase($phase)) {
page::set_component_phase($phase);
$output = $page->doComponentPhase($phase);
if ($after) $page->afterComponentPhase($phase);
return true;
}
}
return false;
}
protected ?array $renderers = null;
protected function getRenderer(IPage $page) {
$class = $page->RENDERER_CLASS() ?? Html5PageRenderer::class;
return $this->renderers[$class] ??= new $class();
}
function render($component) {
config::configure(config::CONFIGURE_INITIAL_ONLY);
$this->routeManager = new RouteManager();
$this->configureRoutes();
$this->configureLogs();
$components = [];
$component = $this->resolvePage($component);
while (true) {
page::set_component($component);
array_unshift($components, $component);
if (!$this->ensurePhase($component, page::PHASE_PREPARE)) break;
$component->afterComponentPhase(page::PHASE_PREPARE);
if ($component->USE_SESSION()) $this->configureSession();
else $this->configureNoSession();
if (!$this->ensurePhase($component, page::PHASE_CONFIGURE)) break;
$component->afterComponentPhase(page::PHASE_CONFIGURE);
if (!$this->ensurePhase($component, page::PHASE_SETUP, false, $output)) break;
$component->afterComponentPhase(page::PHASE_SETUP);
page::set_component_phase(page::PHASE_PRINT);
if ($output instanceof IComponent) {
# composant autonome. recommencer la phase de préparation
} elseif (is_callable($output)) {
# générateur de contenu
$output();
break;
} elseif ($output === false) {
# retour en l'état (contenu déjà généré dans setup)
break;
} elseif ($output !== null) {
# contenu json ou texte
if (is_iterable($output)) $renderer = new JsonRenderer();
else $renderer = new TextRenderer();
page::set_renderer($renderer);
$renderer->render($output);
break;
} else {
# afficher la page normalement
$renderer = $this->getRenderer($component);
page::set_renderer($renderer);
$renderer->render($component);
break;
}
}
foreach ($components as $component) {
if ($component->didComponentPhase(page::PHASE_SETUP)) {
$this->ensurePhase($component, page::PHASE_TEARDOWN, true);
}
}
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace nulib\web\base;
namespace nulib\app\web;
use nulib\cl;
use nulib\cv;

View File

@ -0,0 +1,17 @@
<?php
namespace nulib\web\base;
use nulib\web\model\IComponent;
abstract class AbstractComponent implements IComponent {
use TComponentPhase;
const COMPONENT_CAN_PREPARE = false;
const COMPONENT_CAN_CONFIGURE = false;
const COMPONENT_CAN_SETUP = false;
const COMPONENT_CAN_TEARDOWN = false;
function RENDERER_CLASS(): ?string {
return static::RENDERER_CLASS;
} const RENDERER_CLASS = null;
}

View File

@ -0,0 +1,15 @@
<?php
namespace nulib\web\base;
use nulib\web\model\IPage;
abstract class AbstractPage extends AbstractComponent implements IPage {
function USE_SESSION(): bool {
return static::USE_SESSION;
} const USE_SESSION = false;
/** faut-il fermer automatiquement la session avant l'affichage? */
protected function AUTOCLOSE_SESSION(): bool {
return static::AUTOCLOSE_SESSION;
} const AUTOCLOSE_SESSION = true;
}

View File

@ -1,10 +1,5 @@
<?php
namespace nulib\web\base;
use nulib\web\model\IPage;
class BasicPage implements IPage {
use TRenderer;
const RENDERER = Html5Renderer::class;
class BasicPage extends AbstractPage {
}

View File

@ -0,0 +1,246 @@
<?php
namespace nulib\web\base;
use nulib\exceptions;
use nulib\php\content\c;
use nulib\str;
use nulib\web\ly;
use nulib\web\model\IPage;
use nulib\web\model\IRenderer;
use nulib\web\page;
class Html5PageRenderer implements IRenderer {
function render($page): void {
if ($page instanceof IPage) {
page::set_html_output();
$this->page = $page;
$this->printStartHtml();
$this->printStartHead();
$this->printCssLinks();
$this->printCss();
$this->printJsLinks();
$this->printJs();
$this->printScript();
$this->printHeadTitle();
$this->printEndHead();
$this->printStartBody();
$this->printEndBody();
$this->printEndHtml();
} else {
throw exceptions::invalid_type($page, "page", IPage::class);
}
}
protected IPage $page;
function printStartHtml(): void {
?>
<!DOCTYPE html>
<html lang="fr" xmlns="http://www.w3.org/1999/xhtml">
<?php
}
function printStartHead(): void {
$prefix = $this->page->getSelfRelativePrefix();
?>
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="<?=$prefix?>nur-base/base.css" rel="stylesheet"/>
<?php
}
function printCssLinks(): void {
$prefix = $this->page->getSelfRelativePrefix();
foreach ($this->cssUrls as $url) {
$url = prefix::add($url, $prefix);
?>
<link href="<?=$url?>" rel="stylesheet"/>
<?php
}
}
function printCss(): void {
foreach ($this->getPlugins() as $plugin) {
$plugin->printCss();
}
}
function printJsLinks(): void {
$prefix = $this->page->getSelfRelativePrefix();
foreach ($this->jsUrls as $url) {
$url = prefix::add($url, $prefix);
?>
<script src="<?=$url?>" type="text/javascript"></script>
<?php
}
}
function printJs(): void {
foreach ($this->getPlugins() as $plugin) {
$plugin->printJs();
}
}
protected function beforeCapture(): void {
ob_start(null, 0, PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE);
}
private static function strip_lines(array &$lines): void {
while (count($lines) > 0 && !$lines[0]) {
# enlever les lignes vides au début
array_shift($lines);
}
while (($count = count($lines)) > 0 && !$lines[$count - 1]) {
# enlever les lignes vides à la fin
array_pop($lines);
}
}
const RE_START_SCRIPT = '/^\s*<script\s+type="(?:text\/)?javascript">\s*(.*)/';
const RE_END_SCRIPT = '/(.*?)\s*<\/script>\s*$/';
private static function unwrap_script(array &$lines): void {
$count = count($lines);
if ($count >= 2) {
$first = $lines[0];
$last = $lines[$count - 1];
if (preg_match(self::RE_START_SCRIPT, $first)
&& preg_match(self::RE_END_SCRIPT, $last)) {
$last = preg_replace(self::RE_END_SCRIPT, '$1', $last);
if ($last) $lines[$count - 1] = $last;
else $lines = array_slice($lines, 0, $count - 1);
$first = preg_replace(self::RE_START_SCRIPT, '$1', $first);
if ($first) $lines[0] = $first;
else $lines = array_slice($lines, 1);
}
}
}
protected $scripts = [];
protected function captureScript(): void {
$lines = trim(ob_get_clean());
if ($lines) {
$lines = str::split_nl($lines);
self::strip_lines($lines);
self::unwrap_script($lines);
$this->scripts[] = implode("\n", $lines);
}
}
const RE_START_JQUERY = '/^\s*jQuery(?:.noConflict\(\))?\(function\(\$\) {\s*(.*)/';
const RE_END_JQUERY = '/(.*?)\s*}\);\s*$/';
private static function unwrap_jquery(array &$lines): void {
$count = count($lines);
if ($count >= 2) {
$first = $lines[0];
$last = $lines[$count - 1];
if (preg_match(self::RE_START_JQUERY, $first)
&& preg_match(self::RE_END_JQUERY, $last)) {
$last = preg_replace(self::RE_END_JQUERY, '$1', $last);
if ($last) $lines[$count - 1] = $last;
else $lines = array_slice($lines, 0, $count - 1);
$first = preg_replace(self::RE_START_JQUERY, '$1', $first);
if ($first) $lines[0] = $first;
else $lines = array_slice($lines, 1);
}
}
}
protected $jqueries = [];
protected function captureJquery(): void {
$lines = trim(ob_get_clean());
if ($lines) {
$lines = str::split_nl($lines);
self::strip_lines($lines);
self::unwrap_script($lines);
self::unwrap_jquery($lines);
$this->jqueries[] = implode("\n", $lines);
}
}
protected function resolvePluginsScripts(): void {
foreach ($this->getPlugins() as $plugin) {
if ($plugin->haveScript()) {
$this->beforeCapture();
$plugin->printScript();
$this->captureScript();
}
if ($plugin->haveJquery()) {
$this->beforeCapture();
$plugin->printJquery();
$this->captureJquery();
}
}
}
protected function printMergedScripts(): void {
$scripts = $this->scripts;
$jqueries = $this->jqueries;
if ($scripts || $jqueries) {
?>
<script type="text/javascript">
<?php
foreach ($scripts as $script) {
echo "$script\n";
}
}
if ($jqueries) {
?>
jQuery.noConflict()(function($) {
<?php
foreach ($jqueries as $jquery) {
echo "$jquery\n";
}
?>
});
<?php
}
if ($scripts || $jqueries) {
?>
</script>
<?php
}
}
function printScript(): void {
$this->resolvePluginsScripts();
$this->printMergedScripts();
}
function printHeadTitle(): void {
$title = c::to_string($this->page->TITLE());
echo "<title>$title</title>\n";
}
function printEndHead(): void {
?>
</head>
<?php
}
function printStartBody(): void {
?>
<body>
<?php
}
function printContent(): void {
c::write($this->page);
ly::end();
}
function printEndBody(): void {
?>
</body>
<?php
}
function printEndHtml(): void {
?>
</html>
<?php
}
}

View File

@ -1,26 +0,0 @@
<?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());
}
}
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace nulib\web\base;
use nulib\web\model\IComponent;
use nulib\web\model\IRenderer;
class HtmlRenderer implements IRenderer {
function render($data): void {
header("Content-Type: text/html; charset=utf-8");
if ($data instanceof IComponent) {
$data->print();
} else {
echo $data;
}
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace nulib\web\base;
use nulib\web\model\IRenderer;
use nur\json;
class JsonRenderer implements IRenderer {
function render($data): void {
header("Content-Type: application/json");
if (is_iterable($data)) {
echo "[";
$sep = "";
foreach ($data as $datum) {
$line = json::encode($datum);
echo "$sep$line\n";
$sep = ",";
}
echo "]";
} else {
echo json::encode($data);
}
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace nulib\web\base;
use nulib\web\page;
trait TComponentPhase {
protected int $currentComponentPhase;
function beforeComponentPhase(int $phase): bool {
return true;
}
function doComponentPhase(int $phase) {
$this->currentComponentPhase = $phase;
switch ($phase) {
case page::PHASE_PREPARE:
if (static::COMPONENT_CAN_PREPARE) $this->prepare();
break;
case page::PHASE_CONFIGURE:
if (static::COMPONENT_CAN_CONFIGURE) $this->configure();
break;
case page::PHASE_SETUP:
if (static::COMPONENT_CAN_SETUP) return $this->setup();
case page::PHASE_TEARDOWN:
if (static::COMPONENT_CAN_TEARDOWN) $this->teardown();
break;
}
return null;
}
function afterComponentPhase(int $phase): void {
}
function didComponentPhase(int $phase): bool {
return $phase >= $this->currentComponentPhase;
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace nulib\web\base;
use nulib\web\model\IRenderer;
trait TRenderer {
function RENDERER(): IRenderer {
$class = static::RENDERER ?? Html5Renderer::class;
return new $class();
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace nulib\web\base;
use nulib\web\model\IRenderer;
class TextRenderer implements IRenderer {
function render($data): void {
header("Content-Type: text/plain; charset=utf-8");
echo $data;
}
}

7
src/web/ly.php Normal file
View File

@ -0,0 +1,7 @@
<?php
namespace nulib\web;
class ly {
static function end(): void {
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace nulib\web\model;
interface IComponent {
/** retourner la classe de renderer à utiliser pour afficher ce composant */
function RENDERER_CLASS(): ?string;
function beforeComponentPhase(int $phase): bool;
function doComponentPhase(int $phase);
function afterComponentPhase(int $phase): void;
function didComponentPhase(int $phase): bool;
}

View File

@ -1,6 +1,29 @@
<?php
namespace nulib\web\model;
interface IPage {
function RENDERER(): IRenderer;
interface IPage extends IComponent {
/** indiquer si cette page a besoin d'avoir une session valide */
function USE_SESSION(): bool;
/**
* retourner le chemin vers le *script* courant par rapport à la racine de
* l'application, e.g 'index.php' s'il est différent de la valeur par défaut
* basename($_SERVER["SCRIPT_NAME"]), ou null pour utiliser la valeur par
* défaut
*
* celà permet de calculer les chemins relatifs aux resources, même si
* l'application n'est pas servie à la racine ou si on utilise path_info
*/
function SELF(): ?string;
/**
* retourner le titre de la page, ou null pour utiliser la valeur par défaut
*/
function TITLE(): ?string;
/**
* retourner le préfixe à rajouter aux chemins relatifs exprimés depuis la
* racine pour les atteindre depuis la page courante
*/
function getSelfRelativePrefix(): string;
}

View File

@ -2,5 +2,5 @@
namespace nulib\web\model;
interface IRenderer {
function render(IPage $page): void;
function render($data): void;
}

View File

@ -1,26 +1,65 @@
<?php
namespace nulib\web;
use nulib\app\app;
use nulib\app\config;
use nulib\exceptions;
use nulib\php\func;
use nulib\web\model\IPage;
use nulib\app\web\Application;
use nulib\web\model\IComponent;
use nulib\web\model\IRenderer;
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);
protected static ?Application $app = null;
static function set_app(Application $app) {
self::$app = $app;
}
static function get_app(): ?Application {
return self::$app;
}
protected static ?IComponent $component = null;
static function set_component(IComponent $component) {
self::$component = $component;
}
static function get_component(): ?IComponent {
return self::$component;
}
const PHASE_NONE = 0;
const PHASE_PREPARE = 1;
const PHASE_CONFIGURE = 2;
const PHASE_SETUP = 3;
const PHASE_PRINT = 4;
const PHASE_TEARDOWN = 5;
protected static int $component_phase = self::PHASE_NONE;
static function set_component_phase(int $component_phase): void {
self::$component_phase = $component_phase;
}
static function get_component_phase(): int {
return self::$component_phase;
}
protected static ?IRenderer $renderer = null;
static function set_renderer(IRenderer $renderer): void {
self::$renderer = $renderer;
}
static function get_renderer(): ?IRenderer {
return self::$renderer;
}
protected static bool $html_output = false;
static function set_html_output(): void {
self::$html_output = true;
}
static function is_html_output(): bool {
return self::$html_output;
}
}

View File

@ -1,27 +0,0 @@
<?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();
}
};