lunes, 12 de agosto de 2013

PHP orientado a objetos - Métodos mágicos - Parte 1

En PHP las clases tiene reservadas ciertos nombres de métodos que llevan como prefijo distintivo una doble barra baja. El programador será el encargado de escribir el comportamiento de estos métodos. Pero la caracteristica más importante es que estos métodos nunca serán llamados directamente por el programador. PHP, si están definidos, se encarga de llamarlos en el momento adecuado. Y es por eso que reciben el nombre de método mágicos.
Dos de estos métodos mágicos, __construct y __destruct(), ya los vimos en un tutorial anterior. Por lo tanto vamos a centrarnos en otros métodos nuevos.

__clone()

En PHP 4 se podía copiar un objeto simplemente asignándolo de una variable a otra. Y de esta forma se podía modificar la copia sin que ello afectara al original.

 $objecto1->nombre = "nombre 1";  
 $objecto2 = $objecto1;  
 $objecto2->nombre = "nombre 2";  
 echo $objecto1->nombre; // nombre 1"  

Pero en PHP 5  los objetos son asignados por referencia. Lo que significa que si en dos variables asignamos el mismo objeto lo que obtendremos será dos referencias al mismo objeto. Por lo que necesitamos algo más si queremos realizar una copia exacta pero no enlazada de un objeto. Y para ello existe la palabra reservada clone.

 $objecto1->nombre = "nombre 1";  
 $objecto2 = clone $objecto1;  
 $objecto2->nombre = "nombre 2";  
 echo $objecto1->nombre; // nombre 1"  

Y  una vez el clonado se haya realizado, se llamará automáticamente al método __clone() del nuevo objeto creado. Siempre y cuando __clone() este definido. Recuerda que los métodos mágicos son invocados automáticamente por PHP si estos están definidos. Y de esta forma, dentro de __clone() se pueden realizar, por ejemplo, cambios sobre los atributos del nuevo objeto creado. Por si queremos alguna variación respecto el objeto clonado.

 class Objeto {  
   private $id;  
   private $nombre;  
   private $email;  
   function __construct($id,$nombre, $email) {  
     $this->id = $id;  
     $this->nombre = $nombre;  
     $this->email = $email;  
   }  
   function __clone() {  
     $this->id = ++$this->id;   
   }  
   public function getId() {  
     return $this->id;  
   }  
   public function setId($id) {  
     $this->id = $id;  
   }  
   public function getNombre() {  
     return $this->nombre;  
   }  
   public function setNombre($nombre) {  
     $this->nombre = $nombre;  
   }  
   public function getEmail() {  
     return $this->email;  
   }  
   public function setEmail($email) {  
     $this->email = $email;  
   }  
 }  
 $obj = new Objeto(1, "objeto1", "prueba1@ejemplo.com");  
 $p = clone $obj;  
 echo $p->getId();  //2

Como se puede ver en el ejemplo, cuando clonemos un objeto incrementamos el identificador en 1.

__set y __get

Como hemos comentado en el tutorial de introducción de programación orientada a objetos una práctica en programación es proteger los atributos de una clase con una visibilidad restrictiva. Y acceder a dichos atributos mediante métodos 'get' y 'set'. Pero declarar por cada atributo un método de acceso (get) y otro de modificación (set) puede ser muy tedioso si el número de atributos es elevado. Una solución está en la utilización de un IDE como Netbeans que automáticamente crea los 'getters/setters' básicos (ctrl + insert). Aunque vamos a ver como PHP5 tiene unos métodos mágicos que se encargan de ayudarnos a no tener que declarar un método 'get' y otro 'set' por cada atributo. Con los métodos mágicos __set y __get podemos implementar un funcionamiento para poder modificar o acceder a los distintos atributos de la clase. Evitando tener que crear un método para cada uno de ellos.
Una vez declarados estos métodos, si se intenta acceder a un atributo como si este fuera público, PHP llamara automáticamente a __get(). Y si asignamos un valor a un atributo, llamará a  __set().

Evidentemente, __set() necesita dos parámetros de entrada: nombre del atributo y el valor a asignar. Y __get() solo necesita el nombre del atributo a obtener su valor.

 class Objeto {  
   private $id;  
   private $nombre;  
   private $email;  
   function __construct($id, $nombre, $email) {  
     $this->id = $id;  
     $this->nombre = $nombre;  
     $this->email = $email;  
   }  
   function __clone() {  
     $this->id = ++$this->id;  
   }  
   public function __set($var, $valor) {  
     if (property_exists(__CLASS__, $var)) {  
       $this->$var = $valor;  
     } else {  
       echo "No existe el atributo $var.";  
     }  
   }  
   public function __get($var) {  
     if (property_exists(__CLASS__, $var)) {  
       return $this->$var;  
     }  
     return NULL;  
   }  
 }  
 $obj = new Objeto(1, "objeto1", "prueba1@ejemplo.com");  
 $p = clone $obj;  
 echo $p->id;//2  
 $p->nombre = "nombre cambiado";  
 echo $p->nombre;//nombre cambiado  

Hay que tener en cuenta que estos método solo se llamarán desde fuera del objeto. Dentro utilizaremos la referencia al objeto $this.

__toString

Este método es quizás uno de los métodos mágicos menos importantes. Permite asignar una cadena (string) al objeto que será mostrada si el objeto es usado como una cadena. Osea el valor que mostrará si se intenta hacer echo $objeto.

 class Objeto {  
   private $id;  
   private $nombre;  
   private $email;  
   //...  
   public function __toString(){  
     return "esto es una prueba";  
   }  
 }  
 $obj = new Objeto(1, "objeto1", "prueba1@ejemplo.com");
 $p = clone $obj;
 echo $p;  //esto es una prueba

Si no se hubiera definido __toString, al intentar imprimir $p daría un error indicando que no se puede convertir el objeto a un string. Además dentro de este método no se pueden utilizar excepciones ya que también ocasionarían un error.

En PHP 5.2  y versiones superiores, el método __toString será invocado automáticamente cuando se utilice las funciones de impresión echo(), print() y printf(). Esta última únicamente cuando se utiliza el modificador %s.

_call

Este método mágico es llamado automáticamente cuando se intenta llamar a un método que no esta definido en la clase o es inaccesible dentro del objeto (por ejemplo un método privado).
Este método recibe dos parámetros, uno es el nombre del método al que se intenta invocar, y el otro los parámetros que le hemos pasado al método al que hemos intentado llamar.

 class Objeto {  
   private $id;  
   private $nombre;  
   private $email;  
   //...  
   public function __toString(){  
     return "esto es una prueba";  
   }  
   public function __call($metodo, $args) {  
     $args = implode(', ', $args);  
     echo "fallo al llamar al método $metodo() con los argumentos $args";  
   }  
 }  
 $obj = new Objeto(1, "objeto1", "prueba1@ejemplo.com");  
 $p = clone $obj;  
 echo $p;  
 $p->noExiste();  

Visto este ejemplo nos puede parecer que este método solo sirve para mostrar un mensaje evitando el error que se debería mostrar por intentar acceder a algo que no existe. Pero también se puede utilizar para crear 'setter' y 'getters' dinámicos sin tener que utilizar los métodos mágicos __set() y __get(). Ya que es posible que la utilización de estos últimos no nos guste (los atributos se acceden como si fueran públicos para que se invoquen los métodos __get() y __set()).

Vamos a ver un ejemplo.

 class Objeto {  
   private $id;  
   private $nombre;  
   private $email;  
   function __construct($id, $nombre, $email) {  
     $this->id = $id;  
     $this->nombre = $nombre;  
     $this->email = $email;  
   }  
   function __clone() {  
     $this->id = ++$this->id;  
   }  
   public function __toString() {  
     return "esto es una prueba";  
   }  
   public function __call($metodo, $params = null) {  
     // todo en minúsculas para evitar problemas. Por ejemplo setNombre  
     $metodo = strtolower($metodo);  
     $prefijo = substr($metodo, 0, 3);  
     $atributo = substr($metodo, 3);  
     if ($prefijo == 'set' && count($params) == 1) {  
       if (property_exists(__CLASS__, $atributo)) {  
         $valor = $params[0];  
         $this->$atributo = $valor;  
       } else {  
         echo "No existe el atributo $atributo.";  
       }  
     } elseif ($prefijo == 'get') {  
       if (property_exists(__CLASS__, $atributo)) {  
         return $this->$atributo;  
       }  
       return NULL;  
     } else {  
       echo 'Método no definido <br/>';  
     }  
   }  
 }  
 $obj = new Objeto(1, "objeto1", "prueba1@ejemplo.com");  
 $p = clone $obj;  
 $p->noExiste(); //Método no definido
 $p->setId(2);  
 echo $p->getId() . "<br/>"; //2  

Vamos a ampliar un poco el ejemplo haciendo uso de la herencia. Esta vez se utiliza la función get_class() que nos devuelve el nombre de la clase del objeto que se le pase como parámetro.

 string get_class ([ object $object = NULL ] )  

Estando dentro de la clase, si se omite el parámetro, devolverá el nombre de la clase. Pero si dicha clase hereda de otra, tendremos que usar $this como parámetro para obtener el nombre de la clase actual y no el de la padre.

 class Objeto {  
   //...
   public function __call($metodo, $params = null) {  
     // todo en minúsculas para evitar problemas. P.E. setNombre  
     $metodo = strtolower($metodo);  
     $prefijo = substr($metodo, 0, 3);  
     $atributo = substr($metodo, 3);  
     $nombreClase = get_class($this);  
     if ($prefijo == 'set' && count($params) == 1) {  
       if (property_exists($nombreClase, $atributo)) {  
         $valor = $params[0];  
         $this->$atributo = $valor;  
       } else {  
         echo "No existe el atributo $atributo.";  
       }  
     } elseif ($prefijo == 'get') {  
       if (property_exists($nombreClase, $atributo)) {  
         return $this->$atributo;  
       }  
       return NULL;  
     } else {  
       echo 'Método no definido <br/>';  
     }  
   }  
 }  
 class ClaseHijo extends Objeto{  
   protected $apellido = "apellido";  
 }  
 $obj = new ClaseHijo(1, "objeto1", "prueba1@ejemplo.com");  
 $p = clone $obj;  
 $p->noExiste();//Método no definido
 $p->setId(2);  
 echo $p->getId() . "<br/>";//2  
 echo $p->getApellido();//apellido  

__callStatic

El método anterior __call era lanzado al intentar llamar a un método inaccesible en el contexto del objeto. Pero no está pensado si el método es inaccesible en un contexto estático. Osea que si llamamos a un método estático que no existe, no se llamará automáticamente a __call aunque esté definido (__call).
En PHP 5.3 aparece un nuevo método mágico llamado __callStatic que soluciona este problema.

 class Objeto {  
   protected $id;  
   protected $nombre;  
   protected $email;  
   function __construct($id, $nombre, $email) {  
     $this->id = $id;  
     $this->nombre = $nombre;  
     $this->email = $email;  
   }  
   function __clone() {  
     $this->id = ++$this->id;  
   }  
   function who() {  
     return __CLASS__;  
   }  
   public function __toString() {  
     return "esto es una prueba";  
   }  
   public static function __callStatic($metodo, $args) {  
     echo "Método $metodo ha sido llamado!!!<br/>";  
     echo "Con los siguientes argumentos: " . implode(", ", $args) . "<br/>";  
   }  
   public function __call($metodo, $params = null) {  
     // todo en minúsculas para evitar problemas. P.E. setNombre  
     $metodo = strtolower($metodo);  
     $prefijo = substr($metodo, 0, 3);  
     $atributo = substr($metodo, 3);  
     $nombreClase = get_class($this);  
     if ($prefijo == 'set' && count($params) == 1) {  
       if (property_exists($nombreClase, $atributo)) {  
         $valor = $params[0];  
         $this->$atributo = $valor;  
       } else {  
         echo "No existe el atributo $atributo.";  
       }  
     } elseif ($prefijo == 'get') {  
       if (property_exists($nombreClase, $atributo)) {  
         return $this->$atributo;  
       }  
       return NULL;  
     } else {  
       echo 'Método no definido <br/>';  
     }  
   }  
 }  
 class ClaseHijo extends Objeto {  
   protected $apellido = "apellido";  
 }  
 $obj = new ClaseHijo(1, "objeto1", "prueba1@ejemplo.com");  
 $p = clone $obj;  
 $p->noExiste();  
 $p->setId(2);  
 echo $p->getId() . "<br/>"; //2  
 echo $p->getApellido(); //apellido  
 Objeto::algunMetodo('con', 'varios', 'argumentos');  


Entradas relacionadas

PHP orientado a objetos - Introducción

No hay comentarios:

Publicar un comentario