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");
 | 
						|
  }
 | 
						|
}
 |