El nuevo PHP5 se apoya en la llamada Zend Engine 2, la nueva
versión del motor Zend --desarrollado por Zeev Suraski y Andi Gutmans--
que es el corazón de PHP desde la versión 4. Zend 2 supone un auténtico
"cambio de paradigma" en la programación con PHP. Veámoslo.
Command Line Interface
En los ejemplos que presentaré he empleado la versión CLI (Command Line
Interface) de PHP5. Os recomiendo usarla si queréis probar vosotros
mismos las características de PHP5, ya que prescindiréis de problemas de
integración con Apache, etc.
Una rápida manera de conseguir el intérprete CLI es compilar los fuentes
con:
$ ./configure --disable-cgi --enable-cli --without-pear --disable-all
$ make
El ejecutable aparecerá en el subdirectorio php-5.0.0/sapi/cli/, que
podéis copiar a cualquier directorio que queráis usar para las pruebas.
Observaréis que he desactivado todas las extensiones con
--disable-all. Es otra manera de no complicarse la vida y
obtener una versión con la que juguetear con el intérprete PHP5 sin
tener que instalar nada adicional. Pero si quisiérais probar alguna de
las nuevas extensiones, tendréis que compilar el CLI convenientemente.
Nota: a pesar de no estar en un entorno web, el intérprete CLI sigue
requiriendo que se rodee el código PHP con <?php ... ?> o <? ... ?>.
Objetos y Referencias
En PHP4 las variables que nombran los objetos guardan el objeto
completo en sí (sus datos), tal y como hace una variable simple o de
array. Esto significa, por ejemplo, que una asignación de objetos del
tipo $ob2 = $ob1 realiza en realidad una copia completa, bit a bit,
del objeto original en el objeto destino.
Aunque este comportamiento puede parecer deseable a simple vista,
acarreaba una serie de problemas consigo. Por ejemplo, si nuestra
intención era la de tener no una copia, sino una segunda referencia al
mismo objeto, debíamos usar el operador de referencia '&' para
indicarlo, de la siguiente manera: $ob2 = & $ob1;.
Pero donde realmente afecta esta decisión de diseño es en el paso de
parámetros a una función o método, que en PHP4 se realiza por defecto
por valor --al copiarse el argumento actual en el parámetro formal--,
también en el caso de objetos. De esta forma, cualquier cambio dentro de
la función del objeto en cuestión, se hace sobre la copia del argumento
y no sobre el objeto original externo, que ha quedado intacto.
Ello obliga a utilizar el paso de parámetros por referencia en la
mayoría de los objetos pasados a funciones y métodos:
function modifico_un_objeto( & $objeto ) { ... }
El hecho de tener que utilizar explícitamente el operador de
referencia '&' de forma continuada provoca la proliferación de errores
en PHP4 por "descuidos" y olvidos, errores por lo demás difíciles de
detectar y encontrar.
Por este motivo, los diseñadores de PHP5 han realizado un cambio radical
en el tratamiento de las variables objeto: en PHP5 todas las variables
que nombran objetos son en realidad referencias. No hay que usar el
operador '&' ni en las asignaciones, ni en el paso de parámetros que son
objetos, ahorrándose con ello un mónton de potenciales errores.
Los que conozcan un lenguaje de programación como Java, no dejarán de
observar la "inspiración" de dicha decisión de diseño, y las ventajas
que ello comporta al modelo de gestión de la memoria y los objetos. Pero
no todo son ventajas. El uso de referencias también trae consigo una
serie de cuestiones que el codificador debe tener en cuenta, y que antes
no se le planteaban.
Por ejemplo, lo que antes era una sencilla asignación de objetos, ahora
se convierte en una asignación de referencias. En PHP5 la expresión $ob2
= $ob1 no realiza ninguna copia del objeto, sino lo que se denomina un
alias --ambas variables sirven para manipular el mismo objeto--.
Si el programador no lo tiene en cuenta, lo más probable es que termine
llevándose más de una sorpresa.
También hay que tener cuidado con que el paso de parámetros objeto a una
función o método no produzca efectos colaterales indeseados: si
antes en PHP4 un objeto pasado se modificaba dentro del ámbito local de
la función, pero no se deseaba que tal cambio se propagara, ahora en
PHP5 deberemos utilizar una copia temporal de dicho objeto dentro del
cuerpo de la función para que el comportamiento sea idéntico:
function sin_modificaciones( $objeto ) {
$temporal = clonar( $objeto )
manipular( $temporal );
...
}
Todas estas cuestiones problemáticas van a obligar a que PHP5 ofrezca
una serie de mecanismos adicionales que ayuden a evitarlas o
resolverlas, como el constructor de copia. Mecanismos ya presentes en
otros lenguajes orientados a objetos, y que veremos más adelante.
No obstante, el lector no debería llevarse la impresión de que el uso de
referencias es "problemático". Una expresión como la siguiente:
$ob1->getOb2()->getOb3()->metodo();
es perfectamente válida en PHP5 y se comporta como se espera --invocando
metodo(), y posiblemente cambiando el estado del objeto devuelto por
getOb3()--, mientras que la misma semántica en PHP4 requiere de la
utilización de dos asignaciones de variables temporales de referencia:
$ob2 = & $ob1->getOb2();
$ob3 = & $ob2->getOb3();
$ob3->metodo();
algo realmente engorroso a medida que se anidan las llamadas.
Clases
La principal novedad en las clases de PHP5 es la inclusión de
modificadores de control de acceso para implementar la encapsulación
--piedra angular en la programación orientada a objetos de la que
adolecía PHP4--.
PHP5 introduce tres palabras clave (public, private y
protected) que sustituyen a var en la definición de
variables miembro --atributos-- de la clase, y que preceden a la
definición de funciones miembro --métodos--:
class Clase {
private $atributo;
public metodo( $x, $y ) { ... }
}
Los tres modificadores tienen el significado "natural" esperado por
cualquier programador que provenga de otros lenguajes OO:
- public: la variable o función es accesible desde
cualquier ámbito --la misma clase, otra clase o el ámbito global--.
- private: la variable o función sólo es accesible desde
dentro de la clase a la que pertenece --métodos de dicha clase--.
- protected: la variable o función sólo es accesible desde
dentro de la clase a la que pertenece o de cualquiera de sus clases
derivadas.
En PHP4 todas las variables y las funciones son públicas. Por
compatibilidad, PHP5 considera públicas todas las funciones a las que
explícitamente no se les proporcione un modificador de acceso de los
tres anteriores. PHP5 también admite por compatibilidad el uso de
var como sinónimo de public, pero su uso está
desaconsejado (se considera 'deprecated').
El control de acceso es un mecanismo necesario para poder limitar el
estado que puede tener un objeto, representado por el valor de su
variables. Lo habitual es declarar éstas privadas y acceder a las mismas
mediante métodos proporcionados a tal efecto: los getters o
accesores y los setters o mutadores:
<?php
class Clase {
private $propiedad;
function getPropiedad() {
return $this->propiedad;
}
function setPropiedad( $val ) {
$this->propiedad = $val;
}
}
$ob = new Clase;
$ob->setPropiedad( "Un valor" );
print( $ob->getPropiedad() );
?>
Estas variables privadas y accesibles mediante
getter/setter suelen denominarse habitualmente
propiedades. PHP5 lleva más allá el control de las propiedades mediante
dos métodos especiales: __get() y __set(). El
intérprete de PHP los invoca (si se han implementado en la clase) si se
intenta leer (__get) o escribir (__set) en una variable de la clase
que no existe:
<?php
class Clase {
public $a;
protected $b;
private $c;
function __set( $name, $val ) {
print("Intentando escribir en la propiedad $name el valor $val\n");
}
function __get( $name ) {
print("Intentando acceder a la propiedad $name\n");
}
function test() {
print( "------------- dentro de la clase ---------------\n" );
$this->a = 4; // atributos que existen
$this->b = 5;
$this->c = 6;
$this->x = 7; // atributo que no existe
print( "------------------------------------------------\n" );
}
}
$ob = new Clase();
$ob->a = 1; // atributo que existe
print( $ob->z ); // atributos que no existen (acceso y escritura)
$ob->y = 2;
//$ob->b = 3; // intentar acceder a un atributo privado produce error
$ob->test(); // probar desde dentro de la clase
?>
Como se puede observar, __get() y __set() sólo
funcionan con variables no declaradas en la clase. Si están declaradas,
no se les invoca. En el caso de ser privadas, se produce un error de
acceso.
En el ejemplo anterior hemos empleado esta característica para realizar
un primitivo control de errores, pero con ella podríamos por ejemplo
crear y destruir dinámicamente propiedades a nuestro antojo
--guardándolas en un array--.
(Nota: PHP reserva todos los identificadores que comienzan por "__" para
su uso interno. Nunca deberían nombrarse métodos de nuestras clases con
nombres de tal estilo, o podrían colisionar con nombres utilizados por
el propio intérprete, produciéndose errores o resultados inesperados).
Sobrecarga de métodos (Overloading)
La sobrecarga de funciones es otra de las características habituales en
lenguajes OO de la que carece PHP4. Los habilidosos programadores de
PHP4 lidiaban con esta carencia utilizando diversas técnicas, como el
empleo de parámetros opcionales:
class Clase {
...
function sobrecargada( $x, $y = 0, $z= "" ) { ... }
}
$ob = new Clase;
$ob->sobrecargada( 1, 2, "hola" );
$ob->sobrecargada( 1 );
$ob->sobrecargada( 1, 2 );
Esta técnica está limitada por las posibilidades de posicionamiento de
parámetros opcionales, pero otra manera más completa es utilizar las
funciones que proporciona PHP para la gestión de parámetros
--func_get_args(), func_num _args() y
func_get_arg()--:
<?php
class Clase {
public function sobrecargado() {
$args = func_get_args();
if ( 1 == func_num_args() ) {
if ( is_int( $args[0] ) )
$this->sobrecargado_int( $args[0] );
if ( is_string( $args[0] ) )
$this->sobrecargado_string( $args[0] );
}
else if ( 2 == func_num_args() ) {
if ( is_int( $args[0] ) && is_string( $args[1] ) )
$this->sobrecargado_int_string( $args[0], $args[1] );
}
}
private function sobrecargado_int( $i ) {
print( "sobrecargado_int = $i\n" );
}
private function sobrecargado_string( $s ) {
print( "sobrecargado_string: $s\n" );
}
private function sobrecargado_int_string( $i, $s ) {
print( "sobrecargado_int_string: $i, $s\n" );
}
}
$ob = new Clase();
$ob->sobrecargado( 1 );
$ob->sobrecargado( "hola" );
$ob->sobrecargado( 2, "mundo" );
?>
La mala noticia es que tampoco PHP5 soporta sobrecarga de métodos. Sin
embargo, los programadores de PHP5 tienen la vida un poco más fácil
porque Zend 2 ha añadido un soporte equivalente al que __get()
y __set() permiten con las variables, pero para los métodos. El
método __call() --si existe-- es invocado para todo método
que no exista en la clase actual. Ello nos permite utilizarlo
como una función catch-all desde la que controlar todas las
funciones que queramos sobrecargar, agrupando todo el código de gestión
de la "sobrecarga" en un único punto:
<?php
class Clase {
function otra( $x ) {
print( "dentro de otra(): $x\n" );
}
private function privada( $x ) {
print( "dentro de privada(): $x\n" );
}
function __call( $name, $args ) {
// metodo 'sobrecargado'
if ( $name == 'sobrecargado' ) {
if ( 1 == count($args) ) {
if ( is_int( $args[0] ) )
$this->sobrecargado_int( $args[0] );
if ( is_string( $args[0] ) )
$this->sobrecargado_string( $args[0] );
}
else if ( 2 == count($args) ) {
if ( is_int( $args[0] ) && is_string( $args[1] ) )
$this->sobrecargado_int_string( $args[0], $args[1] );
}
}
else if ( $name == 'privada' ) {
$this->privada( 93 );
}
}
private function sobrecargado_int( $i ) {
print( "sobrecargado_int = $i\n" );
}
private function sobrecargado_string( $s ) {
print( "sobrecargado_string: $s\n" );
}
private function sobrecargado_int_string( $i, $s ) {
print( "sobrecargado_int_string: $i, $s\n" );
}
}
$ob = new Clase();
$ob->sobrecargado( 1 );
$ob->sobrecargado( "hola" );
$ob->otra( 56 ); // existe, no capturada por __call
$ob->sobrecargado( 2, "mundo" );
$ob->privada(); // existe, no capturada por __call (error)
?>
También en el caso de __call() sólo se llama cuando un método
no ha sido definido. Si se ha definido, se llama al método normalmente
sin invocar a _call() (y si es privado, el control de acceso producirá
el error que debería dar).
La mejora no es demasiado significativa, pero tal vez podrían
encontrarse otros usos más creativos (además del obvio de control de
errores de llamadas a métodos inexistentes).
Constructores y destructores
En PHP4, el constructor de un objeto se denotaba con el mismo nombre que
la clase, tal y como se hace en muchos lenguajes de programación
orientados a objetos. Sin embargo, a diferencia de éstos, los
constructores en PHP4 sí se heredan, creando una serie de
inconvenientes que analizaremos más adelante.
Para evitar estas situaciones, en PHP5 se ha escogido que el constructor
tenga un nombre homogéneo en todas las clases: __construct().
Por compatibilidad hacia atrás, si dicho método __construct()
no existe, se seguirá la misma regla que en PHP4 --se buscará un método
con el mismo nombre que la clase--; y si tampoco se encuentra, se
proporcionará un constructor por defecto que simplemente reservará el
espacio en memoria para el objeto creado con new.
Un detalle que conviene remarcar es que, al no existir la sobrecarga de
métodos, tampoco puede sobrecargarse el constructor. El constructor es
único, y si necesitaramos distintas versiones, deberíamos proceder
aplicando alguna de las técnicas ya comentadas en el apartado de
sobrecarga de métodos.
Por otro lado, PHP5 también soporta el concepto de destructor,
implementado --si se desea-- mediante el identificador
__destruct(). Este destructor funciona así: cuando un objeto ha
quedado inaccesible, al llegar la cuenta de referencias a cero, el
destructor se invoca antes de liberar la memoria definitivamente.
No sé hasta que punto se puede hablar de garbage recollector en
PHP, comparado con los complejos mecanismos de recogida de basura
implementados en la máquina virtual de Java o .NET, pero por hacer una
(siempre peligrosa) analogía, el mecanismo de destructor de PHP5 está
más próximo a C#, donde existe destructor, que se invoca siempre antes
que el GC libere el objeto, que a C++ --sin GC, liberación de memoria
explícita-- o Java --sin destructor, finalize() no está
garantizada su ejecución--.
<?php
class Clase {
private $val;
function __construct( $val ) {
printf( "Invocando al constructor... ($val)\n" );
$this->val = $val;
}
function show() {
printf( "Valor de val=$this->val\n" );
}
function __destruct() {
printf( "Invocando al destructor...($this->val)\n" );
}
}
function test( $i ) {
$ob = new Clase( $i );
$ob->show();
} // se acaba el ambito
for( $i=0; $i<100; $i++ ) {
test( $i );
}
?>
En el ejemplo anterior podemos ver como los objetos no son
"recolectados" por el GC cuando se necesita memoria, sino que son
destruidos directamente al salir del ámbito.
Clonado. Constructor copia
Para resolver el problema del aliasing, PHP5 ofrece un mecanismo
de constructor de copia: cuando queremos copiar un objeto, utilizaremos
una nueva palabra clave del lenguaje clone.
$ob2 = clone $ob1;
El efecto de clone es realizar una copia completa, bit a bit,
del objeto origen ($ob1) en el objeto destino ($ob2). Es el mismo
comportamiento que teníamos en la copia de objetos de PHP4.
Sin embargo, este operador no es suficiente. Otra de las consecuencias
de usar referencias a objetos es que, si un objeto tiene a su vez
variables que son objetos, esta copia será superficial (shallow
copy), y en realidad ambos objetos estarán apuntando a la misma
instancia del objeto contenido.
Para poder realizar una copia en profundidad (deep copy) o, en
general, controlar el proceso de copia --clonado-- de un objeto, podemos
redefinir en cualquier clase el método especial __clone().
Este método se invoca después de copiar todos los atributos del objeto,
de forma que sólo nos tendremos que preocupar de aquellos atributos
en los cuales la simple copia no nos sirva:
<?php
class Clase {
private $val;
function __construct( $val ) {
$this->val = $val;
}
function getValor() {
return $this->val;
}
function setValor( $val ) {
$this->val = $val;
}
}
class ClaseCompuesta {
private $val;
private $ob;
function __construct( $val, $ob ) {
$this->val = $val;
$this->ob = new Clase( $ob );
}
function getValor() {
return $this->val;
}
function setValor( $val ) {
$this->val = $val;
}
function getObjeto() {
return $this->ob->getValor();
}
function setObjeto( $val ) {
$this->ob->setValor( $val );
}
function __clone() {
// copia en profundidad
$this->ob = new Clase( $this->getObjeto() );
}
}
$ob = new ClaseCompuesta( 1, 1 );
$clon = clone $ob;
printf( "ob: val=". $ob->getValor() ." ob=". $ob->getObjeto() ."\n");
printf( "clon: val=". $clon->getValor() ." ob=". $clon->getObjeto() ."\n");
printf( "Cambiando clon...\n" );
$clon->setValor( 2 );
$clon->setObjeto( 3 );
printf( "ob: val=". $ob->getValor() ." ob=". $ob->getObjeto() ."\n");
printf( "clon: val=". $clon->getValor() ." ob=". $clon->getObjeto() ."\n");
?>
En este ejemplo, si comentáis la función __clone(), podréis
observar el comportamiento de la copia superficial.
Una advertencia importante respecto al clonado. Esta operación
ha ido variando a lo largo del desarrollo de PHP5, y es posible que
encuentre documentación que difiera de los expuesto aquí: desde llamadas
directas a __clone() hasta el uso de un segundo parámetro
implícito ($that).
Ninguna de ellas funciona en la versión definitiva de PHP5. Si desea
clonar un objeto, debe emplear clone, no llamar al
método __clone(). En cuanto a recuperar valores del objeto
original, el objeto $this tiene dichos valores copiados, y no se
requiere el uso de ningún otro objeto implícito.
<hr>
En la segunda parte seguiremos profundizando en las novedades de PHP5,
echando un vistazo a los miembros de clase, a la herencia, clases
abstractas e interfaces, excepciones y otras novedades del entorno PHP5.