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()
ouisXyz()
, un setter avec le nomsetXyz()
- 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);