From 944daa8ea4eb1cc4ca626a16fb104a260eab1582 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Tue, 11 Nov 2025 10:48:04 +0400 Subject: [PATCH] application web --- src/app/web/Application.php | 236 ++++++++++++++++++++ src/{web/base => app/web}/RouteManager.php | 2 +- src/web/base/AbstractComponent.php | 17 ++ src/web/base/AbstractPage.php | 15 ++ src/web/base/BasicPage.php | 7 +- src/web/base/Html5PageRenderer.php | 246 +++++++++++++++++++++ src/web/base/Html5Renderer.php | 26 --- src/web/base/HtmlRenderer.php | 16 ++ src/web/base/JsonRenderer.php | 23 ++ src/web/base/TComponentPhase.php | 37 ++++ src/web/base/TRenderer.php | 11 - src/web/base/TextRenderer.php | 11 + src/web/ly.php | 7 + src/web/model/IComponent.php | 12 + src/web/model/IPage.php | 27 ++- src/web/model/IRenderer.php | 2 +- src/web/page.php | 77 +++++-- src/web/route.php | 27 --- 18 files changed, 706 insertions(+), 93 deletions(-) create mode 100644 src/app/web/Application.php rename src/{web/base => app/web}/RouteManager.php (99%) create mode 100644 src/web/base/AbstractComponent.php create mode 100644 src/web/base/AbstractPage.php create mode 100644 src/web/base/Html5PageRenderer.php delete mode 100644 src/web/base/Html5Renderer.php create mode 100644 src/web/base/HtmlRenderer.php create mode 100644 src/web/base/JsonRenderer.php create mode 100644 src/web/base/TComponentPhase.php delete mode 100644 src/web/base/TRenderer.php create mode 100644 src/web/base/TextRenderer.php create mode 100644 src/web/ly.php create mode 100644 src/web/model/IComponent.php delete mode 100644 src/web/route.php diff --git a/src/app/web/Application.php b/src/app/web/Application.php new file mode 100644 index 0000000..fd6a255 --- /dev/null +++ b/src/app/web/Application.php @@ -0,0 +1,236 @@ +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); + } + } + } +} diff --git a/src/web/base/RouteManager.php b/src/app/web/RouteManager.php similarity index 99% rename from src/web/base/RouteManager.php rename to src/app/web/RouteManager.php index 5981668..85391bb 100644 --- a/src/web/base/RouteManager.php +++ b/src/app/web/RouteManager.php @@ -1,5 +1,5 @@ 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 { + ?> + + +page->getSelfRelativePrefix(); + ?> + + + + + +page->getSelfRelativePrefix(); + foreach ($this->cssUrls as $url) { + $url = prefix::add($url, $prefix); + ?> + +getPlugins() as $plugin) { + $plugin->printCss(); + } + } + + function printJsLinks(): void { + $prefix = $this->page->getSelfRelativePrefix(); + foreach ($this->jsUrls as $url) { + $url = prefix::add($url, $prefix); + ?> + +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*\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) { + ?> + +resolvePluginsScripts(); + $this->printMergedScripts(); + } + + function printHeadTitle(): void { + $title = c::to_string($this->page->TITLE()); + echo "$title\n"; + } + + function printEndHead(): void { + ?> + + + +page); + ly::end(); + } + + function printEndBody(): void { + ?> + + + +render(func::with(route::get_error($e))->invoke()); - } - } - } -} diff --git a/src/web/base/HtmlRenderer.php b/src/web/base/HtmlRenderer.php new file mode 100644 index 0000000..dd87ff1 --- /dev/null +++ b/src/web/base/HtmlRenderer.php @@ -0,0 +1,16 @@ +print(); + } else { + echo $data; + } + } +} diff --git a/src/web/base/JsonRenderer.php b/src/web/base/JsonRenderer.php new file mode 100644 index 0000000..565e297 --- /dev/null +++ b/src/web/base/JsonRenderer.php @@ -0,0 +1,23 @@ +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; + } +} diff --git a/src/web/base/TRenderer.php b/src/web/base/TRenderer.php deleted file mode 100644 index 59b2a18..0000000 --- a/src/web/base/TRenderer.php +++ /dev/null @@ -1,11 +0,0 @@ -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; } } diff --git a/src/web/route.php b/src/web/route.php deleted file mode 100644 index be247b0..0000000 --- a/src/web/route.php +++ /dev/null @@ -1,27 +0,0 @@ -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(); - } -};