Programación orientada a objetos en PHP4
Una de las principales ventajas que ofrece PHP como lenguaje de desarrollo de aplicaciones web, es su flexibilidad, que nos permite hacer una gran cantidad de cosas de forma muy rápida. Sin embargo, conforme se van desarrollando aplicaciones más grandes, merece la pena dedicar tiempo a estructurar bien la aplicación, con el fin de hacerla lo más modular, reutilizable y mantenible que sea posible. Al igual que en otros lenguajes, la orientación a objetos nos puede ayudar en gran medida a conseguir estos objetivos.
Lamentablemente PHP, en su versión 4, no se puede considerar que sea completamente un lenguaje orientado a objetos, ya que carece de algunas características básicas como ocultación de información y sobrecarga de métodos, pero sin embargo es posible conseguir estas características con un poco de rigor. Aparte de esto, nos encontraremos con una serie de problemas, principalmente debido a la forma en que maneja las referencias, que puede hacer que nos volvamos locos en más de una ocasión si no tenemos claro cómo funcionan y no somos estrictos en su utilización.
En este artículo no voy a explicar los fundamentos de la orientación a objetos, ya que de eso ya hay multitud de libros y artículos, voy a explicar únicamente las peculiaridades que tiene PHP 4 y cómo trabajar con ellas. Comienzo explicando brevemente cómo funcionan las referencias en las asignaciones y en las llamadas a funciones y métodos, y continúo dando alguna sugerencia para conseguir la ocultación de información y la sobrecarga de métodos en las clases, finalizando el artículo con una introducción a la serialización de objetos.
Referencias
La forma en la que PHP maneja las referencias es uno de los principales problemas con el que nos podemos encontrar al programar en este lenguaje. Por defecto todas las asignaciones y pasos de parámetro se hacen por valor, de forma que si hacemos una asignación de un objeto en otro, $obj_a = $obj_b
, lo que estamos haciendo es crear dos instancias distintas. Esto puede que no nos cause problemas en muchas ocasiones, y que ni siquiera nos demos de cuenta, pero en otras situaciones en las que tengamos referencias entre objetos, o sea imprescindible que la instancia sea la misma, podemos tener verdaderos quebraderos de cabeza para encontrar el problema exacto que hace que la aplicación no funcione.
Para evitar este tipo de problemas es necesario utilizar referencias en las asignaciones de objetos, incluyendo las creaciones, y marcarnos la rutina de hacerlo siempre del mismo modo. Hay que tener en cuenta también que las referencias en PHP no son lo mismo que los típicos punteros de lenguajes como C. Los punteros son variables que contienen una dirección de memoria, y suelen aceptar dos operadores, en C el «*» para obtener el contenido de la dirección que contiene la variable, y el «&» para obtener la dirección de memoria de una variable. En PHP sin embargo tenemos únicamente un operador, el «&», llamado referencia. Este operador no devuelve la dirección de memoria de una variable, si no un alias de su nombre de variable.
En una asignación $a =& $b
, no estamos asignando a $a la dirección de memoria de $b, estamos haciendo que la variable $a sea un alias de la variable $b. Conviene tener esto en cuenta ya que este comportamiento puede hacer que las cosas no funcionen como esperamos en algunas circunstancias. Veamos un ejemplo para clarificar la situación.
<?php $a = 'salida por defecto'; $v1 =& $a; $v2 =& $a; echo $a."\n"; // -> salida por defecto echo $v1."\n"; // -> salida por defecto echo $v2."\n"; // -> salida por defecto $v2 = 'Salida modificada'; echo $a."\n"; // -> salida modificada echo $v1."\n"; // -> salida modificada echo $v2."\n"; // -> Salida modificada ?> |
En este ejemplo se define una cadena de texto con el nombre $a y se crean dos referencias hacia ella, $v1 y $v2. Tras esto se modifica la variable $v2 para que sea una cadena de texto, lo cual produce que se modifiquen también las variables $v1 y $a. Si estuviésemos trabajando con punteros, $a mantendría su valor y $v1 seguiría apuntando a $a, habiendo cambiado únicamente $v2.
Paso de parámetros
Al igual que en las asignaciones, es necesario prestar también atención al paso de parámetros a los métodos de una clase, tanto en la entrada como en la salida. Unos y otros se pasan y devuelven por valor, siendo necesario explicitar que se desea hacer por referencia cuando trabajemos con objetos.
Para indicar que queremos pasar un parámetro por referencia a un método o función, es necesario con anteponer el carácter «&» a la definición del parámetro en el método o función, no siendo necesario especificar nada más en la llamada. En el siguiente ejemplo se puede apreciar la diferencia existente entre indicar el paso por referencia o no indicarlo.
<?php class Punto{ var $x = 0; var $y = 0; function Punto($x, $y){ $this->x = $x; $this->y = $y; } } class Ejemplo{ function metodo1($p){ $p->x = 123; $p->y = 123; } function metodo2(&$p){ $p->x = 123; $p->y = 123; } } $punto =& new Punto(1,1); print_r($punto); Ejemplo::metodo1($punto); print_r($punto); // -> 1,1 Ejemplo::metodo2($punto); print_r($punto); // -> 123,123 ?> |
En el ejemplo se crea una instancia de la clase Punto con valores de coordenadas 1 y 1, y se llama de manera estática a los métodos de la clase Ejemplo. En la primera llamada no se especifica el paso por referencia, lo cual produce que dentro del método se trabaje con una copia local del objeto, haciendo que las modificaciones realizadas no se mantengan a la salida. Sin embargo en la llamada al método 2 se indica la referencia, lo que produce que las modificaciones realizadas en los atributos del objeto sean visibles a su salida.
La situación es similar cuando queremos que un método o función devuelva un objeto. Por defecto se devuelve por valor y hay que indicar explícitamente que se desea devolver por referencia, poniendo un «&» tras la palabra clave «function», en la definición del método o función. En esta ocasión sí es obligatorio indicar en la llamada que el valor devuelto se quiere coger por referencia. Todo esto puede apreciarse en el siguiente ejemplo.
<?php class A{ var $mensaje = "Clase A\n"; function trace(){ echo $this->mensaje; } } class B{ var $a; var $mensaje = "Clase B\n"; function B(){ $this->a =& new A(); } function trace(){ echo $this->mensaje; } function getA(){ return $this->a; } function& getA_2(){ return $this->a; } } $b =& new B(); $a = $b->getA(); $a->mensaje = "modificado\n"; $a->trace(); // -> modificado $b->trace(); // -> Clase B $b->a->trace(); // -> Clase A echo "\n\n"; $b2 =& new B(); $a2 =& $b2->getA_2(); $a2->mensaje = "modificado\n"; $a2->trace(); // -> modificado $b2->trace(); // -> Clase B $b2->a->trace(); // -> modificado ?> |
El ejemplo consta de una clase A y una clase B que contiene una referencia a una instancia de la clase A. La clase B dispone de dos métodos para obtener la instancia interna de la clase A, getA, que devuelve por valor, y getA_2 que devuelve por referencia. En un primer paso se crea una instancia de $b y se obtiene su $a mediante getA. Al modificar el mensaje interno de la instancia devuelta, podemos comprobar como la llamada a trace sobre ella devuelve la cadena modificada, pero que la llamada hecha directamente sobre el atributo interno de $b, nos sigue devolviendo el valor inicial, lo que nos demuestra que la llamada a getA no nos ha devuelto en realidad la instancia interna de la clase A, sino una copia. En la segunda parte del ejemplo puede verse como utilizando la llamada a getA_2 e indicando que nos devuelve una referencia, el resultado es el esperado.
Suele ser frecuente, sobretodo cuando no se lleva mucho tiempo programando en OO en PHP, encontrarnos con problemas derivados de que estamos trabajando con instancias distintas cuando creíamos que estábamos trabajando con una única instancia. La siguiente función permite realizar una comprobación que nos indica si dos variables son la misma instancia o no.
function compareReferences(&$a, &$b){ // creating unique name for the new member variable $tmp = uniqid(""); // creating member variable with the name $tmp in the object $a $a->$tmp = true; // checking if it appeared in $b $bResult = !empty($b->$tmp); // cleaning up unset($a->$tmp); return $bResult; } |
Utilizando el ejemplo previo, podemos comprobar su funcionamiento sobre los objetos que habíamos creado.
if(compareReferences($a, $b->a)) echo "a y b->a son iguales\n"; else echo "a y b->a son distintas\n"; if(compareReferences($a2, $b2->a)) echo "a2 y b2->a son iguales\n"; else echo "a2 y b2->a son distintas\n"; |
Obteniendo el siguiente resultado:
a y b->a son distintas a2 y b2->a son iguales |
Para terminar con toda esta explicación sobre las referencias, merece la pena dedicar algo de tiempo al uso de foreach. Cuando trabajamos con arrays foreach nos permite una manera muy cómoda de recorrer sus valores, sin embargo cuando lo que tenemos es una array de objetos puede causarnos problemas. Veamos el motivo.
La sentencia foreach recorre el array que se le indique, preparando en cada iteración dos variables, una para la clave y otra para el valor asociado a la clave.
$arr = array(1,2,3,4,5); foreach($arr as $inx => $val){ echo "$val"; } |
El problema radica en que los valores asociados a la clave ($val), se obtienen por valor, pudiendo así producirse resultados inesperados.
$o1 = new A(); $o2 = new A(); $o3 = new A(); $arr = array($o1, $o2, $o3); foreach($arr as $inx => $val){ if(compareReferences($val, $arr[$inx])) echo "val y arr[inc] son iguales\n"; else echo "val y arr[inc] son distintos\n"; } |
Como resultado de la ejecución obtendríamos 3 veces la cadena val y arr[inc] son distintos
. Si queremos evitar problemas de este tipo hay que utilizar el foreach de una manera algo diferente y tener el rigor de hacerlo siempre igual.
$o1 = new A(); $o2 = new A(); $o3 = new A(); $arr = array($o1, $o2, $o3); foreach($arr as $inx => $nousar){ $val =& $arr[$inx]; if(compareReferences($val, $arr[$inx])) echo "val y arr[inc] son iguales\n"; else echo "val y arr[inc] son distintos\n"; } |
Lo que hacemos es dar el nombre $nousar a la variable que nos crea automáticamente foreach, y como primera línea del bucle, poner siempre la asignación equivalente que nos devuelve el valor correcto, utilizando la referencia. En nuestro caso $val =& $arr[$inx]
.
Tras toda esta explicación en las siguientes páginas nos introducimos en otras cuestiones como visibilidad, sobrecarga de métodos, constructores y serialización.
Excelente clase de PHP.
Muchas gracias.
me parece estupendo el concepto que utiliza el autor de este articulo.felicitaciones!
Muy buen documental, realmente.
Perfectamente explicado y con ejemplos muy aclaratorios.
Por otro lado, en el post Próximos artículos de PHP, indicabas como futura publicacion «Acceso a bases de datos». Estaría muy interesada en saber cuál sería una buena estructura de acceso a datos en una aplicación web en PHP4 basada en clases y objetos, y siguiendo el modelo MVC.
Muchas gracias!
¡¡ Excelente exposicion !!
Realmente de muy buen nivel, seguro de las cosas que hablas, las exlpicaciones de por qué referenciar un objeto (cuando lo creas), los tips.
Muy claros ejemplos y su desarrollo.
Mis felicitaciones.
Saludos
Gastón.