diff --git a/.idea/nulib.iml b/.idea/nulib.iml index 265b084..4a9634a 100644 --- a/.idea/nulib.iml +++ b/.idea/nulib.iml @@ -6,6 +6,8 @@ + + diff --git a/composer.json b/composer.json index 879eb3b..3150c95 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,8 @@ "autoload": { "psr-4": { "nulib\\": "php/src_base", + "nulib\\ref\\": "php/src_ref", + "nulib\\sys\\": "php/src_sys", "nulib\\output\\": "php/src_output", "nulib\\web\\": "php/src_web" } diff --git a/php/src_ref/sys/ref_func.php b/php/src_ref/sys/ref_func.php new file mode 100644 index 0000000..12523f8 --- /dev/null +++ b/php/src_ref/sys/ref_func.php @@ -0,0 +1,12 @@ + ["string", null, "Ne sélectionner que les méthode dont le nom commence par ce préfixe"], + "args" => ["?array", null, "Arguments avec lesquels appeler les méthodes"], + "static_only" => ["bool", false, "N'appeler que les méthodes statiques si un objet est spécifié"], + "include" => ["?array", 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" => ["?array", null, "Exclure les méthodes dont le nom correspond à ce motif, qui peut être une sous-chaine ou une expression régulière"], + ]; +} diff --git a/php/src_sys/README.md b/php/src_sys/README.md new file mode 100644 index 0000000..08ccfe2 --- /dev/null +++ b/php/src_sys/README.md @@ -0,0 +1,5 @@ +# nulib\sys + +Ce package contient des services généraux spécifiques à PHP + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/php/src_sys/func.php b/php/src_sys/func.php new file mode 100644 index 0000000..d48287d --- /dev/null +++ b/php/src_sys/func.php @@ -0,0 +1,430 @@ + 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::invalid_type($func, "callable"); + } + } + + 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::invalid_type($func, "callable"); + } + $minArgs = $rf->getNumberOfRequiredParameters(); + $maxArgs = $rf->getNumberOfParameters(); + $variadic = $rf->isVariadic(); + return [$rf instanceof ReflectionMethod, $object, $rf, $minArgs, $maxArgs, $variadic]; + } + + static final function _fill(array $context, array &$args): void { + $minArgs = $context[3]; + $maxArgs = $context[4]; + $variadic = $context[5]; + if (!$variadic) $args = array_slice($args, 0, $maxArgs); + 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 MASK_PS = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC; + const MASK_P = ReflectionMethod::IS_PUBLIC; + const METHOD_PS = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC; + const METHOD_P = ReflectionMethod::IS_PUBLIC; + + private static final 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_PARAMS_SCHEMA} + */ + static function get_all($class_or_object, $params=null): array { + Schema::nv($paramsv, $params, null, $schema, ref_func::CALL_ALL_PARAMS_SCHEMA); + 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 = $params["static_only"]? self::MASK_PS: self::MASK_P; + $expected = $params["static_only"]? self::METHOD_PS: self::METHOD_P; + } else { + throw new ValueException("$class_or_object: vous devez spécifier une classe ou un objet"); + } + $prefix = $params["prefix"]; $prefixlen = strlen($prefix); + $args = $params["args"]; + $includes = $params["include"]; + $excludes = $params["exclude"]; + $methods = []; + foreach ($c->getMethods() as $m) { + if (($m->getModifiers() & $mask) != $expected) continue; + $name = $m->getName(); + if (substr($name, 0, $prefixlen) != $prefix) continue; + if (!self::matches($name, $includes, $excludes)) continue; + $methods[] = cl::merge([$class_or_object, $name], $args); + } + 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, $params=null): array { + $methods = self::get_all($class_or_object, $params); + $values = []; + foreach ($methods as $method) { + 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); + } + } +} diff --git a/php/tests/sys/funcTest.php b/php/tests/sys/funcTest.php new file mode 100644 index 0000000..35f1f37 --- /dev/null +++ b/php/tests/sys/funcTest.php @@ -0,0 +1,247 @@ +")); + self::assertTrue(func::is_method("->xxx")); + self::assertFalse(func::is_method([])); + self::assertFalse(func::is_method([""])); + self::assertTrue(func::is_method(["->xxx"])); + self::assertTrue(func::is_method(["->xxx", "aaa"])); + self::assertFalse(func::is_method([null, "->"])); + self::assertTrue(func::is_method([null, "->yyy"])); + self::assertFalse(func::is_method(["xxx", "->"])); + self::assertTrue(func::is_method(["xxx", "->yyy"])); + self::assertTrue(func::is_method([null, "->yyy", "aaa"])); + self::assertTrue(func::is_method(["xxx", "->yyy", "aaa"])); + } + + function testFix_method() { + $object = new \stdClass(); + $func= null; + func::fix_method($func, $object); + self::assertSame(null, $func); + $func= ""; + func::fix_method($func, $object); + self::assertSame("", $func); + $func= "->"; + func::fix_method($func, $object); + self::assertSame("->", $func); + $func= "->xxx"; + func::fix_method($func, $object); + self::assertSame([$object, "xxx"], $func); + $func= []; + func::fix_method($func, $object); + self::assertSame([], $func); + $func= [""]; + func::fix_method($func, $object); + self::assertSame([""], $func); + $func= ["->xxx"]; + func::fix_method($func, $object); + self::assertSame([$object, "xxx"], $func); + $func= ["->xxx", "aaa"]; + func::fix_method($func, $object); + self::assertSame([$object, "xxx", "aaa"], $func); + $func= [null, "->"]; + func::fix_method($func, $object); + self::assertSame([null, "->"], $func); + $func= [null, "->yyy"]; + func::fix_method($func, $object); + self::assertSame([$object, "yyy"], $func); + $func= ["xxx", "->"]; + func::fix_method($func, $object); + self::assertSame(["xxx", "->"], $func); + $func= ["xxx", "->yyy"]; + func::fix_method($func, $object); + self::assertSame([$object, "yyy"], $func); + $func= [null, "->yyy", "aaa"]; + func::fix_method($func, $object); + self::assertSame([$object, "yyy", "aaa"], $func); + $func= ["xxx", "->yyy", "aaa"]; + func::fix_method($func, $object); + self::assertSame([$object, "yyy", "aaa"], $func); + } + + function testCall() { + self::assertSame(36, func::call("func36")); + self::assertSame(12, func::call(TC::class."::method")); + self::assertSame(12, func::call([TC::class, "method"])); + $closure = function() { + return 21; + }; + self::assertSame(21, func::call($closure)); + } + + function testCall_all() { + $c1 = new C1(); + $c2 = new C2(); + $c3 = new C3(); + + self::assertSameValues([11, 12], func::call_all(C1::class)); + self::assertSameValues([11, 12, 21, 22], func::call_all($c1)); + self::assertSameValues([13, 11, 12], func::call_all(C2::class)); + self::assertSameValues([13, 23, 11, 12, 21, 22], func::call_all($c2)); + self::assertSameValues([111, 13, 12], func::call_all(C3::class)); + self::assertSameValues([111, 121, 13, 23, 12, 22], func::call_all($c3)); + + $options = "conf"; + self::assertSameValues([11], func::call_all(C1::class, $options)); + self::assertSameValues([11, 21], func::call_all($c1, $options)); + self::assertSameValues([11], func::call_all(C2::class, $options)); + self::assertSameValues([11, 21], func::call_all($c2, $options)); + self::assertSameValues([111], func::call_all(C3::class, $options)); + self::assertSameValues([111, 121], func::call_all($c3, $options)); + + $options = ["prefix" => "conf"]; + self::assertSameValues([11], func::call_all(C1::class, $options)); + self::assertSameValues([11, 21], func::call_all($c1, $options)); + self::assertSameValues([11], func::call_all(C2::class, $options)); + self::assertSameValues([11, 21], func::call_all($c2, $options)); + self::assertSameValues([111], func::call_all(C3::class, $options)); + self::assertSameValues([111, 121], func::call_all($c3, $options)); + + self::assertSameValues([11, 12], func::call_all($c1, ["include" => "x"])); + self::assertSameValues([11, 21], func::call_all($c1, ["include" => "y"])); + self::assertSameValues([11, 12, 21], func::call_all($c1, ["include" => ["x", "y"]])); + + self::assertSameValues([21, 22], func::call_all($c1, ["exclude" => "x"])); + self::assertSameValues([12, 22], func::call_all($c1, ["exclude" => "y"])); + self::assertSameValues([22], func::call_all($c1, ["exclude" => ["x", "y"]])); + + self::assertSameValues([12], func::call_all($c1, ["include" => "x", "exclude" => "y"])); + } + + function testCons() { + $obj1 = func::cons(WoCons::class, 1, 2, 3); + self::assertInstanceOf(WoCons::class, $obj1); + + $obj2 = func::cons(WithEmptyCons::class, 1, 2, 3); + self::assertInstanceOf(WithEmptyCons::class, $obj2); + + $obj3 = func::cons(WithCons::class, 1, 2, 3); + self::assertInstanceOf(WithCons::class, $obj3); + self::assertSame(1, $obj3->first); + } + } + + class WoCons { + } + class WithEmptyCons { + function __construct() { + } + } + class WithCons { + public $first; + function __construct($first) { + $this->first = $first; + } + } + + class TC { + static function method() { + return 12; + } + } + + class C1 { + static function confps1_xy() { + return 11; + } + static function ps2_x() { + return 12; + } + function confp1_y() { + return 21; + } + function p2() { + return 22; + } + } + class C2 extends C1 { + static function ps3() { + return 13; + } + function p3() { + return 23; + } + } + class C3 extends C2 { + static function confps1_xy() { + return 111; + } + function confp1_y() { + return 121; + } + } +}