eroutes = []; $this->proutes = []; if ($routes !== null) $this->addRoute(...$routes); } private static function check_page_type($page): array { if (is_array($page) && array_key_exists(0, $page) && is_string($page[0])) { return [$page[0], $page]; } elseif ($page instanceof IPage) { return [$page, $page]; } elseif (is_string($page)) { return [$page, [$page]]; } throw ValueException::unexpected_type(IPage::class, $page); } /** @param $args IPage|array */ private static function get_page($args): IPage { if ($args instanceof IPage) return $args; else return func::cons(...$args); } /** @var IPage|array */ protected $errorPage; function setErrorPage($page) { $this->errorPage = self::check_page_type($page)[1]; } /** * @var array routes pour des chemins exacts. elles ont la priorité sur les * préfixes */ protected $eroutes; /** @var array routes pour des préfixes de chemin */ protected $proutes; private static final 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) { md::ensure_schema($route, self::ROUTE_SCHEMA, null, false); str::del_prefix($route["path"], "/"); $path = $route["path"]; [$page, $cons_args] = self::check_page_type($route["page"]); $route["page"] = $page; $route["cons_args"] = $cons_args; $route["package"] = self::get_package($route["page"]); A::ensure_array($route["aliases"]); switch ($route["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; } # faire les aliases foreach ($route["aliases"] as $alias) { str::del_prefix($alias, "/"); $route["path"] = $alias; $route["mode"] = self::MODE_EXACT; $eroutes[$alias] = $route; } } } /** * 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, "\\", true); return $package.implode("\\", $parts); } /** * sur la base des routes définies, résoudre la classe à instancier pour * traiter le chemin spécifié * * @return IPage|array */ function resolvePage(?string $path) { if ($path === null) $path = $_SERVER["PHP_SELF"]; str::del_prefix($path, "/"); # essayer d'abord des routes exactes $route = A::get($this->eroutes, $path); if ($route !== null) return $route["cons_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, "/", true); if (str::starts_with($prefix, $path)) return $route["cons_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, "/", true); if (str::starts_with($prefix, $path)) { $page = $this->computePackagePage( str::without_prefix($prefix, $path), $route["package"]); if (class_exists($page)) return [$page]; } break; } } # sinon prendre la page par défaut (qui est en fait la page d'erreur) $error = $this->errorPage; if ($error === null) $error = [static::DEFAULT_PAGE]; if (is_array($error)) { if ($page) { A::merge($error, ["$path: unable to find page class $page"]); } else { A::merge($error, ["$path: unable to find page class"]); } } return $error; } function getPage(?string $path=null): IPage { $page = $this->resolvePage($path); if ($page instanceof IPage) return $page; else return func::cons(...$page); } /** * 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, "/", true); # 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 à l'instance de {@link IPage} ou à la * classe spécifiée. * * @param string|IPage $page instance ou classe de la page dont on veut le * chemin depuis la racine de l'application */ function getPath($page): string { if ($page instanceof IPage) { $page = get_class($page); } elseif (!is_string($page)) { throw ValueException::unexpected_type(["string", IPage::class], $page); } # 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 new ValueException(": $page: unable to find path"); } }