SmartObject

SmartObject a amélioré pendant des années le comportement des objets en PHP. Depuis la version PHP 8.4, toutes ses fonctions font déjà partie de PHP lui-même, achevant ainsi sa mission historique d'être un pionnier de l'approche objet moderne en PHP.

Installation :

composer require nette/utils

SmartObject a été créé en 2007 comme une solution révolutionnaire aux lacunes du modèle objet PHP de l'époque. À une époque où PHP souffrait de nombreux problèmes de conception objet, il a apporté une amélioration significative et une simplification du travail pour les développeurs. Il est devenu une partie légendaire du framework Nette. Il offrait des fonctionnalités que PHP n'a acquises que de nombreuses années plus tard – du contrôle d'accès aux propriétés des objets jusqu'aux sucres syntaxiques sophistiqués. Avec l'arrivée de PHP 8.4, il a achevé sa mission historique, car toutes ses fonctions sont devenues une partie native du langage. Il a devancé le développement de PHP de 17 années remarquables.

Techniquement, SmartObject a subi une évolution intéressante. Initialement implémenté comme une classe Nette\Object, dont les autres classes héritaient les fonctionnalités nécessaires. Un changement fondamental est survenu avec PHP 5.4, qui a apporté le support des traits. Cela a permis la transformation sous forme de trait Nette\SmartObject, ce qui a apporté une plus grande flexibilité – les développeurs pouvaient utiliser la fonctionnalité même dans les classes qui héritaient déjà d'une autre classe. Alors que la classe originale Nette\Object a disparu avec l'arrivée de PHP 7.2 (qui a interdit la dénomination des classes avec le mot Object), le trait Nette\SmartObject continue de vivre.

Passons en revue les caractéristiques qu'offraient autrefois Nette\Object et plus tard Nette\SmartObject. Chacune de ces fonctions, à son époque, représentait une avancée significative dans le domaine de la programmation orientée objet en PHP.

États d'erreur cohérents

L'un des problèmes les plus brûlants du PHP précoce était le comportement incohérent lors du travail avec les objets. Nette\Object a apporté ordre et prévisibilité à ce chaos. Regardons à quoi ressemblait le comportement original de PHP :

echo $obj->undeclared;    // E_NOTICE, plus tard E_WARNING
$obj->undeclared = 1;     // passe silencieusement sans rapport
$obj->unknownMethod();    // Erreur fatale (non interceptable avec try/catch)

L'erreur fatale terminait l'application sans possibilité de réagir de quelque manière que ce soit. L'écriture silencieuse dans des membres inexistants sans avertissement pouvait conduire à des erreurs graves difficiles à détecter. Nette\Object interceptait tous ces cas et lançait une exception MemberAccessException, ce qui permettait aux programmeurs de réagir aux erreurs et de les résoudre.

echo $obj->undeclared;   // lance Nette\MemberAccessException
$obj->undeclared = 1;    // lance Nette\MemberAccessException
$obj->unknownMethod();   // lance Nette\MemberAccessException

Depuis PHP 7.0, le langage ne cause plus d'erreurs fatales non interceptables et depuis PHP 8.2, l'accès aux membres non déclarés est considéré comme une erreur.

Aide “Did you mean?”

Nette\Object est venu avec une fonctionnalité très agréable : une aide intelligente en cas de fautes de frappe. Quand un développeur faisait une erreur dans le nom d'une méthode ou d'une variable, non seulement il signalait l'erreur, mais il offrait aussi un coup de main sous forme de suggestion du nom correct. Ce message emblématique, connu sous le nom de “did you mean?”, a épargné aux programmeurs des heures de recherche de fautes de frappe :

class Foo extends Nette\Object
{
	public static function from($var)
	{
	}
}

$foo = Foo::form($var);
// lance Nette\MemberAccessException
// "Appel à la méthode statique non définie Foo::form(), vouliez-vous dire from() ?"

Le PHP d'aujourd'hui, bien qu'il n'ait pas de forme de « did you mean? », mais Tracy sait compléter cet ajout aux erreurs. Et même les corriger automatiquement.

Propriétés avec accès contrôlé

Une innovation significative que SmartObject a apportée à PHP étaient les propriétés avec accès contrôlé. Ce concept, courant dans des langages comme C# ou Python, a permis aux développeurs de contrôler élégamment l'accès aux données de l'objet et d'assurer leur cohérence. Les propriétés sont un outil puissant de la programmation orientée objet. Elles fonctionnent comme des variables, mais en réalité sont représentées par des méthodes (getters et setters). Cela permet de valider les entrées ou de générer des valeurs seulement au moment de la lecture.

Pour utiliser les propriétés, vous devez :

  • Ajouter une annotation à la classe sous la forme @property <type> $xyz
  • Créer un getter avec le nom getXyz() ou isXyz(), un setter avec le nom setXyz()
  • Assurer que le getter et le setter soient public ou protected. Ils sont optionnels – peuvent donc exister comme propriété en lecture seule ou en écriture seule

Montrons un exemple pratique sur la classe Circle, où nous utiliserons les propriétés pour assurer que le rayon sera toujours un nombre non négatif. Remplaçons l'original public $radius par une propriété :

/**
 * @property float $radius
 * @property-read bool $visible
 */
class Circle
{
	use Nette\SmartObject;

	private float $radius = 0.0; // n'est pas public !

	// getter pour la propriété $radius
	protected function getRadius(): float
	{
		return $this->radius;
	}

	// setter pour la propriété $radius
	protected function setRadius(float $radius): void
	{
		// nous nettoyons la valeur avant de l'enregistrer
		$this->radius = max(0.0, $radius);
	}

	// getter pour la propriété $visible
	protected function isVisible(): bool
	{
		return $this->radius > 0;
	}
}

$circle = new Circle;
$circle->radius = 10;  // appelle en réalité setRadius(10)
echo $circle->radius;  // appelle getRadius()
echo $circle->visible; // appelle isVisible()

Depuis PHP 8.4, on peut atteindre la même fonctionnalité en utilisant les property hooks, qui offrent une syntaxe beaucoup plus élégante et concise :

class Circle
{
	public float $radius = 0.0 {
		set => max(0.0, $value);
	}

	public bool $visible {
		get => $this->radius > 0;
	}
}

Méthodes d'extension

Nette\Object a apporté à PHP un autre concept intéressant inspiré des langages de programmation modernes – les méthodes d'extension. Cette fonction, reprise de C#, a permis aux développeurs d'étendre élégamment les classes existantes avec de nouvelles méthodes sans nécessité de les modifier ou d'en hériter. Par exemple, vous pouviez ajouter au formulaire une méthode addDateTime() qui ajoute un DateTimePicker personnalisé :

Form::extensionMethod(
	'addDateTime',
	fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);

$form = new Form;
$form->addDateTime('date');

Les méthodes d'extension se sont avérées peu pratiques, car leurs noms n'étaient pas suggérés par les éditeurs, au contraire, ils signalaient que la méthode n'existait pas. C'est pourquoi leur support a été arrêté. Aujourd'hui, il est plus courant d'utiliser la composition ou l'héritage pour étendre la fonctionnalité des classes.

Obtention du nom de la classe

Pour obtenir le nom de la classe, SmartObject offrait une méthode simple :

$class = $obj->getClass(); // en utilisant Nette\Object
$class = $obj::class;      // depuis PHP 8.0

Accès à la réflexion et aux annotations

Nette\Object offrait l'accès à la réflexion et aux annotations en utilisant les méthodes getReflection() et getAnnotation(). Cette approche a simplifié significativement le travail avec les méta-informations des classes :

/**
 * @author John Doe
 */
class Foo extends Nette\Object
{
}

$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // retourne 'John Doe'

Depuis PHP 8.0, il est possible d'accéder aux méta-informations sous forme d'attributs, qui offrent encore plus de possibilités et un meilleur contrôle de type :

#[Author('John Doe')]
class Foo
{
}

$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];

Getters de méthode

Nette\Object offrait une manière élégante de passer des méthodes comme s'il s'agissait de variables :

class Foo extends Nette\Object
{
	public function adder($a, $b)
	{
		return $a + $b;
	}
}

$obj = new Foo;
$method = $obj->adder;
echo $method(2, 3); // 5

Depuis PHP 8.1, il est possible d'utiliser la dite syntaxe callable de première classe, qui pousse ce concept encore plus loin :

$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5

Événements

SmartObject offre une syntaxe simplifiée pour travailler avec les événements. Les événements permettent aux objets d'informer les autres parties de l'application des changements de leur état :

class Circle extends Nette\Object
{
	public array $onChange = [];

	public function setRadius(float $radius): void
	{
		$this->onChange($this, $radius);
		$this->radius = $radius;
	}
}

Le code $this->onChange($this, $radius) est équivalent à la boucle suivante :

foreach ($this->onChange as $callback) {
	$callback($this, $radius);
}

Pour des raisons de clarté, nous recommandons d'éviter la méthode magique $this->onChange(). Un remplacement pratique est par exemple la fonction Nette\Utils\Arrays::invoke :

Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);
version: 4.0