Getters et setters en PHP
Venant du monde de ruby, j’ai pris l’habitude de ne pas faire de distinction dans l’utilisation d’un objet entre une méthode et un attribut. En ruby, un attribut se récupère exactement de la même manière qu’une méthode s’appelle.
Prenons le script suivant :
class Person attr_accessor :food def initialize @food = 10 @firstname = 'John' @lastname = 'Doe' end def eat @food -= 1 true end def name @firstname + ' ' + @lastname end def name=( fullname ) parts = fullname.split( ' ', 2 ) @firstname = parts[0] @lastname = parts[1] end end michel = Person.new michel.name = 'Michel Dupont' michel.food # => 10 michel.eat # => true michel.name # => 'Michel Dupont'
Nous voyons ici qu’il n’est pas possible de distinguer, formellement, la méthode “eat” et la méthode “name” de l’attribut “food”. Que nous importe, finalement, leur implémentation? Lorsque j’utilise l’interface d’un objet, je veux soit exécuter une action, soit manipuler une valeur. Dans le cas où je souhaite retrouver une valeur, peu m’importe de savoir si cette valeur se trouve directement dans un attribut ou si une méthode est appelée pour me le retourner : cela relève de l’implémentation et non de l’usage. Au niveau de l’usage, ici, je sais que “food” et “name” me retourneront des valeurs, parce que ce sont des noms, et je sais que “eat” exécutera une action, parce que c’est un verbe.
Qu’en est-il en PHP? Le même script s’écrirait :
< ?php class Person { public $food; protected $firstname; protected $lastname; public function __construct() { $this->food = 10; $this->firstname = 'John'; $this->lastname = 'Doe'; } public function eat() { $this->food -= 1; return true; } public function getName() { return $this->firstname . ' ' . $this->lastname; } public function setName( $fullname ) { $parts = preg_split( '/ /', $fullname, 2 ); $this->firstname = $parts[0]; $this->lastname = $parts[1]; return $fullname; } } $michel = new Person(); $michel->setName( 'Michel Dupont' ); $michel->food; // => 10 $michel->eat(); // => true $michel->getName(); // => 'Michel Dupont'
Nous voyons ici que l’implémentation est clairement visible. “food” est un attribut, “eat”, “setName” et “getName” sont des méthodes. L’astuce, chez PHP, est de transformer un nom en verbe par l’emploi des setters et des getters ( setName() et getName() ) pour justifier l’appel d’une méthode. Néanmoins, rien ne justifie le fait que je doive exécuter une action sur une Person pour avoir son nom. Il s’agit d’une valeur et cela ne me regarde pas, à l’usage, de savoir comment ce nom a été retrouvé.
Suggestion d’une implémentation
Dans l’extension framework que j’ai faite pour Typolight, j’ai généralisé l’emploi d’un design différent pour répondre à ce problème. En voici un exemple :
< ?php class Person { public $food; protected $firstname; protected $lastname; protected $_cache = array(); protected $_uncachable = array(); public function __construct() { $this->food = 10; $this->firstname = 'John'; $this->lastname = 'Doe'; } /** * Check if a getter method exists * * @param string the attribute name * @return mixed */ public function __get( $key ) { $firstLetter = substr( $key, 0, 1 ); $rest = substr( $key, 1 ); $getter = 'get' . strtoupper( $firstLetter ) . $rest; if ( method_exists( $this, $getter ) ) { if ( array_key_exists( $key, $this->_cache ) and ! in_array( $key, $this->_uncachable ) ) { return $this->_cache[ $key ]; } $result = $this->$getter(); if ( ! in_array( $key, $this->_uncachable ) ) { $this->_cache[ $key ] = $result; } return $result; } throw new Exception( "Unknown attribute" ); } /** * Check if a setter method exists * * @param string the attribute name * @param string the attribute value * @return mixed */ public function __set( $key, $value ) { $firstLetter = substr( $key, 0, 1 ); $rest = substr( $key, 1 ); $setter = 'set' . strtoupper( $firstLetter ) . $rest; if ( method_exists( $this, $setter ) ) { $this->_cache[ $key ] = $this->$setter( $value ); } else { $getter = 'get' . strtoupper( $firstLetter ) . $rest; if ( method_exists( $this, $getter ) ) { $this->_cache[ $key ] = $value; return true; } } throw new Exception( "Unknown attribute" ); } public function eat() { $this->food -= 1; return true; } public function getName() { return $this->firstname . ' ' . $this->lastname; } public function setName( $fullname ) { $parts = preg_split( '/ /', $fullname, 2 ); $this->firstname = $parts[0]; $this->lastname = $parts[1]; return true; } } // Nous pouvons maintenant faire ceci : $michel = new Person(); $michel->name = 'Michel Dupont'; $michel->food; // => 10 $michel->eat(); // => true $michel->name; // => 'Michel Dupont'
Ainsi, en utilisant les méthodes magiques __set() et __get(), nous pouvons rétablir le respect de la blackbox. Lorsque $michel->name est exécuté, __get() est appelé ( puisqu’il n’existe pas d’attribut $name ) et vérifie s’il existe une méthode getName().
En outre, cela permet d’ajouter un système de cache. La prochaine fois que j’appellerai $michel->name, la valeur précédente sera directement retournée plutôt que de devoir executer la méthode à nouveau. Si je veux éviter ce comportement, il me suffit d’appeler directement la méthode en faisant $michel->getName(), et le nom sera recalculé.
Bien entendu, mettre en place un tel mécanisme n’a pas de sens pour une petite classe isolée. J’ai employé personnellement ce mécanisme pour la couche modèle et la couche controller de mon extension. Cela est particulièrement intéressant pour les modèles, qui doivent se comporter de la manière la plus naturelle possible et qui font régulièrement des requêtes sur la base de donnée. Avec un tel mécanisme vous pouvez, dans le cadre d’un framework MVC, utiliser autant de fois que vous le voulez vos modèles dans vos vues de manière intuitive et sans vous soucier de savoir si cela va exécuter une requête sql ou non.