Vamos a explicar un sencillo mecanismo que nos va a permitir crear objetos con el API que nosotros decidamos en tiempo de ejecución. Es decir, se les va a poder añadir tanto atributos como funciones a los objetos de tipo “Objeto Anónimo” de forma dinámica.
Lo primero que necesitamos es definir una clase (nosotros la hemos llamado AnonymousObject) que contenga un par de atributos contenedores. Decimos que son atributos contenedores puesto que en esos atributos se almacenarán los atributos y las funciones establecidos en tiempo de ejecución.
El primer atributo es $dynamicAttributes que es un array en el que se irán guardando pares clave-valor donde la clave es el nombre del atributo dinámico y el valor es el valor que le hemos asignado.
El segundo atributo es $closures, que, de forma similar a como hace $dynamicAttributes almacena una tabla hash con las clausuras que hemos definido.
En este diagrama se puede ver lo que intentamos conseguir, que un objeto pueda tener herencia de interfaces y de implementación y la almacene de forma dinámica en su interior (en este caso lo hacemos mediante los dos atributos anteriormente descritos) y ciertas características propias del lenguaje en el que vamos a implementar esta solución.
En código PHP, tendríamos la siguiente estructura:
/** Clase anónima */
class AnonymousObject
{
/** Nombre de la clase anónima */
const CLASS_NAME = "AnonymousObject";
/** Array para establecer propiedades dinámicas */
protected $dynamicAttributes;
/** Array para acceder a las Closures */
protected $closures;
//...
}
Si estuviéramos ante un lenguaje tradicional tendríamos que construirnos métodos __set y __get para establecer los atributos y clausuras, pero como estamos usando PHP que es un lenguaje dinámico, las clases tienen métodos especiales (llamados por los desarrolladores “métodos mágicos”) que se ejecutan bajo determinadas condiciones.
El primero de estos métodos mágicos es __isset, que se ejecuta cuando un se llama al método isset de un atributo de una clase que no existe. Podremos implementar ahí nuestra llamada isset al array de atributos dinámicos.
Después tenemos la pareja de métodos mágicos __set y __get que se llaman cuando, respectivamente, se realiza una asignación a un atributo que no existe y se accede a un atributo que no existe. El método __set permitirá asignar funciones, en la implementación de éste se comprueba si lo que se está asignando es una clausura y, en ese caso, se asigna al almacén de funciones.
También existe el método __unset, de manera que podemos destruir atributos asignados de forma dinámica.
Dicho esto, tendremos que implementar estos métodos mágicos, de manera que el exterior de esta clase sea “engañado” y piense que realmente los atributos dinámico son atributos de objeto y las clausuras son métodos. Se muestra a continuación una posible implementación de éstos:
/**
* Método mágico: comprueba si está asignado el atributo en tiempo de ejecución.
* @param string $name Nombre del atributo.
* @return boolean true si está asignado como atributo dinámico, false en otro caso (comportamiento de isset).
*/
public function __isset($name){
return (isset($this->dynamicAttributes[$name]) or isset($this->closure[$name]));
}
/**
* Método mágico: devuelve un atributo asignado en tiempo de ejecución.
* @param string $name Nombre del atributo.
* @return mixed Valor del atributo.
*/
public function __get($name){
if(isset($this->dynamicAttributes[$name]))
return $this->dynamicAttributes[$name];
if(isset($this->closure[$name]))
return $this->closure[$name];
return null;
}
/**
* Método mágico: asigna un atributo dinámico en tiempo de ejecución.
* @param string $name Nombre del atributo.
* @param string $value Valor del atributo.
*/
public function __set($name, $value)
{
if($value instanceof Closure)
$this->closures[$name] = $value;
else
$this->dynamicAttributes[$name] = $value;
}
/**
* Método mágico: ejecuta unset sobre un atributo asignado en tiempo de ejecución.
* @param $name Nombre del atributo.
*/
public function __unset($name){
if(isset($this->dynamicAttributes[$name]))
unset($this->dynamicAttributes[$name]);
if(isset($this->closure[$name]))
unset($this->closure[$name]);
}
Para terminar con los método mágicos vamos a mostrar la función mágica __call, que se llama cuando no se llama a un método que no existe de forma nativa en el objeto. No hay mucha ciencia en lo que vamos a hacer, vamos a comprobar que existe el nombre de la función llamada en nuestro almacén de funciones y en el caso de que exista, se podría lanzar una excepción o un código de error según la metodología de desarrollo del programador.
/**
* La clase anónima no tiene más que un método para añadir métodos desde clausuras.
* */
public function __call($name, $arguments)
{
if (isset($this->closures[$name]) && $this->$name instanceof Closure)
{
return call_user_func_array($this->$name, $arguments);
}
// Si no existe, podríamos lanzar una excepción
// ...
}
Por último, cabe mencionar que es útil tener métodos para obtener todos los atributos que tiene asignados actualmente el objeto, de ahí que se implemente este getAsArray.
/**
* Devuelve los valores del objeto anónimo como array.
* @return array Array con los valores del objeto.
* */
public function getAsArray()
{
$values = array();
// Si los atributos dinámicos no existen, devolvemos un array vacío
if(!is_array($this->dynamicAttributes) or count($this->dynamicAttributes)==0)
return $values;
// Para cada atributo dinámico, lo añadimos en el array
foreach($this->dynamicAttributes as $key=>$value)
{
$values[$key] = $value;
}
return $values;
}
¿Nos faltaría algo? Lo dejo a la elección del lector que espero que comente y critique esta solución.
Diego J. Romero
Lo primero que necesitamos es definir una clase (nosotros la hemos llamado AnonymousObject) que contenga un par de atributos contenedores. Decimos que son atributos contenedores puesto que en esos atributos se almacenarán los atributos y las funciones establecidos en tiempo de ejecución.
El primer atributo es $dynamicAttributes que es un array en el que se irán guardando pares clave-valor donde la clave es el nombre del atributo dinámico y el valor es el valor que le hemos asignado.
El segundo atributo es $closures, que, de forma similar a como hace $dynamicAttributes almacena una tabla hash con las clausuras que hemos definido.
En este diagrama se puede ver lo que intentamos conseguir, que un objeto pueda tener herencia de interfaces y de implementación y la almacene de forma dinámica en su interior (en este caso lo hacemos mediante los dos atributos anteriormente descritos) y ciertas características propias del lenguaje en el que vamos a implementar esta solución.
En código PHP, tendríamos la siguiente estructura:
/** Clase anónima */
class AnonymousObject
{
/** Nombre de la clase anónima */
const CLASS_NAME = "AnonymousObject";
/** Array para establecer propiedades dinámicas */
protected $dynamicAttributes;
/** Array para acceder a las Closures */
protected $closures;
//...
}
Si estuviéramos ante un lenguaje tradicional tendríamos que construirnos métodos __set y __get para establecer los atributos y clausuras, pero como estamos usando PHP que es un lenguaje dinámico, las clases tienen métodos especiales (llamados por los desarrolladores “métodos mágicos”) que se ejecutan bajo determinadas condiciones.
El primero de estos métodos mágicos es __isset, que se ejecuta cuando un se llama al método isset de un atributo de una clase que no existe. Podremos implementar ahí nuestra llamada isset al array de atributos dinámicos.
Después tenemos la pareja de métodos mágicos __set y __get que se llaman cuando, respectivamente, se realiza una asignación a un atributo que no existe y se accede a un atributo que no existe. El método __set permitirá asignar funciones, en la implementación de éste se comprueba si lo que se está asignando es una clausura y, en ese caso, se asigna al almacén de funciones.
También existe el método __unset, de manera que podemos destruir atributos asignados de forma dinámica.
Dicho esto, tendremos que implementar estos métodos mágicos, de manera que el exterior de esta clase sea “engañado” y piense que realmente los atributos dinámico son atributos de objeto y las clausuras son métodos. Se muestra a continuación una posible implementación de éstos:
/**
* Método mágico: comprueba si está asignado el atributo en tiempo de ejecución.
* @param string $name Nombre del atributo.
* @return boolean true si está asignado como atributo dinámico, false en otro caso (comportamiento de isset).
*/
public function __isset($name){
return (isset($this->dynamicAttributes[$name]) or isset($this->closure[$name]));
}
/**
* Método mágico: devuelve un atributo asignado en tiempo de ejecución.
* @param string $name Nombre del atributo.
* @return mixed Valor del atributo.
*/
public function __get($name){
if(isset($this->dynamicAttributes[$name]))
return $this->dynamicAttributes[$name];
if(isset($this->closure[$name]))
return $this->closure[$name];
return null;
}
/**
* Método mágico: asigna un atributo dinámico en tiempo de ejecución.
* @param string $name Nombre del atributo.
* @param string $value Valor del atributo.
*/
public function __set($name, $value)
{
if($value instanceof Closure)
$this->closures[$name] = $value;
else
$this->dynamicAttributes[$name] = $value;
}
/**
* Método mágico: ejecuta unset sobre un atributo asignado en tiempo de ejecución.
* @param $name Nombre del atributo.
*/
public function __unset($name){
if(isset($this->dynamicAttributes[$name]))
unset($this->dynamicAttributes[$name]);
if(isset($this->closure[$name]))
unset($this->closure[$name]);
}
Para terminar con los método mágicos vamos a mostrar la función mágica __call, que se llama cuando no se llama a un método que no existe de forma nativa en el objeto. No hay mucha ciencia en lo que vamos a hacer, vamos a comprobar que existe el nombre de la función llamada en nuestro almacén de funciones y en el caso de que exista, se podría lanzar una excepción o un código de error según la metodología de desarrollo del programador.
/**
* La clase anónima no tiene más que un método para añadir métodos desde clausuras.
* */
public function __call($name, $arguments)
{
if (isset($this->closures[$name]) && $this->$name instanceof Closure)
{
return call_user_func_array($this->$name, $arguments);
}
// Si no existe, podríamos lanzar una excepción
// ...
}
Por último, cabe mencionar que es útil tener métodos para obtener todos los atributos que tiene asignados actualmente el objeto, de ahí que se implemente este getAsArray.
/**
* Devuelve los valores del objeto anónimo como array.
* @return array Array con los valores del objeto.
* */
public function getAsArray()
{
$values = array();
// Si los atributos dinámicos no existen, devolvemos un array vacío
if(!is_array($this->dynamicAttributes) or count($this->dynamicAttributes)==0)
return $values;
// Para cada atributo dinámico, lo añadimos en el array
foreach($this->dynamicAttributes as $key=>$value)
{
$values[$key] = $value;
}
return $values;
}
¿Nos faltaría algo? Lo dejo a la elección del lector que espero que comente y critique esta solución.
Diego J. Romero
Hola Diego, muy bueno el artículo.
ResponderEliminarSupongo que habréis visto las novedades de PHP 5.4, os dejo el enlace porque quizás para lograr lo mismo, os pueda servir una de sus novedades.
http://php.net/manual/es/language.oop5.traits.php
Saludos.
Francisco J.