263 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			263 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace nur\v\base;
 | |
| 
 | |
| use nur\A;
 | |
| use nur\b\ValueException;
 | |
| use nur\func;
 | |
| use nur\md;
 | |
| use nur\str;
 | |
| use nur\v\html5\Html5BasicErrorPage;
 | |
| use nur\v\model\IPage;
 | |
| use nur\v\model\IRouteManager;
 | |
| 
 | |
| class RouteManager implements IRouteManager {
 | |
|   const DEFAULT_PAGE = Html5BasicErrorPage::class;
 | |
| 
 | |
|   function __construct(?array $routes=null) {
 | |
|     $this->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 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");
 | |
|   }
 | |
| }
 |