Création d'un mécanisme permettant l'usage de multiples constructeurs en PHP

Utilisation de la reflection en PHP pour créer une classe abstraction permettant l'utilisation automatique de plusieurs constructeurs dans la même classe.

  Publié le

En php à l’inverse de nombreux langages il n’est pas nativement possible de créer un objet avec différents constructeurs.

Pour cause le moteur de PHP qui n’autorise qu’une seule fonction du même nom par classe et, cela sans tenir compte du nombre des types et des arguments qui lui sont fournis.

La création de plusieurs fonctions __construct dans notre classe lèvera obligatoirement une erreur fatale.

Pour palier à ce problème nombreux débutants utilises un mécanisme qui comptabilise le nombre de paramètres envoyé au constructeur de l’objet et qui appel la fonction __constructX (X étant le nombre d’argument).

Cette fausse bonne idée est pernicieuse et amènera immanquablement à des problèmes majeurs par la suite. En outre elle ne couvre pas les paramètres optionnels et leurs valeurs par défaut.

Il est donc préférable d’utiliser une logique plus complète qui repose en partie sur les objets proposés par la Reflection.

Un objet abstract_object servira de classe de référence et contiendra le code capable de couvrir l’intégralité des besoins d’un mécanisme de multi constructeurs.

A savoir :

  • Comptabiliser le nombre d’argument.

  • Contrôler le typage des arguments envoyés.

  • Détecter si un argument optionnel est absent.

  • Assigner les valeurs par défaut des arguments optionnels de la fonction.

  • Exclure le plus tôt possible les constructeurs qui ne correspondent pas aux arguments.

  • Garantir une structure de code et un cloisonnement en permettant l’appel de constructeur d’accessibilité publique ou privé.

/**Objet abstrait générique permettant de gérer les constructeurs multiples */
	abstract class abstract_object
	{
		public function __construct($in_class_caller, array $in_args)
		{
			try {
				$this->auto_map_multi_constructors($in_class_caller, $in_args);
			} catch (\Throwable $th) {
				throw  $th;
			}
		}

		/**Initialise automatiquement le bon constructeur par rapport aux arguments envoyés au constructeur natif */
		private function auto_map_multi_constructors($in_class_caller, array $in_c_args)
		{
			try {
				/**Arguments à envoyer à la fonction finale */
				$safety_args = [];
				$init = false;

				/**Récupère les paramètres du constructeur*/
				$c_nb_args = count($in_c_args);

				/**Détecte les constructeurs altérnatifs présent dans la classe appelantequi sont soit d'accessibilité private ou public */
				$class = new ReflectionClass(get_class($in_class_caller));
				$public_methods = $class->getMethods(ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PRIVATE);
				foreach ($public_methods as $index => $method) {
					/**Contrôle que la méthode répond bien au format __constructVOTREEXTENSION pour empécher l'appel d'autre fonction*/
					if (preg_match("#^__construct[a-z0-9\_]{1,}$#", $method->getName())) {
						/**Récupère les paramètres attendues par la méthode */
						$i_args = $method->getParameters();
						/**Compte le total des paramètres attendus */
						$i_max_args = count($i_args);

						/**Si plus de paramètre que le maximum : ignoré le constructeur */
						if ($c_nb_args <= $i_max_args) {

							$i = 0;
							foreach ($i_args as $arg) {
								//Type de l'argument attendu par ce paramètre
								$m_type = $arg->getType();

								if (!isset($in_c_args[$i]) && !isset($safety_args[$i])) {
									if ($arg->isOptional()) {
										/**Force la valeur par défaut pour les paramètres optionels */
										$safety_args[$i] = $arg->getDefaultValue();
									} else {
										//throw new \Error("Argument manquant : [" . $arg->getName() . "]");
									}
								} else {
									/**Contrôle les typages */
									$c_val = $in_c_args[$i];
									$c_type = gettype($c_val);

									/**Transcodage des typages */
									switch ($c_type) {
										case 'object':
											$c_type = get_class($c_val);
											break;
										case 'integer':
											$c_type = 'int';
											break;
										case 'boolean':
											$c_type = 'bool';
											break;
										default:

											break;
									}

									/**Le type correspond pour l'index ? Si oui, ajouter */
									if (strtolower($c_type) == strtolower($m_type->getName())) {
										$safety_args[$i] = $c_val;
									} else {
										/** Typage discordant par rapport à l'index : ignoré le constructeur*/
										break;
									}
								}
								$i++;
							}
							
							/**Si les arguments générés répondent aux nombres d'arguments attendus, appeler la fonction. */
							if (count($safety_args) === $i_max_args) {
								$method->invokeArgs($in_class_caller, $safety_args);
								$init = true;
								break;
							}
						}
					}
				}

				if ($init == false) throw new \Error("Objet non initialisée constructeur introuvable par rapport aux paramètres");
			} catch (\Throwable $th) {
				throw  $th;
			}
		}
	}

Un objet test qui servira de classe de test et s’appuiera sur l’objet abstract_object et permettra de tester l’ensemble des différents constructeurs.

On notera que les constructeurs alternatifs sont d'accessibilités private pour garantir l'usage du constructeur natif.

class test extends abstract_object
	{
		public $name = "undefine";
		public function __construct()
		{
			try {
				parent::__construct($this, func_get_args());
			} catch (\Throwable $th) {
				throw $th;
			}
		}

		private function __construct_a(\Datetime $in_dt, machin $in_machin)
		{
			$this->name = "construct_a";
		}

		private function __construct_b(machin $in_machin, \Datetime $in_dt)
		{
			$this->name = "construct_b";
		}

		private function __construct_c(\Datetime $in_dt, machin $in_machin, string $in_text, int $in_size, bool $a, bool $b, ?array $in_arr = null)
		{
			$this->name = "construct_c";
		}

		private function __construct_d(\Datetime $in_dt, string $in_text, int $in_size, bool $a, bool $b, ?array $in_arr = null)
		{
			$this->name = "construct_d";
		}

		private function __construct_e(string $in_text, \Datetime $in_dt, int $in_size, bool $a, bool $b, ?array $in_arr = null)
		{
			$this->name = "construct_e";
		}

		public function get_name()
		{
			return $this->name;
		}
	}

Un objet machin qui sera utilisé pour vérifier que les typages des paramètres sont bien contrôlés par notre fonction magique.

	class machin
	{
		public function __construct(string $in_text)
		{
		}
	}

Les tests multi constructeurs en PHP

try {
		$dt = new \Datetime("now");
		$machin = new machin("sdfsdfsdf");
	
		/**
		* Test d'exécution ne doit appeler aucun constructeur et lever une erreur */
		//$t = new test(new machin("bidule"), $dt, "toto", 10, true, false, ["tutu"]);
	
		/**Test d'exécution doit appeler construct_a */
		$t = new test($dt, $machin);
		if($t->get_name() != "construct_a") throw new \Error("fail test 1");
	
		/**Test d'exécution doit appeler construct_b */
		$t = new test($machin, $dt);
		if($t->get_name() != "construct_b") throw new \Error("fail test 2");
	
		/**Test d'exécution doit appeler construct_c */
		$t = new test($dt, $machin, "toto", 10, false, false);
		if($t->get_name() != "construct_c") throw new \Error("fail test 3");
	
		$t = new test($dt, $machin, "toto", 10, false, false, null);
		if($t->get_name() != "construct_c") throw new \Error("fail test 4");
		
		$t = new test($dt, $machin, "toto", 10, false, true, ["bloup"]);
		if($t->get_name() != "construct_c") throw new \Error("fail test 5");
	
	
		/**Test d'exécution doit appeler contruct_d */
		$t = new test($dt, "toto", 50, false, false, ["koala"]);
		if($t->get_name() != "construct_d") throw new \Error("fail test 6");
	
		$t = new test($dt, "toto", 50, false, false, null);
		if($t->get_name() != "construct_d") throw new \Error("fail test 7");
	
		$t = new test($dt, "toto", 50, false, false);
		if($t->get_name() != "construct_d") throw new \Error("fail test 8");
	
	
		/**Test d'exécution doit appeler construct_e */
		$t = new test("test", $dt, 50, true, false, ["bidule"]);
		if($t->get_name() != "construct_e") throw new \Error("fail test 6");
		$t = new test("test", $dt, 50, true, false, null);
		if($t->get_name() != "construct_e") throw new \Error("fail test 6");
		$t = new test("test", $dt, 50, true, false);
		if($t->get_name() != "construct_e") throw new \Error("fail test 6");

	} catch (\Throwable $th) {
		echo  $th->getMessage();
	}
	finally{
		echo "tests terminés";
		die();
	}