|
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.
|