Méthodes magiques : __set() et __get()

Rechercher

Méthodes magiques : __set() et __get()

  • Par Palleas
  • 12 commentaires
  • 40921 lectures
  • RSS -  Atom

PHP a fait un grand pas en avant en matière de programmation orientée objet avec sa version 5. Depuis cette version, il permet d'implémenter des méthodes au comportement prédéfini par PHP. Ces méthodes sont appelées « méthodes magiques », les méthodes __set() et __get() en font partie.

Rappels concernant la visibilité des propriétés et des méthodes

Accès public des propriétés

La programmation orientée objet permet de spécifier la visibilité des méthodes et propriétés d'une classe. Dans le cas des propriétés, il est très peu recommandable de leur donner une visiblité publique car de cette manière il devient possible de les modifier sans qu'aucun contrôle ne soit effectué sur les valeurs.

<?php
class Kid {
/**
* Age du kid
*
* @var int
* @access public
*/
public $age;
/**
* Retourne l'âge du Kid sous forme d'une chaîne
*
* @param void
* @return string
*/
public function showAge() {
return 'Son âge est : '. $this->age;
}
}
$billy = new Kid();
$billy->age = 'rouge';
echo $billy->showAge();
?>

Cette portion de code vous retournera donc « Son âge est rouge » ce qui, vous en conviendrez, ne veut strictement rien dire.

Accès privé et protégé des propriétés

Il est donc préférable de leur accorder une visiblité limitée : private ou protected.

<?php
class Kid {
/**
* Age du kid
*
* @var int
* @access private
*/
private $age;
}
$billy = new Kid();
$billy->age = "encore plus rouge qu'avant";
?>

Ce bout de code vous retournera une erreur fatale :

Fatal error: Cannot access private property Kid::$age in /path/to/Apprendre-php/magic_methods.php on line 6.

Une solution serait de créer un accesseur public setAge() qui permettrait de spécifier l'age en s'assurant que vous lui avez bien passé un chiffre, et getAge() pour pouvoir l'afficher.

Cette méthode est tout à fait envisageable, à condition de ne pas avoir un grand nombre de propriétés, le nombre d'accesseurs devenant bien trop important : vous pouvez utiliser implicitement les méthodes magiques __set() et __get().

De plus, la syntaxe suivante est tout à fait valable avec PHP :

<?php
class Kid {
}
$billy = new Kid();
$billy->age = 14;
$billy->cheveux = 'noirs';
// etc...
echo 'Billy est agé de ', $billy->age,' ans et ses cheveux sont de couleur ', $billy->cheveux;
?>

Et oui, vous pouvez renseigner et ensuite récuperer des propriétés à un objet php, sans que celles-ci aient été déclarées dans votre classe, plus sale non ? Il est donc possible de « boucher » cette petite bévue, toujours en utilisant implicitement les méthodes magiques __set() et __get() qui seront respectivement appelées lorsque l'on renseigne la propriété et lorsqu'on essaye d'en lire la valeur.

La méthode magique __set()

La méthode magique __set() permet de faire ce que l'on appelle de la surchage de propriétés d'une classe. En effet, lorsque l'on essaie de fixer la valeur d'une propriété inexistante de la classe, PHP appelle automatiquement cette méthode de manière implicite. Voici sa structure générale.

<?php
class MyObject
{
/**
* Methode magique __set()
*
* @param string $property Nom de la propriété
* @param mixed $value Valeur à affecter à la propriété
* @return void
*/
public function __set($property, $value)
{
// Code personnalisé à exécuter
}
}
?>

En redéfinissant explicitement cette méthode dans le corps de la classe, cela permet au développeur de réaliser des contrôles d'accès et de s'assurer que seules quelques propriétés peuvent être mises à jour. C'est ce que nous introduirons dans notre cas d'application plus loin dans ce cours.

La méthode magique __get()

La méthode magique __get() permet, quant à elle, de lire la valeur d'une propriété inexistante de la classe. Au même titre que la méthode magique __set(), la méthode magique __get() doit être redéfinie dans la classe pour exécuter du code personnalisé lorsque PHP appelle implicitement cette méthode. Là encore, cela permet de réaliser un contrôle d'accès sur les propriétés dont on essaie de lire les valeurs. Le prototype de cette méthode est présenté ci-dessous et sera développé davantage dans le cas d'application pratique de ce tutoriel.

<?php
class MyObject
{
/**
* Methode magique __get()
*
* @param string $property Nom de la propriété à atteindre
* @return mixed|null
*/
public function __get($property)
{
// Code personnalisé à exécuter
}
}
?>

Cas d'application pratique

Nous allons reprendre notre exemple précédent de notre classe Kid et nous allons lui intégrer ces deux méthodes magiques _get() et __set(). L'objectif est ainsi de réaliser un contrôle d'accès lorsque l'on essaie de fixer ou de lire la valeur de la propriété privée $age.

<?php
class Kid {
/**
* Age du kid
*
* @var int
* @access private
*/
private $age;
/**
* Methode magique __get()
*
* Retourne la valeur de la propriété appelée
*
* @param string $property
* @return int $age
* @throws Exception
*/
public function __get($property) {
if('age' === $property) {
return $this->age;
} else {
throw new Exception('Propriété invalide !');
}
}
/**
* Methode magique __set()
*
* Fixe la valeur de la propriété appelée
*
* @param string $property
* @param mixed $value
* @return void
* @throws Exception
*/
public function __set($property,$value) {
if('age' === $property && ctype_digit($value)) {
$this->age = (int) $value;
} else {
throw new Exception('Propriété ou valeur invalide !');
}
}
}
?>

De cette manière, on s'assure de ne pouvoir spécifier que l'âge de Billy, et rien de plus. Vous remarquerez que l'on effectue des tests « en dur » c'est à dire que l'on ne vérifie pas simplement que la propriété $age existe, mais seulement que l'utilisateur a cherché à spécifier « $age ». De plus, on contrôle le type de la valeur au moyen de la fonction ctype_digit() qui s'assure que le format de la valeur correspond bien à un nombre entier.

Il est tout à fait possible de récupérer les propriétés d'un objet, mais pour faire cela proprement, il est préférable d'utiliser les classes d'introspection (« reflection » en anglais), un tutoriel est actuellement en cours d'écriture concernant cet sujet de POO.

Inconvénients des méthodes magiques __get() et __set()

Bien que ces deux méthodes magiques soient très pratiques à utiliser, elles posent tout de même deux désagréments non négligeables lorsque l'on développe en environnemet professionnel. En effet, l'utilisation de __get() et __set() empêche tout d'abord la génération automatique de documentation de code au moyen des APIs (PHPDocumentor par exemple) utilisant les objets d'introspection (Reflection). D'autre part, cela empêche également les IDE tels qu'Eclipse d'introspecter le code de la classe et ainsi proposer l'auto-complétion du code.



Les commentaires

1. Par John le 09/11/2008 15:28

Salut Palleas. Merci tout d'abord pour ce tuto.

Je suis débutant en POO PHP. J'essaye donc de comprendre la base.

Petite interrogation.

Si je comprends bien, ces deux méthodes permettent de spécifier et d'accéder à des paramètres non repris lors de la création initial de la classe ??

2. Par Emacs le 09/11/2008 19:56

@John : oui et non ! __set() et __get() sont respectivement des méthodes magiques permettant de fixer un attribut existant (ou non) de ta classe et de le récupérer en l'appelant par son nom. Elles te permettent notamment d'éviter d'écrire un setter() et un getter() explicite pour chaque attribut de ta classe.

3. Par saturn1 le 27/11/2008 08:10

Oui mais alors il faut l'utiliser ou pas car à la fin tu déconseilles...

4. Par Emacs le 27/11/2008 13:38

@saturn1 : oui je les déconseille pour plusieurs raisons.

1. Les IDEs et les éditeurs de texte comme VIM, Eclipse, Netbeans... ont besoin des noms de méthodes explicitement définis dans les corps de classe pour assurer l'auto complétion du code. Avec les méthodes magiques, on perd cette fonctionnalité...

2. Ca t'oblige à faire tous les tests de set / get dans une seule et même méthode. Quand tu dois contrôler 3 paramètres ça reste jouable mais lorsque tu as plein d'attributs c'est impensable. Tu te retrouves avec une méthode __set() de plusieurs dizaines, voire centaine de lignes de code. C'est vite inmaintenable.

3. Le debug est moins aisé forcément quand une erreur est généré dans __set() car il faut te replonger dans une méthode de 50 km de long pour localiser l'erreur et la débugger. Avec une bonne vieille méthode setTruc() tu sais où est l'erreur et tu la corriges illico.

4. Les méthodes magiques sont moins performantes que les méthodes explicites.

Le seul avantage à les utiliser c'est pour coder vite sans se prendre la tête, mais là c'est mal...

5. Par saturn1 le 14/12/2008 13:17

merci!

6. Par Zwozer le 08/03/2009 12:48

Merci pour ces tutos, je les trouve très bien fait !

Il y a juste un point qui n'est pas bien clair à mes yeux :

Quand on a plein d'attributs, vaut il mieux utiliser une abondance de setters et de getters, ou avoir un __set() et un __get() surchargés de conditions ?

Car en fait au début du tuto il est écrit, à propos des setters et getters : "Cette méthode est tout à fait envisageable, à condition de ne pas avoir un grand nombre de propriétés, le nombre d'accesseurs devenant bien trop important"

Et à la fin, à propos des méthodes magiques __get() et __set() : "lorsque tu as plein d'attributs c'est impensable."

7. Par Joris le 16/04/2009 13:02

Bonne question. A mon avis, il faudrait essayer de faire des classes héritées autant que possible, non ?

8. Par Emacs le 17/04/2009 09:39

@Zwozer : l'idéal c'est de ne pas les utiliser et préférer écrire à la main les getter et setter. Pourquoi ? Il y a deux raisons évidentes à cela. La première c'est pour éviter d'avoir deux méthodes qui contiennent chacune trop de lignes de code. En effet, si tu as des setters avec des validations complexes à l'intérieur, tu risques d'avoir au final une longue méthode __set() si tu as beaucoup de propriétés pour lesquelles il faut valider la valeur à setter.

La seconde raison et non des moindres, c'est pour avoir l'autocomplétion du code dans un IDE tels que Eclipse, Netbeans, Zend Studio... Quand tu utilises les méthodes magiques, forcément tu n'as pas l'autocomplétion vu que les vrais accesseurs et mutateurs ne sont pas explicitement déclarés dans ta classe. Au final, c'est assez embêtant pour coder ou bien pour documenter le code.

En fin de compte, je m'efforce d'écrire mes propres getters et setters et de limiter les méthodes magiques. Tout du mois, je n'utilise très peu __set() et __get() mais en revanche j'utilise de temps en temps __call() qui est bien pratique dans certains cas.

9. Par Thierry009 le 11/05/2009 16:33

Sympa ce petit cours.

L'exemple est pertinent.

Juste un détail à propos de la fonction ctype_digit, quel est l'intérêt du faire un cast (int) de la valeur une fois que l'on a vérifié que cette même valeur était bien un entier avec ctype_digit (dans la méthode __set) ?

Merci

10. Par Emacs le 11/05/2009 20:14

@Thierry : PHP n'étant pas fortement typé, il est impossible de forcer le passage d'un entier en paramètre d'une fonction / méthode comme c'est le cas en Java par exemple.

Dans le cas présent, on souhaite fixer la valeur d'une variable $age de la classe. Or, dans la méthode __set(), rien ne nous garantit que la valeur transmise est bien un entier. C'est pour cette raison que l'on utilise ctype_digit() qui nous assure que la valeur transmise est bien du même format que celui d'un entier. Par conséquent, ctype_digit() retourne TRUE aussi bien pour une valeur typée entière comme pour une chaîne de caractères ayant le format d'un entier. Donc on est obligé de caster ensuite en entier pour être sûr que la valeur '18' (string) par exemple devienne bien 18 (entier). Enregistrer une chaîne de caractères dans une variable $age n'a pas de sens. Un âge est un nombre entier, c'est pourquoi on décide de forcer en valeur entière la valeur donnée en paramètre. Cela permettra par exemple d'éviter de mauvaises surprises sur des calculs mathématiques impliquant cette valeur.

11. Par Thierry009 le 15/05/2009 11:27

Merci c'est parfaitement clair

12. Par Thierry_009 le 15/05/2009 11:43

Par contre je crois que tu te trompes sur ctype_digit, apparemment il analyse bien le format de la chaine et renvoie true lorsque cette chaine représente un nombre, par contre s'il ne s'agit pas d'une chaine, elle semble renvoyer faux.

Ex avec le code que je viens de tester :

<?php
$x = 3;
$y = '3';

if(ctype_digit($x))
{
echo '$x est un nombre <br />';
}
else
{
echo '$x n\'est pas un nombre <br />';
}

if(ctype_digit($y))
{
echo '$y est un nombre <br />';
}
else
{
echo '$y n\'est pas un nombre <br />';
}
?>

Cela m'affiche :

$x n'est pas un nombre
$y est un nombre