373 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			373 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace nur\v\base;
 | |
| 
 | |
| use nur\A;
 | |
| use nur\b\coll\BaseArray;
 | |
| use nur\b\coll\GenericArray;
 | |
| use nur\b\ExitError;
 | |
| use nur\co;
 | |
| use nur\config;
 | |
| use nur\func;
 | |
| use nur\json;
 | |
| use nur\v\html5\Html5BasicErrorPage;
 | |
| use nur\v\model\IChildComponent;
 | |
| use nur\v\model\IComponent;
 | |
| use nur\v\model\IErrorPage;
 | |
| use nur\v\model\IPage;
 | |
| use nur\v\model\IPageContainer;
 | |
| use nur\v\model\IPlugin;
 | |
| use nur\v\page;
 | |
| use nur\v\prefix;
 | |
| use nur\v\vo;
 | |
| use Throwable;
 | |
| use Traversable;
 | |
| 
 | |
| abstract class AbstractPageContainer implements IPageContainer {
 | |
|   protected static function ensure_preparec(IComponent $c, bool $afterPrepare=false): bool {
 | |
|     if (!$c->didPrepare()) {
 | |
|       $c->beforePrepare();
 | |
|       $c->prepare();
 | |
|       if ($afterPrepare) $c->afterPrepare();
 | |
|       return true;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
|   
 | |
|   protected static function ensure_configc(IComponent $c, array &$config, bool $afterConfig=false): bool {
 | |
|     if (!$c->didConfig()) {
 | |
|       $c->beforeConfig($config);
 | |
|       $c->config($config);
 | |
|       if ($afterConfig) $c->afterConfig();
 | |
|       return true;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
|   
 | |
|   protected static function ensure_setupc(IComponent $c, bool $afterSetup=false, &$output=null): bool {
 | |
|     if (!$c->didSetup()) {
 | |
|       $c->beforeSetup();
 | |
|       $output = $c->setup();
 | |
|       if ($afterSetup) $c->afterSetup();
 | |
|       return true;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
|   
 | |
|   protected static function ensure_teardownc(IComponent $c, bool $afterTeardown=false): bool {
 | |
|     if (!$c->didTeardown()) {
 | |
|       $c->beforeTeardown();
 | |
|       $c->teardown();
 | |
|       if ($afterTeardown) $c->afterTeardown();
 | |
|       return true;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
|   
 | |
|   protected static function printc(IComponent $c, ?array &$config): void {
 | |
|     self::ensure_preparec($c, true);
 | |
|     self::ensure_configc($c, $config, true);
 | |
|     self::ensure_setupc($c, true);
 | |
|     try {
 | |
|       if ($c->haveContent()) vo::write($c);
 | |
|     } finally {
 | |
|       self::ensure_teardownc($c, true);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   #############################################################################
 | |
| 
 | |
|   /** configurer les classes utilisées dans le cadre de ce container */
 | |
|   protected abstract function initToolkit(): void;
 | |
| 
 | |
|   function __construct(?array $config=null) {
 | |
|     $this->initToolkit();
 | |
|     $this->config = [
 | |
|       "configure_options" => null,
 | |
|       "error_page_class" => null,
 | |
|       "self" => null,
 | |
|       "prefix" => prefix::compute(null),
 | |
|       "title" => null,
 | |
|       "css" => [],
 | |
|       "js" => [],
 | |
|       "plugins" => [],
 | |
|     ];
 | |
|     $css = A::getdel($config, "css");
 | |
|     $js = A::getdel($config, "js");
 | |
|     $plugins = A::getdel($config, "plugins");
 | |
|     A::merge_nz($this->config, $config);
 | |
|     A::merge($this->config["css"], $css);
 | |
|     A::merge($this->config["js"], $js);
 | |
|     A::merge($this->config["plugins"], $plugins);
 | |
|     $this->params = new GenericArray();
 | |
|     $this->components = [];
 | |
|   }
 | |
| 
 | |
|   /** @return array les options par défaut pour {@link config::configure()} */
 | |
|   protected function CONFIGURE_OPTIONS(): ?array {
 | |
|     return static::CONFIGURE_OPTIONS;
 | |
|   } const CONFIGURE_OPTIONS = null;
 | |
| 
 | |
|   /** @return string la page utilisée pour afficher les erreurs */
 | |
|   protected function ERROR_PAGE_CLASS(): string {
 | |
|     return static::ERROR_PAGE_CLASS;
 | |
|   } const ERROR_PAGE_CLASS = Html5BasicErrorPage::class;
 | |
| 
 | |
|   /**
 | |
|    * @return string chemin du script correspondant à cette page, depuis la racine
 | |
|    * de l'application e.g. index.php, ou null s'il faut utiliser la valeur par
 | |
|    * défaut
 | |
|    */
 | |
|   protected function SELF(): ?string {
 | |
|     return static::SELF;
 | |
|   } const SELF = null;
 | |
| 
 | |
|   /** @return string titre de la page */
 | |
|   protected function TITLE(): ?string {
 | |
|     return static::TITLE;
 | |
|   } const TITLE = null;
 | |
| 
 | |
|   /** @return ?string|array liste de feuilles CSS à charger dans la page */
 | |
|   protected function CSS() {
 | |
|     return static::CSS;
 | |
|   } const CSS = null;
 | |
| 
 | |
|   /** @return ?string|array liste de scripts à charger dans la page */
 | |
|   protected function JS() {
 | |
|     return static::JS;
 | |
|   } const JS = null;
 | |
| 
 | |
|   /** @return ?string|array liste de plugins à ajouter à cette page */
 | |
|   protected function PLUGINS() {
 | |
|     return static::PLUGINS;
 | |
|   } const PLUGINS = null;
 | |
| 
 | |
|   /** @var array */
 | |
|   protected $config;
 | |
| 
 | |
|   protected function initConfig(): void {
 | |
|     A::replace_z($this->config, "configure_options", $this->CONFIGURE_OPTIONS());
 | |
|     A::replace_z($this->config, "error_page_class", $this->ERROR_PAGE_CLASS());
 | |
|     $self = $this->SELF();
 | |
|     if ($self !== null) {
 | |
|       A::merge($this->config, [
 | |
|         "self" => $self,
 | |
|         "prefix" => prefix::compute($self),
 | |
|       ]);
 | |
|     }
 | |
|     A::replace_z($this->config, "title", $this->TITLE());
 | |
|     A::merge($this->config["css"], $this->CSS());
 | |
|     A::merge($this->config["js"], $this->JS());
 | |
|     A::merge($this->config["plugins"], $this->PLUGINS());
 | |
|   }
 | |
| 
 | |
|   function getConfig(): array { return $this->config; }
 | |
|   function getErrorPage(): IErrorPage {
 | |
|     # NB: cette fonction peut être appelée AVANT initConfig()
 | |
|     $error_page_class = $this->config["error_page_class"];
 | |
|     if ($error_page_class === null) $error_page_class = $this->ERROR_PAGE_CLASS();
 | |
|     if ($error_page_class === null) $error_page_class = Html5BasicErrorPage::class;
 | |
|     $error_page = new $error_page_class();
 | |
|     $error_page->initContainer($this);
 | |
|     return $error_page;
 | |
|   }
 | |
|   function getSelf(): ?string { return $this->config["self"]; }
 | |
|   function getSelfRelativePrefix(): string { return $this->config["prefix"]; }
 | |
|   function getTitle(): string { return $this->config["title"]; }
 | |
|   function getCssUrls(): array { return $this->config["css"]; }
 | |
|   function getJsUrls(): array { return $this->config["js"]; }
 | |
|   function getPlugins(): array { return $this->config["plugins"]; }
 | |
| 
 | |
|   /** @var BaseArray */
 | |
|   protected $params;
 | |
| 
 | |
|   function getParams(): BaseArray {
 | |
|     return $this->params;
 | |
|   }
 | |
| 
 | |
|   function haveError(): bool {
 | |
|     return boolval($this->params["page_error"]);
 | |
|   }
 | |
| 
 | |
|   function setError(?string $message, ?Throwable $exception=null): void {
 | |
|     $this->params["page_error"] = [
 | |
|       "message" => $message,
 | |
|       "exception" => $exception,
 | |
|     ];
 | |
|   }
 | |
| 
 | |
|   function getError(): ?array {
 | |
|     return $this->params["page_error"];
 | |
|   }
 | |
| 
 | |
|   # null = init, 0 = prepare, 1 = config, 2 = setup, 3 = print, 4 = teardown
 | |
|   const INIT_PHASE = null;
 | |
|   const PREPARE_PHASE = 0;
 | |
|   const CONFIG_PHASE = 1;
 | |
|   const SETUP_PHASE = 2;
 | |
|   const PRINT_PHASE = 3;
 | |
|   const TEARDOWN_PHASE = 4;
 | |
| 
 | |
|   protected $phase;
 | |
| 
 | |
|   protected function ensure_phasec(IComponent $c) {
 | |
|     $phase = $this->phase;
 | |
|     if ($phase !== self::INIT_PHASE) {
 | |
|       if ($phase >= self::PREPARE_PHASE) self::ensure_preparec($c);
 | |
|       if ($phase >= self::CONFIG_PHASE) self::ensure_configc($c, $this->config);
 | |
|       if ($phase >= self::SETUP_PHASE) self::ensure_setupc($c);
 | |
|       if ($phase >= self::TEARDOWN_PHASE) self::ensure_teardownc($c);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /** @var array composants enregistrés dans ce container, indexés par nom */
 | |
|   protected $components;
 | |
| 
 | |
|   function addPlugin($plugin, ?string $name=null): IPlugin {
 | |
|     if (is_string($plugin)) $plugin = new $plugin();
 | |
|     elseif (is_array($plugin)) $plugin = func::cons(...$plugin);
 | |
|     if ($plugin instanceof IChildComponent) $plugin->initContainer($this);
 | |
|     if ($plugin instanceof IComponent) $this->ensure_phasec($plugin);
 | |
|     A::set($this->components, $name, $plugin);
 | |
|     return $plugin;
 | |
|   }
 | |
| 
 | |
|   /** @var IPage composant de page à afficher dans ce container */
 | |
|   protected $page;
 | |
| 
 | |
|   function setPage(IPage $page): void {
 | |
|     if ($page instanceof IChildComponent) $page->initContainer($this);
 | |
|     $this->page = $page;
 | |
|     $this->haveOutput = false;
 | |
|   }
 | |
| 
 | |
|   /** @var bool ce container a-t-il déjà commencé à afficher du contenu? */
 | |
|   protected $haveOutput;
 | |
| 
 | |
|   function haveOutput(): bool {
 | |
|     return $this->haveOutput;
 | |
|   }
 | |
| 
 | |
|   function print(): void {
 | |
|     $page = $this->page;
 | |
|     page::set_current_page($page);
 | |
|     $output = null;
 | |
| 
 | |
|     try {
 | |
|       $this->phase = self::PREPARE_PHASE;
 | |
|       if (self::ensure_preparec($page)) {
 | |
|         $this->overridePrepare($page);
 | |
|         $page->afterPrepare();
 | |
|       }
 | |
| 
 | |
|       $this->phase = self::CONFIG_PHASE;
 | |
|       $this->initConfig();
 | |
|       if (self::ensure_configc($page, $this->config)) {
 | |
|         $this->overrideConfig($page);
 | |
|         $page->afterConfig();
 | |
|       }
 | |
|       config::configure($this->config["configure_options"]);
 | |
| 
 | |
|       $this->phase = self::SETUP_PHASE;
 | |
|       if (self::ensure_setupc($page, false, $output)) {
 | |
|         if ($output === false)  {
 | |
|           # désactiver la gestion des actions si le retour est false (retour en
 | |
|           # l'état)
 | |
|           $page->dispatchAction(false);
 | |
|         }
 | |
|         $this->overrideSetup($page);
 | |
|         $page->afterSetup();
 | |
|       }
 | |
| 
 | |
|       $this->phase = self::PRINT_PHASE;
 | |
|       if ($output instanceof IComponent) {
 | |
|         # composant autonome
 | |
|         self::ensure_phasec($output);
 | |
|         if ($this->beforePrint($output)) {
 | |
|           $this->haveOutput = true;
 | |
|           co::_print([$output]);
 | |
|         }
 | |
|       } elseif (is_callable($output)) {
 | |
|         # générateur de contenu
 | |
|         if ($this->beforePrint(null)) {
 | |
|           $this->haveOutput = true;
 | |
|           $output();
 | |
|         }
 | |
|       } elseif ($output === false) {
 | |
|         # retour en l'état (contenu déjà géré dans setup())
 | |
|         $this->haveOutput = false;
 | |
|       } elseif ($output !== null) {
 | |
|         # contenu json ou texte
 | |
|         if ($this->beforePrint(null)) {
 | |
|           $this->haveOutput = true;
 | |
|           if (is_iterable($output)) {
 | |
|             header("Content-Type: application/json");
 | |
|             echo "[";
 | |
|             $sep = "";
 | |
|             foreach ($output as $data) {
 | |
|               $line = json::encode($data);
 | |
|               echo "$sep$line\n";
 | |
|               $sep = ",";
 | |
|             }
 | |
|             echo "]";
 | |
|           } else {
 | |
|             co::_write([strval($output)]);
 | |
|           }
 | |
|         }
 | |
|       } else {
 | |
|         # afficher la page normalement
 | |
|         $this->overridePrint($page);
 | |
|       }
 | |
|     } catch (Throwable $e) {
 | |
|       if ($e instanceof ExitError && !$e->isError()) {
 | |
|         # NOP
 | |
|       } else {
 | |
|         $this->setError(null, $e);
 | |
|       }
 | |
| 
 | |
|     } finally {
 | |
|       if ($this->haveError()) {
 | |
|         $errorPage = $this->getErrorPage();
 | |
|         if ($this->haveOutput()) {
 | |
|           # n'afficher que l'erreur si on a déjà du contenu
 | |
|           $errorPage->printError();
 | |
|         } else {
 | |
|           # sinon afficher la page entière
 | |
|           $errorPage->print();
 | |
|         }
 | |
|       }
 | |
|       if ($page->didSetup()) {
 | |
|         $this->phase = self::TEARDOWN_PHASE;
 | |
|         if ($output instanceof IComponent) {
 | |
|           self::ensure_teardownc($output);
 | |
|         }
 | |
|         if (self::ensure_teardownc($page)) {
 | |
|           $this->overrideTeardown($page);
 | |
|           $page->afterTeardown();
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   protected function overridePrepare(IPage $page): void {
 | |
|   }
 | |
| 
 | |
|   protected function overrideConfig(IPage $page): void {
 | |
|   }
 | |
| 
 | |
|   protected function overrideSetup(IPage $page): void {
 | |
|   }
 | |
| 
 | |
|   protected function beforePrint(?IComponent $component): bool {
 | |
|     return $component === null || $component->haveContent();
 | |
|   }
 | |
| 
 | |
|   protected function overridePrint(IPage $page): void {
 | |
|     if (!$this->beforePrint($page)) return;
 | |
|     $this->haveOutput = true;
 | |
|     co::_print([$page]);
 | |
|   }
 | |
| 
 | |
|   protected function overrideTeardown(IPage $page): void {
 | |
|   }
 | |
| }
 |