444 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			444 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace nur;
 | |
| 
 | |
| use Closure;
 | |
| use nur\b\ValueException;
 | |
| use ReflectionClass;
 | |
| use ReflectionFunction;
 | |
| use ReflectionMethod;
 | |
| 
 | |
| /**
 | |
|  * Class func: outils pour appeler des fonctions et méthodes dynamiquement
 | |
|  */
 | |
| class func {
 | |
|   /**
 | |
|    * tester si $func est une chaine de la forme "XXX::method" où XXX est une
 | |
|    * chaine quelconque éventuellement vide, ou un tableau de la forme ["method"]
 | |
|    * ou [anything, "method", ...]
 | |
|    */
 | |
|   static final function is_static($func): bool {
 | |
|     if (is_string($func)) {
 | |
|       $pos = strpos($func, "::");
 | |
|       if ($pos === false) return false;
 | |
|       return $pos + 2 < strlen($func);
 | |
|     } elseif (is_array($func) && array_key_exists(0, $func)) {
 | |
|       $count = count($func);
 | |
|       if ($count == 1) {
 | |
|         return is_string($func[0]) && strlen($func[0]) > 0;
 | |
|       } elseif ($count > 1) {
 | |
|         if (!array_key_exists(1, $func)) return false;
 | |
|         return is_string($func[1]) && strlen($func[1]) > 0;
 | |
|       }
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * si $func est une chaine de la forme "::method" alors la remplacer par la
 | |
|    * chaine "$class::method"
 | |
|    *
 | |
|    * si $func est un tableau de la forme ["method"] ou [null, "method"], alors
 | |
|    * le remplacer par [$class, "method"]
 | |
|    *
 | |
|    * on assume que {@link is_static()}($func) retourne true
 | |
|    *
 | |
|    * @return bool true si la correction a été faite
 | |
|    */
 | |
|   static final function fix_static(&$func, $class): bool {
 | |
|     if (is_object($class)) $class = get_class($class);
 | |
| 
 | |
|     if (is_string($func) && substr($func, 0, 2) == "::") {
 | |
|       $func = "$class$func";
 | |
|       return true;
 | |
|     } elseif (is_array($func) && array_key_exists(0, $func)) {
 | |
|       $count = count($func);
 | |
|       if ($count == 1) {
 | |
|         $func = [$class, $func[0]];
 | |
|         return true;
 | |
|       } elseif ($count > 1 && $func[0] === null) {
 | |
|         $func[0] = $class;
 | |
|         return true;
 | |
|       }
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /** tester si $method est une chaine de la forme "->method" */
 | |
|   private static function isam($method): bool {
 | |
|     return is_string($method)
 | |
|       && strlen($method) > 2
 | |
|       && substr($method, 0, 2) == "->";
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * tester si $func est une chaine de la forme "->method" ou un tableau de la
 | |
|    * forme ["->method", ...] ou [anything, "->method", ...]
 | |
|    */
 | |
|   static final function is_method($func): bool {
 | |
|     if (is_string($func)) {
 | |
|       return self::isam($func);
 | |
|     } elseif (is_array($func) && array_key_exists(0, $func)) {
 | |
|       if (self::isam($func[0])) {
 | |
|         # ["->method", ...]
 | |
|         return true;
 | |
|       }
 | |
|       if (array_key_exists(1, $func) && self::isam($func[1])) {
 | |
|         # [anything, "->method", ...]
 | |
|         return true;
 | |
|       }
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * si $func est une chaine de la forme "->method" alors la remplacer par le
 | |
|    * tableau [$object, "method"]
 | |
|    *
 | |
|    * si $func est un tableau de la forme ["->method"] ou [anything, "->method"],
 | |
|    * alors le remplacer par [$object, "method"]
 | |
|    *
 | |
|    * @return bool true si la correction a été faite
 | |
|    */
 | |
|   static final function fix_method(&$func, $object): bool {
 | |
|     if (!is_object($object)) return false;
 | |
| 
 | |
|     if (is_string($func)) {
 | |
|       if (self::isam($func)) {
 | |
|         $func = [$object, substr($func, 2)];
 | |
|         return true;
 | |
|       }
 | |
|     } elseif (is_array($func) && array_key_exists(0, $func)) {
 | |
|       if (self::isam($func[0])) $func = array_merge([null], $func);
 | |
|       if (count($func) > 1 && array_key_exists(1, $func) && self::isam($func[1])) {
 | |
|         $func[0] = $object;
 | |
|         $func[1] = substr($func[1], 2);
 | |
|         return true;
 | |
|       }
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * si $func est un tableau de plus de 2 éléments, alors déplacer les éléments
 | |
|    * supplémentaires au début de $args. par exemple:
 | |
|    * ~~~
 | |
|    * $func = ["class", "method", "arg1", "arg2"];
 | |
|    * $args = ["arg3"];
 | |
|    * func::fix_args($func, $args)
 | |
|    * # $func === ["class", "method"]
 | |
|    * # $args === ["arg1", "arg2", "arg3"]
 | |
|    * ~~~
 | |
|    *
 | |
|    * @return bool true si la correction a été faite
 | |
|    */
 | |
|   static final function fix_args(&$func, ?array &$args): bool {
 | |
|     if ($args === null) $args = [];
 | |
|     if (is_array($func) && count($func) > 2) {
 | |
|       $prefix_args = array_slice($func, 2);
 | |
|       $func = array_slice($func, 0, 2);
 | |
|       $args = array_merge($prefix_args, $args);
 | |
|       return true;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * s'assurer que $func est un appel de méthode ou d'une méthode statique;
 | |
|    * et renseigner le cas échéant les arguments. si $func ne fait pas mention
 | |
|    * de la classe ou de l'objet, le renseigner avec $class_or_object.
 | |
|    *
 | |
|    * @return bool true si c'est une fonction valide. il ne reste plus qu'à
 | |
|    * l'appeler avec {@link call()}
 | |
|    */
 | |
|   static final function check_func(&$func, $class_or_object, &$args=null): bool {
 | |
|     if ($func instanceof Closure) return true;
 | |
|     if (self::is_method($func)) {
 | |
|       # méthode
 | |
|       self::fix_method($func, $class_or_object);
 | |
|       self::fix_args($func, $args);
 | |
|       return true;
 | |
|     } elseif (self::is_static($func)) {
 | |
|       # méthode statique
 | |
|       self::fix_static($func, $class_or_object);
 | |
|       self::fix_args($func, $args);
 | |
|       return true;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Comme {@link check_func()} mais lance une exception si la fonction est
 | |
|    * invalide
 | |
|    *
 | |
|    * @throws ValueException si $func n'est pas une fonction ou une méthode valide
 | |
|    */
 | |
|   static final function ensure_func(&$func, $class_or_object, &$args=null): void {
 | |
|     if (!self::check_func($func, $class_or_object, $args)) {
 | |
|       throw ValueException::unexpected_type("callable", $func);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   static final function _prepare($func): array {
 | |
|     $object = null;
 | |
|     if (is_callable($func)) {
 | |
|       if (is_array($func)) {
 | |
|         $rf = new ReflectionMethod(...$func);
 | |
|         $object = $func[0];
 | |
|         if (is_string($object)) $object = null;
 | |
|       } elseif ($func instanceof Closure) {
 | |
|         $rf = new ReflectionFunction($func);
 | |
|       } elseif (is_string($func) && strpos($func, "::") === false) {
 | |
|         $rf = new ReflectionFunction($func);
 | |
|       } else {
 | |
|         $rf = new ReflectionMethod($func);
 | |
|       }
 | |
|     } elseif ($func instanceof ReflectionMethod) {
 | |
|       $rf = $func;
 | |
|     } elseif ($func instanceof ReflectionFunction) {
 | |
|       $rf = $func;
 | |
|     } elseif (is_array($func) && count($func) == 2 && isset($func[0]) && isset($func[1])
 | |
|       && ($func[1] instanceof ReflectionMethod || $func[1] instanceof ReflectionFunction)) {
 | |
|       $object = $func[0];
 | |
|       if (is_string($object)) $object = null;
 | |
|       $rf = $func[1];
 | |
|     } elseif (is_string($func) && strpos($func, "::") === false) {
 | |
|       $rf = new ReflectionFunction($func);
 | |
|     } else {
 | |
|       throw ValueException::unexpected_type("callable", $func);
 | |
|     }
 | |
|     if ($rf->isVariadic()) {
 | |
|       $minArgs = $maxArgs = -1;
 | |
|     } else {
 | |
|       $minArgs = $rf->getNumberOfRequiredParameters();
 | |
|       $maxArgs = $rf->getNumberOfParameters();
 | |
|     }
 | |
|     return [$rf instanceof ReflectionMethod, $object, $rf, $minArgs, $maxArgs];
 | |
|   }
 | |
| 
 | |
|   static final function _fill(array $context, array &$args): void {
 | |
|     $minArgs = $context[3];
 | |
|     $maxArgs = $context[4];
 | |
|     if ($maxArgs != -1) $args = array_slice($args, 0, $maxArgs);
 | |
|     if ($minArgs != -1) {
 | |
|       while (count($args) < $minArgs) {
 | |
|         $args[] = null;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   static final function _call($context, array $args) {
 | |
|     self::_fill($context, $args);
 | |
|     $use_object = $context[0];
 | |
|     $object = $context[1];
 | |
|     $method = $context[2];
 | |
|     if ($use_object) {
 | |
|       if (count($args) === 0) return $method->invoke($object);
 | |
|       else return $method->invokeArgs($object, $args);
 | |
|     } else {
 | |
|       if (count($args) === 0) return $method->invoke();
 | |
|       else return $method->invokeArgs($args);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Appeler la fonction spécifiée avec les arguments spécifiés.
 | |
|    * Adapter $args en fonction du nombre réel d'arguments de $func
 | |
|    *
 | |
|    * @param callable|ReflectionFunction|ReflectionMethod $func
 | |
|    */
 | |
|   static final function call($func, ...$args) {
 | |
|     return self::_call(self::_prepare($func), $args);
 | |
|   }
 | |
| 
 | |
|   /** remplacer $value par $func($value, ...$args) */
 | |
|   static final function apply(&$value, $func, ...$args): void {
 | |
|     if ($func !== null) {
 | |
|       if ($args) $args = array_merge([$value], $args);
 | |
|       else $args = [$value];
 | |
|       $value = self::call($func, ...$args);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   const CALL_ALL_SCHEMA = [
 | |
|     "prefix" => [null, null, "Ne sélectionner que les méthode dont le nom commence par ce préfixe"],
 | |
|     "args" => [null, null, "Arguments avec lesquels appeler les méthodes"],
 | |
|     "static_only" => [null, false, "N'appeler que les méthodes statiques si un objet est spécifié"],
 | |
|     "include" => [null, null, "N'inclure que les méthodes dont le nom correspond à ce motif, qui peut être une sous-chaine ou une expression régulière"],
 | |
|     "exclude" => [null, null, "Exclure les méthodes dont le nom correspond à ce motif, qui peut être une sous-chaine ou une expression régulière"],
 | |
|   ];
 | |
|   const MASK_PS = ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC;
 | |
|   const MASK_P = ReflectionMethod::IS_PUBLIC;
 | |
|   const METHOD_PS = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC;
 | |
|   const METHOD_P = ReflectionMethod::IS_PUBLIC;
 | |
| 
 | |
|   private static function matches(string $name, array $includes, array $excludes): bool {
 | |
|     if ($includes) {
 | |
|       $matches = false;
 | |
|       foreach ($includes as $include) {
 | |
|         if (substr($include, 0, 1) == "/") {
 | |
|           # expression régulière
 | |
|           if (preg_match($include, $name)) {
 | |
|             $matches = true;
 | |
|             break;
 | |
|           }
 | |
|         } else {
 | |
|           # tester la présence de la sous-chaine
 | |
|           if (strpos($name, $include) !== false) {
 | |
|             $matches = true;
 | |
|             break;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       if (!$matches) return false;
 | |
|     }
 | |
|     foreach ($excludes as $exclude) {
 | |
|       if (substr($exclude, 0, 1) == "/") {
 | |
|         # expression régulière
 | |
|         if (preg_match($exclude, $name)) return false;
 | |
|       } else {
 | |
|         # tester la présence de la sous-chaine
 | |
|         if (strpos($name, $exclude) !== false) return false;
 | |
|       }
 | |
|     }
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * retourner la liste des méthodes de $class_or_object qui correspondent au
 | |
|    * filtre $options. le filtre doit respecter le schéme {@link CALL_ALL_SCHEMA}
 | |
|    */
 | |
|   static function get_all($class_or_object, $options=null): array {
 | |
|     md::ensure_schema($options, self::CALL_ALL_SCHEMA, null, false);
 | |
|     $prefix = $options["prefix"]; $length = strlen($prefix);
 | |
|     $args = A::with($options["args"]);
 | |
|     $includes = A::with($options["include"]);
 | |
|     $excludes = A::with($options["exclude"]);
 | |
|     $methods = [];
 | |
|     if (is_callable($class_or_object, true) && is_array($class_or_object)) {
 | |
|       # callable sous forme de tableau
 | |
|       $class_or_object = $class_or_object[0];
 | |
|     }
 | |
|     if (is_string($class_or_object)) {
 | |
|       # lister les méthodes publiques statiques de la classe
 | |
|       $mask = self::MASK_PS;
 | |
|       $expected = self::METHOD_PS;
 | |
|       $c = new ReflectionClass($class_or_object);
 | |
|     } elseif (is_object($class_or_object)) {
 | |
|       # lister les méthodes publiques de la classe
 | |
|       $c = new ReflectionClass($class_or_object);
 | |
|       $mask = $options["static_only"]? self::MASK_PS: self::MASK_P;
 | |
|       $expected = $options["static_only"]? self::METHOD_PS: self::METHOD_P;
 | |
|     } else {
 | |
|       throw new ValueException("$class_or_object: vous devez spécifier une classe ou un objet");
 | |
|     }
 | |
|     foreach ($c->getMethods() as $m) {
 | |
|       if (($m->getModifiers() & $mask) != $expected) continue;
 | |
|       $name = $m->getName();
 | |
|       if (substr($name, 0, $length) != $prefix) continue;
 | |
|       if (!self::matches($name, $includes, $excludes)) continue;
 | |
|       $method = [$class_or_object, $name];
 | |
|       A::merge($method, $args);
 | |
|       $methods[] = $method;
 | |
|     }
 | |
|     return $methods;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Appeler toutes les méthodes publiques de $object_or_class et retourner un
 | |
|    * tableau [$method_name => $return_value] des valeurs de retour.
 | |
|    */
 | |
|   static final function call_all($class_or_object, $options=null): array {
 | |
|     $methods = self::get_all($class_or_object, $options);
 | |
|     $values = [];
 | |
|     foreach ($methods as $method) {
 | |
|       $args = null;
 | |
|       self::fix_args($method, $args);
 | |
|       $values[$method[1]] = self::call($method, ...$args);
 | |
|     }
 | |
|     return $values;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * tester si $func est une chaine de la forme "XXX" où XXX est une classe
 | |
|    * valide, ou un tableau de la forme ["XXX", ...]
 | |
|    *
 | |
|    * NB: il est possible d'avoir {@link is_static()} et {@link is_class()}
 | |
|    * vraies pour la même valeur. s'il faut supporter les deux cas, appeler
 | |
|    * {@link is_class()} d'abord
 | |
|    */
 | |
|   static final function is_class($class): bool {
 | |
|     if (is_string($class)) {
 | |
|       return class_exists($class);
 | |
|     } elseif (is_array($class) && array_key_exists(0, $class)) {
 | |
|       return class_exists($class[0]);
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * en assumant que {@link is_class()} est vrai, si $class est un tableau de
 | |
|    * plus de 1 éléments, alors déplacer les éléments supplémentaires au début de
 | |
|    * $args. par exemple:
 | |
|    * ~~~
 | |
|    * $class = ["class", "arg1", "arg2"];
 | |
|    * $args = ["arg3"];
 | |
|    * func::fix_class_args($class, $args)
 | |
|    * # $class === "class"
 | |
|    * # $args === ["arg1", "arg2", "arg3"]
 | |
|    * ~~~
 | |
|    *
 | |
|    * @return bool true si la correction a été faite
 | |
|    */
 | |
|   static final function fix_class_args(&$class, ?array &$args): bool {
 | |
|     if ($args === null) $args = [];
 | |
|     if (is_array($class)) {
 | |
|       if (count($class) > 1) {
 | |
|         $prefix_args = array_slice($class, 1);
 | |
|         $class = array_slice($class, 0, 1)[0];
 | |
|         $args = array_merge($prefix_args, $args);
 | |
|       } else {
 | |
|         $class = $class[0];
 | |
|       }
 | |
|       return true;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * s'assurer que $class est une classe et renseigner le cas échéant les
 | |
|    * arguments.
 | |
|    *
 | |
|    * @return bool true si c'est une classe valide. il ne reste plus qu'à
 | |
|    * l'instancier avec {@link cons()}
 | |
|    */
 | |
|   static final function check_class(&$class, &$args=null): bool {
 | |
|     if (self::is_class($class)) {
 | |
|       self::fix_class_args($class, $args);
 | |
|       return true;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Instancier la classe avec les arguments spécifiés.
 | |
|    * Adapter $args en fonction du nombre réel d'arguments du constructeur
 | |
|    */
 | |
|   static final function cons(string $class, ...$args) {
 | |
|     $c = new ReflectionClass($class);
 | |
|     $rf = $c->getConstructor();
 | |
|     if ($rf === null) {
 | |
|       return $c->newInstance();
 | |
|     } else {
 | |
|       if (!$rf->isVariadic()) {
 | |
|         $minArgs = $rf->getNumberOfRequiredParameters();
 | |
|         $maxArgs = $rf->getNumberOfParameters();
 | |
|         $args = array_slice($args, 0, $maxArgs);
 | |
|         while (count($args) < $minArgs) {
 | |
|           $args[] = null;
 | |
|         }
 | |
|       }
 | |
|       return $c->newInstanceArgs($args);
 | |
|     }
 | |
|   }
 | |
| }
 |