Académique Documents
Professionnel Documents
Culture Documents
PHP Avanzado
1
• Orientación a Objetos
• Interfaces y Clases
abstractas
• Espacios de nombres
• Rasgos
• Métodos mágicos
• Principios SOLID de la
orientación a objetos
Capítulo 1
PHP Orientado a Un desarrollo orientado a objetos pensaría en cada problema como una
identidad única, como un objeto. Puesto que el DNI siempre va asociado
a una persona, puede crear un objeto que identifique a personas, y a su
Objetos vez dentro de ese objeto programar una función (método en este caso)
que pueda calcular la Letra.
2
Mediante la palabra reservada class definimos una clase com- La clase anterior tiene tres propiedades y seis métodos para interactuar
pleta. Las variables que se utilizaran dentro de la clase (propie- con ellas (setters y getters). Los métodos que empiezan por set (por con-
dades) se definen entre el nombre de la clase y los métodos. vención) serán utilizados para dar valor a una propiedad y los que empie-
zan por get para obtener ese valor.
Los métodos son las acciones que puede realizar una clase y se definen
con la palabra reservada function. Nota: El nombre de los métodos siguen la convención adoptada por
PHP CamelCase, esto quiere decir que los métodos que tengan pala-
class Persona { bras compuestas hay que escribirlos con todas las palabras juntas y la
public $nombre;
primera letra de cada palabra en mayúscula exceptuando la primera
public $apellidos;
(lowerCamelCase). Si un método es el encargado de copiar datos ban-
$dni;
carios tendría que escribirse más o menos así copiarDatosBancarios.
function setNombre($nombre) {
$this->nombre = $nombre;
}
Con el código anterior hemos creado un objeto que contiene todas las
function getApellidos() { variables y funciones de la clase Persona. La variable que contiene ese
return $this->apellidos; objeto es $luisMiguel.
}
Para acceder a las funciones desde el nuevo objeto creado tenemos
function setDni($dni) {
que utilizar la variable que contiene al objeto, en este caso $luisMiguel
$this->dni = $dni;
seguido del operador ( -> ) y el nombre del método.
}
function getDni() {
Para trabajar correctamente, graba la definición de la clase en un archi-
return $this->dni;
vo llamado clases.php y crea un nuevo archivo llamado ejemplo.php
}
con el siguiente contenido:
}
<!DOCTYPE HTML>
3
<html> $this->nombre = $nombre;
<body> $this->apellidos = $apellidos;
require_once(“clases.php”); }
$luisMiguel->setNombre(“Luis Miguel”);
Nota: PHP reconoce el juego de caracteres UTF8 en el código por
$luisMiguel->setApellidos(“Cabezas Granado”);
eso es posible poner variables que contengan caracteres propios de
$luisMiguel->setDni(“08868143X”); una lengua como la ñ o la ç.
?>
<h1>
Ahora tenemos la posibilidad de añadir propiedades al objeto en la pro-
Datos de <?= $luisMiguel->getNombre()
pia creación.
. “ “ $luisMiguel = new Persona(“Luis Miguel”, “Cabezas Granado”, “08868143X”);
. $luisMiguel->getApellidos(); ?>
</h1>
En PHP existen algunos métodos especiales en la definición de una cla- • También obtiene todos los métodos miembro de la clase padre, que
se (métodos mágicos). El más importante es el constructor. Ésta se eje- funcionarán exactamente de la misma forma.
cuta cada vez que se crea un nuevo objeto y permite crear las variables
iniciales que se necesitan. El nombre del método constructor debe ser: • La clase hija puede a su vez definir nuevas variables y funciones.
4
function setNombre($nombre) { las culturas exista un nombre y al menos un apellido). La clase hija hace
$this->nombre = $nombre; una particularidad de la Persona para adecuarla al sistema Español, aña-
}
diendo el dni. Si creamos un objeto del tipo PersonaEspaña, todas las
function getNombre() {
propiedades y métodos de la clase superior se heredarán, estando dis-
return $this->nombre;
ponibles para el objeto creado.
}
function setApellidos($apellidos) { Si ahora se ejecuta el código instanciando la clase que hemos creado,
$this->apellidos = $apellidos; se puede ver que los métodos que tienen que ver con el nombre y los
} apellidos pueden utilizarse aunque no estén definidos en la clase Perso-
function getApellidos() { naEspaña.
return $this->apellidos;
} Redefinición de métodos
} Cuando definimos una clase hija, las funciones de la clase padre son au-
tomáticamente heredadas. Se llama redefinición de métodos a la crea-
class PersonaEspaña extends Persona { ción de funciones en la clase hija, con el mismo nombre que en la clase
public $dni; padre.
function setDni($dni) {
$this->dni = $dni; Podemos modificar la clase Persona para que recoja los dos apellidos
} en propiedades distintas. Si creamos una clase PersonaUSA que herede
function getDni() { Persona tendremos que redefinir el método getApellidos para que nos
return $this->dni; devuelva los apellidos en el orden correcto (suponiendo que en USA pri-
} mero se ponga el 2º apellido y después el 1º).
}
class Persona {
public $nombre;
La palabra reservada extends indica que la nueva clase creada será public $apellido1;
una extensión (heredará) de la clase que se escribe justo a la derecha public $apellido2;
de la definición. function setNombre($nombre) {
$this->nombre = $nombre;
El ejemplo anterior muestra una forma de definición de Persona muy ge-
}
nérica, con propiedades universales (o casi, suponiendo que en todas
function getNombre() {
5
return $this->nombre; $luisMiguel->setNombre("Luis Miguel");
} $luisMiguel->setApellidos("Cabezas", "Granado");
function setId($id) {
$this->id = $id;
}
Herencia encadenada
function getId() { Algunos lenguajes de programación permiten heredar de varias clases a
return $this->id; la vez; esto es conocido como herencia múltiple. PHP no permite heren-
} cia múltiple, pero sí herencia encadenada, es decir, permite heredar de
function getApellidos() { varias clases padres correlativamente. Un ejemplo es:
return $this->apellido2 . " " . $this->apellido1;
class A {
}
}
}
class B extends A {
Para ejecutar el código tendremos que modificar el archivo }
ejemplo1.php de esta forma: class C extends B {
}
<!DOCTYPE HTML>
class D extends C {
<html>
}
<body>
<?php
require_once("clases.php");
6
Podemos crear una función que cambie el nombre de un objeto pasado
como parámetro para ver este comportamiento.
Nota: La herencia múltiple tiene una serie de problemas asociados function cambiaNombre($objeto,$nombre) {
que es mejor evitar. El más grave es el efecto diamante. Si una clase $objeto->setNombre($nombre);
hereda de otras dos de forma múltiple es posible que las clases padre }
tengan métodos con el mismo nombre y entrarían en conflicto al no
Si ahora en el código forzamos un cambio de nombre veremos que se
poder determinarse la preferencia de los métodos heredados.
hará efectivo en el resultado.
cambiaNombre($luisMiguel,"Pedro");
Para ver la Herencia encadenada se puede añadir una nueva clase a
nuestro archivo clases.php. Dentro de los objetos hay que tener también cuidado con el alcance de
las variables. Las variables definidas fuera del entorno del objeto no son
class PersonaExtremadura extends PersonaEspaña {
accesibles desde los métodos de las clases, a menos que anteponga la
public $tarjetaFamiliaNumerosa;
palabra global.
function setTarjetaFamiliaNumerosa($tarjetaFamiliaNumerosa) {
$this->tarjetaFamiliaNumerosa = $tarjetaFamiliaNumerosa;
Miembros públicos, privados y protegidos
}
Mientras no se especifique otra cosa, los métodos y propiedades de una
function getTarjetaFamiliaNumerosa() {
clase son siempre públicos, es decir, son accesibles desde fuera del ob-
return $this->tarjetaFamiliaNumerosa;
jeto de la forma: $objeto->método. Pero existe un camino para que las
}
variables y los métodos no puedan ser accesibles desde código externo
}
al objeto.
tampoco será posible desde fuera del objeto hacer llamadas. Para hacer public function getApellido2() {
return $this->apellido2;
un método o propiedad privado, sólo hay que anteponer la palabra priva-
}
te delante de la declaración.
public function getApellidos() {
class Persona { return $this->getApellido1() . " " . $this->getApellido2();
private $nombre; }
private $apellido1;
Ahora ya se puede redefinir el método en PersonaUSA.
private $apellido2;
public function getApellidos() {
8
} La interface anterior define la estructura básica que queremos para las
Propiedades y métodos públicos clases de humano que existen. Solo es necesario exponer qué métodos
Por defecto, todos los métodos y propiedades que se declaren en una básicos son obligatorios para que las clases que implementen esta inter-
clase son públicos, a menos que lleven asociados la palabra protected o face los desarrollen.
private. Como buena práctica de programación, las variables deben defi-
Para implementar la interface:
nirse como privadas o protegidas, para que no puedan modificarse des-
de fuera del objeto. class Persona implements Humano
Clases abstractas
Interfaces
Un interface no permite crear el cuerpo de ninguna función, dejando es-
A veces es necesario que un equipo de varias personas trabajen juntas.
ta tarea a las clases que la implementen. Las clases abstractas permiten
En este caso se hace imprescindible definir unas pautas generales de
definir funciones que deben implementarse obligatoriamente en las cla-
trabajo para que el resultado final sea el esperado. Si el desarrollo con-
ses que hereden y, además, permiten definir funciones completas que
siste en programar varios objetos, el analista de la aplicación puede defi-
pueden heredarse. Las clases abstractas deben llevar la palabra reser-
nir la estructura básica en papel o crear una pequeña plantilla con méto-
vada abstract en la declaración de la clase y en todos los métodos que
dos que el objeto final debería tener obligatoriamente. Esta plantilla es
sólo definan su nombre.
un interface y permite definir una clase con funciones definidas, pero sin
desarrollar, que obliga a todas las clases que lo implementen a declarar abstract class Humano {
private $apellido1;
Las interfaces aseguran que una clase cumple una serie de requisitos private $apellido2;
para que luego puedan utilizarse de una forma concreta. Por ejemplo, public function setNombre($nombre) {
todas las clases que implementen de la interface Iterator pueden utilizar- $this->nombre = $nombre;
se dentro de un bucle del tipo foreach. }
9
protected function getApellido1() { declara como final, no podrá ser redefinido en ninguna clase que herede
return $this->apellido1; de la clase principal.
}
protected function getApellido2() { De esta forma se podrían declarar los métodos setDni y getDni de la cla-
return $this->apellido2; se PersonaEspaña como finales para que ninguna clase que herede de
} esta pueda modificar su funcionamiento.
public function getApellidos() {
final public function setDni($dni) {
return $this->getApellido1() . " " . $this->getApellido2();
$this->dni = $dni;
}
}
abstract public function getNombreCompleto();
final public function getDni() {
}
return $this->dni;
En lugar de un interface, hemos optado por utilizar una clase abstracta, }
porque tenemos claro como debe funcionar parte del código. También
se define el método abstracto getNombreCompleto, que obligatoriamen- Clases con métodos estáticos
te debe ser declarado en la clase que herede de ésta. En PHP se pueden declarar funciones dentro de una clase que no utili-
cen propiedades o métodos de la misma. Estos métodos pueden calcu-
La clase Persona queda así: lar valores numéricos, hacer una conexión a una base de datos o com-
probar que un correo electrónico esté bien definido. Son conocidos co-
class Persona extends Humano{
mo métodos estáticos y pueden ser utilizados sin necesidad de instan-
public function getNombreCompleto() {
ciar un objeto utilizando la palabra reservada static.
return $this->getNombre() . " " . $this->getApellidos();
}
Se puede implementar una comprobación del DNI de forma estática en
}
la clase PersonaEspaña.
En realidad, un interface es una clase que tiene todos sus métodos abs-
public static function comprobarDni($dni) {
tractos.
$letras = explode(",","T,R,W,A,G,M,Y,F,P,D,X,B,N,J,Z,S,Q,V,H,L,C,K,E");
}
La palabra clave final afecta a métodos y clases. Si se define una clase $numero = intval($dni);
como final, no podrá ser heredada por ninguna clase. Si un método se $letra = strtoupper(substr($dni,-1));
10
if (strlen($dni) == 9 && $letra == $letras[$numero%23]) { $this->setNombre($nombre);
return true; $this->setApellidos($apellido1, $apellido2);
} else { }
return false; Como la clase PersonaEspaña necesita, además de los datos básicos,
} el DNI, podría implementar un constructor que almacenara la informa-
}
ción del DNI y llamara al constructor de la clase padre (en este caso Hu-
Para acceder a un método estático desde el cuerpo del programa hay mano) para añadir el resto de los datos. Podría ser algo así:
que escribir el nombre de la clase que lo implementa seguido del opera-
public function __construct($nombre, $apellido1, $apellido2, $dni) {
dor ( :: ) y el nombre del método. En el ejemplo vemos que se puede ac-
$this->setDni($dni);
ceder al método comprobarDni() creando un objeto o mediante la cons-
Humano::__construct($nombre, $apellido1, $apellido2);
trucción Nombre::nombreDefecto. Si intenta llamar a un método que no
}
es estático de esta forma se imprimirá un error en pantalla.
Para llamar al constructor se utiliza directamente el nombre de la clase
El código para comprobar un DNI antes de insertarlo en el objeto podría con el operador (::). El fichero de ejemplo nos quedaría entonces de la
ser algo parecido a esto: siguiente forma:
if ($luisMiguel::comprobarDni("08868143X")) { if (PersonaEspaña::comprobarDni("08868143X")) {
} "08868143X");
}
Llamadas a funciones padre
El operador ( :: ) se puede utilizar también dentro de una clase para ha- Nota: Como el método comprobarDni es estático se puede utilizar sin
cer llamadas a funciones de la clase padre que estén sobrecargadas. Si necesidad de crear un objeto de la forma
define un constructor para la clase padre y otro para la clase hijo, cuan- PersonaEspaña::comprobarDni.
do cree el objeto, el constructor que se ejecuta es el de nivel inferior, el
constructor hijo. Para llamar al constructor padre debe utilizarse la no- Esta forma de actuar puede resultar complicada si no conoce el nombre
menclatura Nombre_clase_ padre::__construct(). de la clase padre. La solución a este pequeño inconveniente la tenemos
en la palabra parent, que hace alusión a la clase de la que heredamos.
La clase abstracta Humano podría implementar un constructor para aña-
dir los datos básicos en la creación de los objetos. Sobrecarga de métodos
public function __construct($nombre,$apellido1, $apellido2) {
11
Algunos lenguajes de programación orientados a objetos permiten decla- necerán al mismo espacio de nombres. De esta forma podemos añadir
rar más de un método con el mismo nombre. La definición de estos mé- el espacio de nombres Humano a nuestro fichero de clases.
todos varía en el número de parámetros o en los tipos de datos de los
namespace Humano;
argumentos. PHP no permite sobrecarga de métodos, pero puede simu-
lar esta actuación empleando la técnica de las funciones con un núme- Para utilizar las clases del espacio de nombres lo primero que hay que
ro variable de parámetros. hacer es incorporar el fichero al flujo del programa con un require_on-
ce() y después utilizar las clases o funciones de la forma normal, con la
private $alias; salvedad de que, al instanciar una clase, hay que añadir como sufijo de
public function __construct($nombre, $apellido1, $apellido2, $dni, $alias) la clase el nombre del namespace. El ejemplo siguiente muestra cómo
{ hacerlo:
if(func_num_args() > 4) {
require_once(“clases.php”)
$this->setAlias($alias);
$luisMiguel = new Humano\PersonaEspaña(......
}
$this->setDni($dni); La clase PersonaEspaña se instancia sumando el espacio de nombres
parent::__construct($nombre, $apellido1, $apellido2); con el nombre de la clase.
}
un espacio de nombres llamado "Loterías" con una clase Loto y un espa- namespace Humano\Persona;
cio de nombres llamado "Flores" con una clase llamada también Loto. namespace Humano\Persona\PersonaEspaña;
Las clases comparten el nombre, pero son totalmente diferentes en su namespace Humano\Persona\PersonaEspaña\PersonaExtremadura;
declaración. Para crear un espacio de nombres hay que añadir al princi- Como las clases están ahora agrupadas por espacio de nombre, es ne-
pio del fichero la palabra reservada namespace seguido de un nombre cesario especificar el espacio de nombres cuando se hereda de una cla-
descriptivo. Todas las clases y funciones declaradas en el fichero, perte-
12
se padre. Por ejemplo, para que la clase PersonaEspaña herede de Per- Fluent interface
sona debería modificarse el código de esta forma: Es habitual que los métodos que añaden datos a los objetos (set) no de-
namespace Humano\Persona\PersonaEspaña; vuelvan nada (void). Esta técnica de la orientación a objetos permite en-
class PersonaEspaña extends \Humano\Persona\Persona { lazar métodos de asignación de datos de manera sencilla y quedando el
código desarrollado muy limpio.
Nota: La herencia debe empezar por (\) seguido del espacio de nom-
bres y terminando en el nombre de la clase de la que se hereda. Para realizarla simplemente hay que devolver en todos los métodos set
el mismo objeto que trata la petición y así podrá enlazarse con otra peti-
El código para utilizar las clases se hace algo más complejo: ción de asignación.
{ return $this;
("Luis Miguel", "Cabezas", "Granado", "08868143X","Mago Lope"); Si todos los métodos devuelven el objeto podremos hacer una carga de
} valores de la forma siguiente:
Alias $luisMiguel->setNombre("Luis Miguel")
Si decide crear una estructura compleja de subniveles, es posible que ->setApellidos("Cabezas","Granado")
tenga que repetir muchas veces el espacio de nombres al instanciar las ! ->setAlias("Mago Lope")
clases. Para evitar esto se puede utilizar el operador use, que crea un ! ->setDni("08868143X");
alias a una clase o a un espacio de nombres para acortar su forma de Aunque no parece un gran cambio en realidad todo se vuelve más legi-
utilizarlo. ble. Veamos la verdadera efectividad con un ejemplo más claro.
Para ello se utiliza el operador use de la siguiente forma: Tenemos dos clases típicas de Pedidos y Lineas de Detalle.
use Humano\Persona\PersonaEspaña\PersonaEspaña as PersonaEspaña; namespace Pedidos;
if (PersonaEspaña::comprobarDni("08868143X")) { class Pedido {
$luisMiguel = new PersonaEspaña("Luis Miguel", "Cabezas", "Granado", private $lineas;
"08868143X","Mago Lope"); public function añadirLineaDetalle($linea) {
} $this->lineas[] = $linea;
13
} $linea2 = new Pedidos\LineaDetalle(5,"Tomates");
public function getLineasDetalle() { $pedido->añadirLineaDetalle($linea2);
} $linea3->setEnLitros();
} $pedido->añadirLineaDetalle($linea3);
} </html>
public function setEnLitros() { El código no tiene nada de especial. Se van creando las líneas de deta-
$this->liquido = true; lle y añadiendo al Pedido principal. Si el producto es en litros (líquido) se
}
activa el método setEnLitros.
}
Para poder utilizarlas tendríamos que escribir el código de la siguiente Estas clases las podemos transformar en fluent para obtener una limpie-
forma: za absoluta del código y obtener un resultado como este:
error_reporting(E_ALL); $luis->nuevoPedido()
?> ->con(2,"Patatas")
<html> ->con(1,"Leche")->liquido();
14
return $this->pedido; incorporando al objeto devolviendo el Pedido para que se puedan enla-
} zar las nuevas adiciones.
public function getPedido() {
return $this->pedido; Como veremos más adelante, puede no ser una solución muy elegante
} a la hora de evitar el acoplamiento entre clases y es posible que incum-
} pla algunos principios a tener en cuenta (Demeter Law).
class Pedido {
$this->lineas[sizeof($this->lineas) - 1]->setEnLitros(); Si volvemos al ejemplo de las personas, podemos crear una nueva cla-
return $this; se PersonaTexas para contener los datos de este tipo de ciudadanos
} americanos.
public function añadirLineaDetalle($linea) {
namespace Humano\Persona\PersonaUSA\PersonaTexas;
$this->lineas[] = $linea;
class PersonaTexas extends \Humano\Persona\PersonaUSA\PersonaUSA {
}
public function decirHola($nombre) {
public function getLineasDetalle() {
return "Hola " . $nombre;
return $this->lineas;
}
}
}
}
La clase PersonaTexas tiene un método para saludar en Español (por-
La clase Cliente es la encargada de proporcionar un objeto del tipo Pedi-
que es de habla hispana). Este método lo podría tener también la clase
do, es la versión más sencilla del Patrón Factory. Se ha añadido a la cla-
PersonaEspaña y PersonaExtremadura pero no se puede implementar
se Pedido un método que crea Lineas de Detalle con los datos y los va
en una clase superior (Persona) porque no todas las clases hijas po-
15
drían decir hola en Español (PersonaUSA no debería saber saludar en Se pueden utilizar varios rasgos definiéndolos en la clase separados por
español). comas. Si dos rasgos tienen métodos con el mismo nombre, se produce
un error fatal.
La solución es repetir el código en la clase que lo necesite, pero esto se-
ría un grave error. Los traits soportan el uso de métodos abstractos y estáticos para impo-
ner requisitos a la clase a la que se exhiban. Además, pueden componer-
Nota: Tratamos de crear buen código, y repetir trozos de código en se de otros traits y crear estructuras complejas.
distintas clases o archivos es algo tan común como desastroso para el
mantenimiento de una aplicación. Métodos mágicos
Son métodos proporcionados por el intérprete de PHP. Todos empiezan
Debemos intentar seguir la metodología DRY (Don´t Repeat Yourself) por dos guiones bajos seguidos (__). Si se insertan en una clase, permi-
en todo momento. ten añadir una funcionalidad determinada a los objetos.
La implementación de los rasgos es muy similar a la de las clases, pero return $this->nombre . “ “ . $this->apellido1 . “ “ . $this->apellido2
16
__get()
Se utiliza para recuperar el valor de la propiedad que le pasemos como
parámetro.
return $this->$variable;
Serialización
Los objetos son, en realidad, un conjunto de datos y funciones que guar-
dan una serie de estados de ejecución. Este conjunto de bits se pueden
almacenar, en un momento dado, y recuperar justo en el mismo estado
en el que se encontraba cuando lo guardamos. Esta técnica se llama se-
rialización y permite almacenar y recuperar el conjunto de bits que for-
man un objeto.
! "Cabezas","Granado","08868143X","Mago Lope");
$luisMiguelcopia = serialize($luisMiguel);
$miguelLuis = unserialize($luisMiguelcopia);
echo $miguelLuis->getNombre();
17
Capítulo 1
Técnicas avanzadas de lo puede lograrse con un workflow, generalmente significa que no inverti-
mos el suficiente tiempo en intentar buscar una solución limpia y buena.
18
mos en los demás. No torturemos a otros desarrolladores creando códi- En ingeniería de software, SOLID (Single responsibility, Open-closed,
go que es difícil de extender y mantener. Además, en algunos meses es Liskov substitution, Interface segregation and Dependency inversion)
posible que nosotros mismos seamos el "otro desarrallador". es un acrónimo mnemónico introducido por Robert C. Martin a comien-
zos de la década del 2003 que representa cinco principios básicos de la
6. Debe tener dependencias mínimas programación orientada a objetos y el diseño. Cuando estos principios
Mientras más dependencias tenga, más difícil va a ser de mantener y se aplican en conjunto es más probable que un desarrollador cree un sis-
cambiar en el futuro. Existen herramientas que nos ayudan a cumplir es- tema que sea fácil de mantener y ampliar en el tiempo.
te objetivo, marcando las dependencias "extrañas" de nuestro código.
Los principio SOLID son guías que pueden ser aplicadas en el desarro-
7. Cuanto más pequeño, mejor llo de software para eliminar código sucio provocando que el programa-
El código debería ser mínimo. Tanto las clases como los métodos debe- dor tenga que refactorizar el código fuente hasta que sea legible y exten-
rían ser cortos, preferentemente con pocas líneas de código. Debe estar sible. Debe ser utilizado con el desarrollo guiado por pruebas o TDD, y
bien dividido. Mientras mejor dividamos el código, más fácil se vuelve forma parte de la estrategia global del desarrollo ágil de software y pro-
leerlo. Este principio puede influenciar positivamente al punto 4 - va a gramación adaptativa.
facilitar la comprensión del código cuando lo lean otros desarrolladores.
Principio de Responsabilidad Única
8. Debe tener pruebas unitarias y de aceptación Una clase o un método deberían tener una única responsabilidad. Si
¿Cómo podemos saber que nuestro código cumple con los requerimien- nos fijamos en la clase PersonaEspaña, introduce un nuevo elemento
tos si no escribimos pruebas? ¿Cómo podemos mantener y extender el que solo es propiedad de las personas residentes en este país, el DNI.
código sin miedo a romper algo? El código sin pruebas no es código lim- class PersonaEspaña extends Persona {
pio. private $dni;
}
Principios SOLID final public function getDni() {
19
return $this->dni; $this->dni = new Dni($dni);
} }
if (strlen($dni) < 9) { }
$this->numero = intval($dni);
La clase PersonaEspaña quedaría de la siguiente forma:
$this->letra = strtoupper(substr($dni,-1));
private $dni; }
} }
20
$letras = explode(",","T,R,W,A,G,M,Y,F,P,D,X,B,N,J,Z,S,Q,V,H,L,C,K,E"); private $letra;
if (strlen($dni) < 9) { private $dni;
} if (Dni::comprobarDni($dni)) {
return true; }
} else { public function __toString() {
} return $this->dni;
} } else {
return $this->dni; }
} }
$this->ponerCerosIzquierdaAlDni($dni);
$luisMiguel = new PersonaEspaña("Luis Miguel","Cabezas","Granado");
$this->setDatosDni($dni);
$luisMiguel->setDni("08868143X");
if (strlen($this->dni) == 9 && $this->letra == $letras[$this-
echo $luisMiguel->getDni();
>numro%23]) {
Hay varias cosas en el ejemplo anterior que no parecen muy correctas. return true;
El sistema funciona, pero hace aguas por alguna parte. } else {
$this->borrarDatosDni();
Por ejemplo, la clase Dni tiene código repetido y eso es muy difícil de return false;
mantener porque es posible que las modificaciones que se hagan en al- }
guna parte del código no se apliquen a la parte donde el código está re- }
petido causando un desajuste en el funcionamiento.
Una posible modificación de Dni puede ser la siguiente:class Dni { public function getDni() {
21
} Imaginemos que la clase persona debe almacenar ahora el pasaporte y
private function ponerCerosIzquierdaAlDni(&$dni) { el libro de familia.
if (strlen($dni) < 9) {
$dni = "0" . $dni; La clase PersonaEspaña tendría que modificarse por cada nuevo docu-
} mento que tuviera que almacena violando el Principio abierto/cerrado.
}
private function setDatosDni($dni) {
Además, existe una dependencia de la clase PersonaEspaña con las cla-
$this->dni = $dni;
ses Dni, LibroFamilia y Pasaporte. Lo ideal es que las clases no sepan
$this->numero = intval($dni);
qué clases almacenan los datos, deberían tratarse de forma transparen-
$this->letra = strtoupper(substr($dni,-1)); te.
}
Hay que buscar una forma más elegante de resolver este problema.
private function borrarDatosDni(){
} private $pasaporte;
Ahora la clase Dni se puede dedicar solo a su cometido, que es saber parent::__construct($nombre, $apellido1, $apellido2);
}
todo lo posible sobre este documento. Podría almacenar la fecha de ex-
public function setDni($dni) {
pedición, datos del padre y la madre, etc.
$this->dni = new Dni($dni);
Principio abierto/cerrado }
Las técnicas de este principio nos van a permitir eliminar algunas depen- public function getLibroFamilia() {
22
} $this->ponerCerosIzquierdaAlDni($dni);
public function setPasaporte($pasaporte) { $this->setDatosDni($dni);
} }
} } else {
Para empezar vamos a crear una interface que deberán implementar to- return "DNI no válido";
}
ña.
public function nombreDocumento() {
interface Documento { return "DNI";
public function __toString(); }
public function nombreDocumento(); public function comprobarDocumento($dni) {
public function comprobarDocumento($documento); if (comprobarDni($dni)) {
public function setDocumento($documento); return true;
public function getDocumento(); } else {
} return false;
Esta interface contiene los datos básicos de cualquier documento legal }
o administrativo. Cada clase implementará además los métodos necesa- }
if (Dni::comprobarDni($dni)) {
La clase Dni podría quedar así: $this->ponerCerosIzquierdaAlDni($dni);
private $numero; }
private $letra; }
if (Dni::comprobarDni($dni)) { }
23
public function comprobarDni($dni) { }
$letras = explode(",","T,R,W,A,G,M,Y,F,P,D,X,B,N,J,Z,S,Q,V,H,L,C,K,E"); Lo más básico que podemos hacer para una clase Pasaporte o LibroFa-
$this->ponerCerosIzquierdaAlDni($dni); milia es:
$this->setDatosDni($dni);
class Pasaporte implements Documento {
if (strlen($this->dni) == 9 && $this->letra == $letras[$this->
private $pasaporte;
numero%23]) {
public function __construct($pasaporte) {
return true;
$this->pasaporte = $pasaporte;
} else {
}
$this->borrarDatosDni();
public function __toString() {
return false;
return "PASAPORTE : " . $this->getPasaporte();
}
}
}
public function setPasaporte($pasaporte) {
public function getDni() {
$this->pasaporte = $pasaporte;
return $this->dni;
}
}
public function getPasaporte() {
private function ponerCerosIzquierdaAlDni(&$dni) {
retunr $this->pasaporte;
if (strlen($dni) < 9) {
}
$dni = "0" . $dni;
public function nombreDocumento() {
}
return "PASAPORTE";
}
}
private function setDatosDni($dni) {
public function comprobarDocumento($pasaporte) {
$this->dni = $dni;
return true;
$this->numero = intval($dni);
}
$this->letra = strtoupper(substr($dni,-1));
public function setDocumento($pasaporte) {
}
$this->setPasaporte = $pasaporte;
private function borrarDatosDni(){
}
$this->letra = null;
public function getDocumento() {
$this->numero = null;
return $this->getPasaporte();
$this->dni = null;
}
}
}
24
Ya tenemos las clases implementando un mismo interface, por lo tanto, La clase PersonaEspaña ha quedado muy reducida y aún así tiene mu-
todas ellas sabrán responder como mínimo a los métodos del interface. cha más funcionalidad porque es capaz de almacenar documentos de
El método nombreDocumento lo utilizaremos para saber el tipo de docu- todo tipo, ¡incluso los que todavía no existen!
mento que estamos tratando en cada momento.
Esta clase cumple con el principio Abierto/cerrado porque está abierta a
Inyección de Dependencias nuevas funcionalidades sin necesidad de modificar el código.
La clase PersonaEspaña tiene una fuerte dependencia con las clases
Dni, Pasaporte y LibroFamilia. Tanto es así que cada vez que se añade Nota: El método setDocumento obliga a que el parámetro implemente
un tipo de documento nuevo es necesario crear nuevos métodos para la interface Documento haciendo un casting en la captura de los da-
soportar la instancia de los nuevos objetos. tos.
En vez de crear un set y un get por cada documento distinto, lo que ha- Ejercicio: Añadir a la clase la posibilidad de almacenar una tarjeta de
cemos es crear un setDocumento genérico al que le inyectamos el obje- crédito.
to del documento que debe almacenar (se lo pasamos como paráme-
tro). Cualquier nuevo documento que se implemente seguirá funcionan-
El código para probar la clase podría ser el siguiente:
do siempre que se cumpla con el interface.
<?php
class PersonaEspaña extends Persona {
error_reporting(E_ALL);
private $documentos;
?>
public function __construct($nombre, $apellido1, $apellido2) {
<!DOCTYPE HTML>
parent::__construct($nombre, $apellido1, $apellido2);
<html>
}
<body>
public function setDocumento(Documento $documento) {
<?php
$this->documentos[] = $documento;
require_once("clasesSolid.php");
}
$miDni = "08868143X";
public function getDocumentos() {
$miPasaporte = "08868143-1325322";
return $this->documentos;
$miLibroFamilia = "08868143AB";
}
$dniLuisMiguel = new Dni($miDni);
}
$pasaporteLuisMiguel = new Pasaporte($miPasaporte);
25
$miNombre = "Luis Miguel"; public function getAncho() {
$miApellido1 = "Cabezas"; return $this->ancho;
$miApellido2 = "Granado"; }
$luisMiguel->setDocumento($pasaporteLuisMiguel); }
$luisMiguel->setDocumento($libroFamiliaLuisMiguel); public function getAlto() {
return $this->alto;
foreach($luisMiguel->getDocumentos() as $documento) { }
?> }
</body> }
</html> Podemos crear ahora una clase Cuadrado derivada de la clase Rectán-
PRINCIPIO DE SUSTITUCIÓN DE LISKOV gulo.
Llamado así porque fue enunciada por primera vez por Bárbara Liskov, class Cuadrado {
premio Turing 2008, puede servirnos para determinar si nuestra jerar- private $ancho;
quía se ajusta al Principio Abierto/Cerrado. Los objetos de una clase de- private $alto;
berían poder sustituirse por instancias de las clases derivadas. public function setAncho($ancho) {
$this->ancho = $ancho;
De otra forma, una clase no solo debería ser de un tipo, sino comportar-
$this->alto = $alto;
se como tal.
}
26
return $this->alto; Como ejemplo de documento se puede crear la clase Visa. El problema
} es que las tiendas virtuales no suelen almacenar los datos de la tarjeta
}
de crédito, por lo tanto, habría que redefinir el método getVisa para que
Vamos a hacer un pequeño test de las clases (sin PHPUnit, solo por pro- no se pueda obtener ningún dato. Técnicamente es posible y no da
bar los valores). error, pero el funcionamiento lógico, que es obtener los datos del docu-
mento, se hace poco operativo. Parece que en este caso es mejor crear
$rectangulo = new Rectangulo();
una interface del tipo TarjetaCredito con algunos métodos más seguros
$rectangulo->setAncho(10);
que permitan hacer otro tipo de operaciones.
$rectangulo->setAlto(2);
do sus propiedades y el área es la correcta. ¿Qué pasa si sustituimos la public function __construct($visa) {
objetos que los utilizan deba ser modificado (A es un subtipo de B). public function comprobarDocumento($visa) {
return true;
En este caso Cuadrado sería una subclase y no un subtipo. }
$this->setVisa = $visa;
27
}
public function getDocumento() {
return $this->getVisa();
28
Patrones de diseño
2
• Singleton
• Factory Method
• Observer
• Decorator
Capítulo 2
Asimismo, no pretenden:
• Evitar la reiteración en la búsqueda de soluciones a problemas ya co- Motivación: Hay veces que es importante asegurar que una clase ten-
nocidos y solucionados anteriormente. ga una sola instancia, por ejemplo:
30
class Singleton {
• Una única cola de impresión.
private static $instancia;
• Un único sistema de ficheros. private $contador;
Aplicabilidad: Cuando debe haber una sola instancia, y debe ser acce- public static function getInstancia() {
sible a los clientes desde un punto de acceso conocido. if ( !self::$instancia instanceof self) {
! $this->contador++;
! $this->contador--;
31
Lo siguiente a destacar es que el método constructor es privado, por lo Intención: Centraliza en una clase constructora la creación de objetos
tanto no se podrá crear un objeto de la clase de la forma new Singleton; de un subtipo de un tipo determinado, ocultando al cliente la casuística
para ello habrá que utilizar el método getInstance(), encargado de com- para elegir el subtipo que crear.
probar si la instancia ya existe y de crear una si no existe todavía.
Motivación: A veces es imposible elegir de antemano cuál objeto espe-
Nota: Hay que destacar el uso de self en lugar de $this en el método cífico debe ser instanciado, ya que la elección de los objetos a utilizar
getInstance(). Como norma general se utiliza self para métodos y pro- puede depender de algo en el entorno de ejecución.
piedades estáticas y $this para propiedades y métodos de objeto.
Aplicabilidad: !
Si hacemos un ejemplo podemos comprobar que las instancias son úni- • Una clase no puede prever la clase de objetos que debe crear.
cas. Aunque se crean dos instancias de la clase Singleton ($singleton y
• Centralizar la creación de objetos.
$singleton2) los valores se van mantienen. $singleton y $singleton2 ha-
cen referencia en al mismo objeto. • Proveer una interfaz para el cliente, permitiendo crear una familia de
objetos sin especificar su clase.
<?php
require_once("clases.php");
Estructura:
$singleton = Singleton::getInstancia();
$singleton->incrementar();
F IGURA 2.2 Factory Method
$singleton2 = Singleton::getInstancia();
$singleton2->getContador();
echo $singleton->getContador() . "<br>";
?>
32
Para ver un ejemplo de este patrón de diseño vamos a utilizar las clases Intención: Definir una dependencia uno-a-muchos entre objetos, de tal
Documento del capítulo anterior eliminando el constructor para simplifi- forma que cuando el objeto cambie de estado, todos sus objetos depen-
car todo. dientes sean notificados automáticamente.
Añadiremos una nueva clase encargada de crear objetos según el tipo Se trata de desacoplar la clase de los objetos clientes del objeto, aumen-
que se solicite. Como las clases cumplen con los principios de la orienta- tando la modularidad del lenguaje, creando las mínimas dependencias y
ción a objetos solo habrá que escribir unas pocas líneas: evitando bucles de actualización (espera activa o polling).
class FactoryMethod { Motivación: Mantener las dependencias entre objetos, sin necesidad de
! private $documento; conocer al otro objeto.
! public function crear($tipo){
! ! if (class_exists($tipo)) { Aplicabilidad:
! $this->documento = new $tipo();
! ! return $this->documento;
• El patrón Observador define una dependencia del tipo uno-a-muchos
! ! } else {
(1: n) entre objetos, de manera que cuando el objeto 1 cambia su esta-
! ! ! return "NO EXISTE EL TIPO DE DOCUMENTO";
do, el observador se encarga de notificar este cambio a todos los n
! ! }
objetos dependientes para que se actualicen automáticamente.
}
• El observador no es un mediador entre los sujetos (objetos que cam-
}
bian de estado) y los objetos dependientes, ya que el mismo es un ob-
El método crear evalúa el tipo de documento que se requiere, e instan- jeto dependiente. El observador recibe la orden de actualizar por par-
cia un objeto que se puede utilizar. te del sujeto "dominante".
Para probar la clase podemos hacer algo parecido a esto: • Distinguimos entonces el objeto observado y los objetos observado-
$factoria = new FactoryMethod(); res. El objeto observado debe mantener una relación de los objetos
$documento = $factoria->crear("Dno"); que le observan y una forma de notificarles. Por su parte, los objetos
print_r($documento); observadores, sean del tipo que sean, deben implementar alguna fun-
$documento->setDocumento("08868143X"); cionalidad para actualizarse en función del nuevo estado del objeto
echo $documento; observado.
! ! unset($this->observers[$key]);
! ! }
F IGURA 2.3 Patrón Observer ! }
! abstract public function notificarObservers();
Vamos a crear una clase abstracta que permita registrar objetos observa- Los observadores deberían implementar la siguiente interface:
dores.
interface Observer{
abstract class Observable{ ! public function notificar($sender, $params);
! protected $observers; }
! function __construct(){
Todavía no hemos hecho nada. En este punto solo se ha definido cómo
! ! $this->observers = array();
deberían funcionar los objetos que pretendan seguir el patrón Observer.
! }
Lo siguiente es definir las clases que van a implementar y extender la
! public function registrarObserver($observer){
clase abstracta.
! ! if(!in_array($observer, $this->observers)){
34
! ! parent::__construct(); class SalvarLog implements Observer{
! } ! public function notificar($sender, $param){
! ! ! $observer->notificar($this, $this->param); ! }
! ! } }
! } Para ver el funcionamiento:
! public function Evento($texto){
<?php
! ! $this->param = $texto;
require_once("clases.php");
! ! $this->notificarObservers();
! }
$obj = new MiObservable();
}
$obj->registrarObserver(new Log());
La clase MiObservable hace una llamada al constructor padre para la
$obj->registrarObserver(new SalvarLog());
creación del array de objetos observadores.
El método Evento guardará el parámetro en una propiedad del objeto y $obj->Evento('Test 1');
sleep(2);
llamará a notificarObservers() encargado de enviar una señal a cada
$obj->Evento('Test 2');
uno de los objetos registrados; para ello se recurre al método notificar
sleep(2);
(que deben implementar todos los objetos observadores que implemen-
$obj->deregistrarObserver(new SalvarLog());
ten la interface Observer).
$obj->Evento('Test 3');
! }
Intención: Responde a la necesidad de agregar funcionalidad a un obje-
}
to por medio de la asociación de clases. Es un objeto con el que se pue-
de ejecutar funcionalidades de varias clases a la vez.
35
Motivación: Añadir responsabilidad a objetos individuales en lugar de a
D IAGRAMA 2.1 Patrón Decorator
toda la clase.
Aplicabilidad: !
Estructura:
class Libro {
private $autor;
private $titulo;
$this->autor = $autor;
$this->titulo = $titulo;
36
public function getAutor() { }
return $this->autor; }
protected $autor; }
} }
$this->titulo = $this->libro->getTitulo();
} private $libroDecorado;
} }
} ! Str_replace(" ","*",$this->libroDecorado->titulo);
37
Todas las clases que derivan de LibroDecorator actuarán con los datos
almacenados en LibroDecorator y la clase original quedará intacta, es
decir, LibroDecoratorExclamacion solo “decora” o “añade funcionalidad”
sin tener que tocar Libro.
$libroTolkienDecoradoExclamacion = new
! LibroDecoratorExclamacion($libroTolkienDecorado);
$libroTolkienDecoradoEstrella = new
! LibroDecoratorEstrella($libroTolkienDecorado);
$libroTolkienDecoradoExclamacion->decoraTituloExclamacion();
$libroTolkienDecoradoExclamacion->decoraTituloExclamacion();
$libroTolkienDecoradoEstrella->decoraTituloEstrella();
Ejercicio: Añadir una nueva clase que decore Libro para que invierta
el orden de salida de los datos (antes el título y después el autor).
38
Bases de datos
3
• SQLite
• MySQL
• Factoría de conectores
• PDO
Capítulo 3
Bases de datos permite trabajar a todos los ficheros como si fueran una única base de
datos.
40
nombre CHAR(255) NOT NULL, Hay veces que necesitamos saber el último valor auto incremental que
cuenta INTEGER NOT NULL); SQLite ha generado para asociar una imagen, o un archivo, a un regis-
INSERT INTO Usuario (id_usuario, nombre, cuenta)
tro de una tabla. Para averiguar este dato se puede utilizar el método de
VALUES (1, \"Luis Miguel\",7011);
SQLite lastInsertRowID().
INSERT INTO Usuario (id_usuario, nombre, cuenta)
VALUES (3, \"Pedro\",3445); $base_datos->query("INSERT INTO Usuario (nombre, cuenta) VALUES (\"José
$base_datos->query($consulta); $base_datos->close();
?> ?>
Al ejecutar el código veremos que existe un nuevo archivo llamado En el ejemplo puede ver la posible utilización del método. Otro método
Usuario.db que contendrá los datos que se han añadido. útil es changes(), que devuelve el número de filas afectadas por una
consulta anterior. Si borra varias filas, esta función le devolverá el núme-
El método query() permite añadir varias líneas de SQL, que se ejecuta- ro de filas que han sido borradas.
rán de forma secuencial.
<?php
Algunos gestores de bases de datos necesitan añadir una sentencia es- $base_datos = new SQLite3("Usuario.db");
pecífica para que uno de los campos sea auto incremental, es decir, que $base_datos->query("INSERT INTO Usuario (nombre, cuenta) VALUES (\"José
realidad es gestionado por la propia base de datos. Los campos enteros $ultimo_usuario = $base_datos->lastInsertRowID();
tratados como campos auto incrementales y, en las órdenes INSERT se echo "Último usuario creado : " . $ultimo_usuario;
?>
puede omitir la inclusión del valor, porque SQLite le dará el valor correla-
tivo de la fila que le corresponda. Recuperar datos
Como hemos visto antes, el método query() da la posibilidad de hacer
Últimos cambios en una tabla cualquier tipo de consulta vista al principio del capítulo. Las sentencias
CREATE, INSERT, DELETE o UPDATE no necesitan devolver ningún
41
dato, pero la sentencia SELECT tiene que devolver los datos requeridos. MySQL
La variable que obtiene los datos se considera como un recurso y debe PHP soporta muchos de los gestores de bases de datos relacionales
aplicarse un método para recuperar las filas una a una. Lo podemos existentes en el mercado. Las dos alternativas más comunes son Post-
comprobar con un ejemplo. greSQL y MySQL. Aunque PostgreSQL es mucho mejor en cuanto a ca-
<?php racterísticas y funciones soportadas del SQL 92 estándar, MySQL se ha
$base_datos = new SQLite3("Usuario.db"); hecho más popular en el ambiente de los servidores Web. Cuando los
$resultado = $base_datos->query("SELECT * FROM Usuario"); servidores Web ofrecen su servicio como LAMP se refieren a Linux +
if (!$resultado) { Apache + MySQL + PHP.
echo "Parece que hay un error";
Este capítulo cubre las operaciones más comunes que los desarrollado-
} else {
res de PHP pueden hacer con MySQL, desde recuperar o modificar da-
while ($fila = $resultado->fetchArray(SQLITE3_ASSOC)) {
tos, buscar textos o hacer una copia de seguridad de la base de datos.
foreach ($fila as $indice => $valor) {
}
Conexión a MySQL
! }
La conexión no puede ser más sencilla. Es un proceso de dos pasos:
}
• Se conecta con el servidor de MySQL.
$base_datos->close();
42
En el caso de MySQLi se utilizan dos propiedades públicas para almace-
$consulta = "CREATE TABLE Usuario nar estos datos:
(id_usuario INTEGER PRIMARY KEY AUTO_INCREMENT,
nombre CHAR(255) NOT NULL, • $base_datos_mysql->insert_id para averiguar el último valor de inser-
cuenta INTEGER NOT NULL); ción.
INSERT INTO Usuario (id_usuario, nombre, cuenta)
VALUES (1, \"Luis Miguel\",7011);
• $base_datos_mysql->affected_rows para obtener las filas afectadas.
INSERT INTO Usuario (id_usuario, nombre, cuenta)
VALUES (2, \"María Fernanda\",3454);
Recuperar datos
INSERT INTO Usuario (id_usuario, nombre, cuenta)
Para recuperar datos de una SELECT tendremos que actuar de la mis-
VALUES (3, \"Pedro\",3445); ma forma que con SQLite.
INSERT INTO Usuario (id_usuario, nombre, cuenta)
<?php
VALUES (4, \"Javier\",1123);";
require_once("configuracion.php");
$base_datos_mysql->multi_query($consulta);
$base_datos_mysql = new mysqli($servidor,$usuario,$pass,$base_datos);
$base_datos_mysql->close();
$resultado = $base_datos_mysql->query("SELECT * FROM Usuario");
?>
if (!$resultado) {
echo "Parece que hay un error";
La clase mysqli permite conectar a un servidor. En este caso, hemos } else {
puesto como servidor a localhost porque estamos utilizando nuestro or- while ($fila = $resultado->fetch_array(MYSQLI_ASSOC)) {
denador local para hacer las pruebas. foreach ($fila as $indice => $valor) {
Tal y como vimos en el apartado de SQLite, la clase mysqli tiene tam- Uno de los grandes problemas de PHP para utilizar sus objetos de cone-
bién definidos los métodos para averiguar el número de filas afectadas y xión a bases de datos es que cada clase tiene un nombre distinto que
el último valor autonumérico generado después de un INSERT. identifica al tipo de conexión (SQLite3, mysqli, PostgreSQL). Cambiar de
gestor de bases de datos un proyecto con muchas líneas de código, pue-
43
de suponer tener que cambiar cada una de las líneas que se refieran al base de datos, su dirección y otros detalles necesarios para su cone-
gestor de bases de datos en particular. Por eso lo ideal es crear una cla- xión.
se genérica que se sirva mediante un Factory Method.
El método query es muy parecido al método de msqli.
Ejercicio: Crear una Factoría que permita seleccionar entre las dos
$result es un poco más interesante. Si bien sigue siendo un objeto opa-
bases de datos distintas.
co, ahora es un generador: ya no es necesario llamar a una función para
extraer un valor y avanzarlo; eso lo hace foreach, dejando más en claro
PDO (PHP DATA OBJECT) que estamos haciendo algo con cada elemento.
PDO es una capa de abstracción sobre las llamadas a bajo nivel de con- PDO agrega soporte global para cosas como consultas preparadas, aun-
sultas a DB. Se compone de 3 clases: PDO, que se encarga de las cone- que el motor no las soporte nativamente. Un beneficio de esto es que
xiones, PDOStatement, que envuelve consultas y resultados y PDOEx- repetir una misma consulta con diferentes valores se vuelve más simple,
ception, su tipo nativo de excepciones. y a veces hasta más rápido (si el motor lo soporta, no es necesario man-
dar (y procesar) la consulta entera, sino sólo los parámetros nuevos).
En su forma más básica, usar PDO puede ser exactamente igual que
usar cualquiera de los otros drivers a mano: Simplemente iniciamos una <?php
conexión, ejecutamos la consulta y usamos los resultados. require_once("configuracion.php");
<?php
$base_datos_PDO = new PDO("mysql:unix_socket=/var/run/mysqld/mysqld.sock;
require_once("configuracion.php");
! ! ! dbname=$base_datos; charset=utf8",$usuario,$pass);
44
Al hacer prepare, creamos una consulta que tiene dos valores sin defi-
nir, :nombre y :cuenta. bindValue hace que el valor de ese hueco sea
igual al nombre que pasamos como parámetro.
45
Servicios Web
4
• XML
• SimpleXML
• Cliente y Servidor SOAP
• REST
Capítulo 4
Servicios Web
<soap:Body>
<getProductDetails xmlns="http://warehouse.example.com/ws">
<productId>827635</productId>
</getProductDetails>
</soap:Body>
</soap:Envelope>
Una pregunta al servidor puede ser como la que sigue: El camino más sencillo para comprender XML es pensar en cómo se utili-
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
zan los documentos HTML. Estos documentos son estructurados y fun-
47
cionan con etiquetas y atributos. Las etiquetas van encerradas entre sím- </biblioteca>
bolos de mayor y menor (<b>) y deben cerrarse de la misma forma aña- Como puedes ver, la estructura del documento es muy similar a la de
diendo un símbolo de barra invertida (</b>). Los documentos HTML tie- una página Web. Está formado por etiquetas y atributos, cuya definición
nen una ortografía específica, es decir, unas reglas que definen la forma puede inventarse sobre la marcha, es decir, las etiquetas <libro>, <titu-
correcta de estructurar un documento; por ejemplo, la etiqueta <body> lo> o <seccion> podrían tener un nombre totalmente diferente, pero no
debe ir siempre después de </head> o, si no existe ésta, de <html>. Ade- así en HTML, que está obligado a mantener el lenguaje definido. Algu-
más, HTML contiene un diccionario cerrado de etiquetas que definen el nos navegadores, como Mozilla, interpretan los ficheros XML.
lenguaje y no podemos utilizar otras que no estén especificadas.
<biblioteca>
<tema id="informatica">
<libro>
</libro>
<libro>
<autor>Lecky Thomson</autor>
</libro>
<libro>
<titulo>Agile Web Development with Rails</titulo> Un documento XML está obligado a cumplir ciertas normas, que permi-
<autor>Sam Ruby</autor> ten definir un texto bien formado:
</libro>
</tema>
48
• Debe tener un único elemento raíz. Sólo puede haber un par de eti- leer un fichero completo con una sola instrucción e, inmediatamente des-
quetas que diferencien el inicio y el final del documento, como en pués, poder leer los datos como conjunto de variables PHP.
HTML, donde se utiliza <html> y </html>.
El conjunto de funciones que pertenece a la API SimpleXML es nuevo
• Los elementos deben ser hereditarios. La estructura <a><b></b></a> en PHP 5. Mantiene una absoluta flexibilidad a favor de la simplicidad y
se permite, pero no la siguiente <a><b></a></b>. En la primera forma bajo nivel de memoria usado. SimpleXML utiliza el menor número de lí-
la etiqueta <a> envuelve a la etiqueta <b>, que es la forma correcta neas de código para leer o escribir datos en un fichero XML. El ejemplo
de escribir un documento XML. HTML, sin embargo, permite la segun- siguiente muestra cómo podemos parsear el archivo biblioteca.xml:
da forma de componer un documento. <a
<?php
href="index.php"><b></a></b>.
$biblioteca = simplexml_load_file("biblioteca.xml");
• Los elementos pueden tener entre las dos etiquetas cualquier tipo de ! ! echo $libro->autor . "<br>";
}
• Los caracteres &, <, >, las comillas simples y las comillas dobles es- ?>
tán prohibidas como contenido y deben utilizarse símbolos de escape
Lo primero que llama la atención es que en apenas 10 líneas de código,
para utilizarlas.
se ha conseguido extraer todos los datos necesarios, a diferencia de
SAX o DOM. La función simplexml_load_file() crea un objeto con el ar-
SimpleXML
chivo XML que pase como argumento. A partir de aquí puede acceder a
Probablemente, el punto más fuerte de PHP 4 fue la incorporación de
todos los datos, así como acceder a las variables de los objetos:
herramientas para el soporte de XML. PHP 5 implementa nuevas herra-
mientas de lectura, elaboración y modificación de archivos XML basado echo $biblioteca->tema->libro[0]->titulo;
Para extraer el contenido que necesita lo más sencillo es crear una es-
Si XML es un lenguaje bien construido y legible por personas y ordena-
tructura de bucles foreach para ir sacando los datos ordenados.
dores, los programas para manipular estos archivos deberían ser tam-
El nombre SimpleXML es totalmente descriptivo, aunque no hay que
bién sencillos de utilizar. SimpleXML nace con esta premisa permitiendo
con- fundir la palabra simple con básico. La prueba de su potencia está
49
en que los desarrolladores de PEAR van a utilizar SimpleXML para desa- define("SOAP_VERSION", "SOAP_1_2");
El Servicio cliente crea un objeto del tipo SoapClient al que le pasa dos
parámetros: el primero a nulo, porque no existe una descripción del Ser-
Cliente SOAP vicio en formato WSDL, y el segundo un conjunto de parámetros que de-
Los servicios SOAP tienen asociados un archivo que define los métodos terminarán el lugar donde preguntar por el Servicio, la codificación, el
que se pueden usar y las variables de entrada que admiten. Actualmen- formato de la llamada.
te hay muchas compañías que permiten ejecutar código propio a través
de peticiones SOAP, como Amazon, Google, Yahoo, eBay, etcétera. Nota: WSDL (en ocasiones leído como wisdel) son las siglas de Web
Services Description Language, un formato XML que se utiliza para
Un cliente SOAP muy sencillo podría ser el siguiente: describir servicios Web .
<?php
require_once("configuracion.php");
Servidor SOAP
try { Para obtener algún resultado es necesario tener un Servidor que respon-
$cliente = new SoapClient(null, array( da a las peticiones.
'uri' => URI,
<?php
'location' => LOCATION
require_once("configuracion.php");
));
class servicios {
echo $cliente->saludo("Luis Miguel");
public function saludo($nombre) {
}
return "HOLA $nombre";
catch (SOAPFault $f) {
}
print $f->faultstring;
}
}
?>
try {
El archivo de configuración debería contener la dirección URI y la locali- $servidorSOAP = new SOAPServer(NULL,array('uri' => URI,
zación del archivo que hará de Servidor SOAP. 'location' => LOCATION));
$servidorSOAP->setClass('servicios');
<?php
$servidorSOAP->handle();
define("URI","http://172.20.102.147:3000/");
}
define("LOCATION","http://172.20.102.147:3000/servidor.php");
50
"urn:holamundowsdl">
catch (SOAPFault $f) { <types>
Para crear un Servidor SOAP hay que definir una clase con los métodos </xsd:schema>
que darán los servicios. Aquellos métodos que se declaren como públi- Aplicaciones prácticas de XML 309
</message>
Ejercicio: Crear por parejas un cliente y un Servidor SOAP que de-
<message name="holamundoResponse">
vuelva la hora del Sistema y el desfase entre máquinas de la hora.
<part name="return" type="xsd:string" />
</message>
son las siglas de Web Services Description Language, un formato XML <operation name="holamundo">
que permite describir Servicios Web. Define la forma de comunicación, <documentation>Devuelve un Ok si el usuario existe en el LDAP
</documentation>
es decir, los requisitos del protocolo y los formatos de los mensajes ne-
<input message="tns:holamundoRequest"/>
cesarios para interactuar con los servicios Listados en su catálogo. Las
<output message="tns:holamundoResponse"/>
operaciones y mensajes que soporta se describen en abstracto y se li-
</operation>
gan después al protocolo concreto de red y al formato del mensaje. A
</portType>
continuación puedes ver un archivo WSDL completo.
<binding name="holawsdlBinding" type="tns:holawsdlPortType">
<?xml version="1.0" encoding="UTF-8"?> <soap:binding style="rpc"
<definitions xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" transport="http://schemas.xmlsoap.org/soap/
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/ http"/>
2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/ <operation name="holamundo">
encoding/" xmlns:tns="urn:holamundowsdl" <soap:operation soapAction="urn:holawsdl#holamundo" style="rpc"/>
xmlns:soap="http://schemas.xmlsoap. <input><soap:body use="encoded" namespace="urn:holawsdl"
org/wsdl/soap/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/></input>
xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace=
51
<output><soap:body use="encoded" namespace="urn:holawsdl" require_once("configuracion.php");
require_once("clases.php");
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/></output>
require("Zend/Soap/Server.php");
</operation>
require("Zend/Soap/Wsdl.php");
</binding>
require("Zend/Soap/Wsdl/Strategy/ArrayOfTypeComplex.php");
<service name="holawsdl">
require("Zend/Soap/AutoDiscover.php");
<port name="holawsdlPort" binding="tns:holawsdlBinding">
<soap:address
ini_set('soap.wsdl_cache_enabled', '0');
location="http://localhost:3000/holaserver.php"/>
ini_set('soap.wsdl_cache_ttl', '0');
</port>
</service>
try {
</definitions>
! if (isset($_GET['wsdl'])){
Como puede ver, resulta algo complejo de leer un archivo WSDL y lo me-
! $autodiscover = new
jor es utilizar alguna herramienta para crearlos. Existe una biblioteca de
Zend_Soap_AutoDiscover('Zend_Soap_Wsdl_Strategy_ArrayOfTypeComplex');
clases que forma parte de Zend Framework, que facilita esta tarea. Las
! $autodiscover->setClass('Fecha');
partes del archivo pueden diferenciarse en:
! $autodiscover->handle();
! } else {
• <definitions> Etiqueta raíz con la descripción del Servicio.
! ! $servidorSOAP = new SOAPServer(WSDL);
! ! $servidorSOAP->handle();
• <message> Mensajes de solicitud y respuesta del Servicio. ! }
}
• <portType> Métodos disponibles.
catch (SOAPFault $f) {
}
Zend Framework utiliza una clase llamada Autodiscover que permite lo- ?>
calizar todos los métodos dentro de una clase y generar un archivo Para obtener el archivo WSDL habrá que invocar el Servicio como servi-
WSDL. El siguiente código muestra como hacer un Servidor con WSDL: dor?wsdl; así obtendremos el descriptor que servirá para hacer una lla-
<?php mada desde un cliente SOAP. Si la variable wsdl se pasa por GET, se
crea un objeto del tipo Autodiscover y realiza su función. Si no está defi-
52
nida esta variable, se crea un objeto SOAPServer y se procede como en ! ! $diferenciaHoras = new DateTime();
! !
$diferenciaHoras->setTimestamp($fechaStampSistema->getTimestamp()
Hay que ayudar un poco a la clase Autodiscover a generar el WSDL di- ! ! + CORRECTOR - $fechaStampRemoto->getTimestamp());
ciéndole los tipos de los datos que se van a pasar como parámetro, y ! ! return $diferenciaHoras->getTimestamp();
los que se van a obtener como respuesta. Esto se hace mediante el es- ! }
tándar docblocks, que permite documentar el código de PHP de forma
que después se genera una documentación en forma de API. }
?>
Así, la clase Fecha quedaría de la siguiente forma:
El cliente SOAP solo tendría que hacer mención al WSDL para empezar
<?php
la ejecución.
class Fecha {
* Exporta la fecha del Sistema. Cuando se utiliza WSDL, la clase SoapClient tiene un método mágico
* para obtener todas las funciones que se pueden realizar con un Servicio
* @return string SOAP determinado.
*/
print_r($cliente->__getFunctions());
! public function fechaDelSistema() {
53
Algunos frameworks modernos como Ruby on Rails exportan directa-
mente REST en varios formatos como XML o JSON.
<?php
require_once("configuracion.php");
$direccionRest = curl_init();
$datosRest = curl_exec($direccionRest);
curl_close($direccionRest);
$xmlRest = simplexml_load_string($datosRest);
echo $datos->nombre;
?>
Ejercicio: Crear una factoría que permita leer REST en formato XML
o JSON.
54
Pruebas Unitarias
5
• Test unitarios
• Dobles de test
• Unit Test at FIRST
• Coverage
Capítulo 5
Pruebas Unitarias te la validez de partes o unidades del software, también llamadas prue-
bas unitarias.
Este tipo de pruebas son las más pequeñas realizables en una aplica-
ción. Evaluarán cada componente individual de una aplicación a nivel de
método de clase. Esto permite aislar de forma más eficiente el comporta-
miento erróneo que pueda producirse en la aplicación.
Existen varios tipos de pruebas cada una dirigida a un aspecto específi- La manera habitual de probar el tamaño del array es imprimiendo por
co de la aplicación. pantalla el valor que devuelve la función sizeof() antes y después de aña-
dir un elemento y comprobar realmente que devuelve 0 y 1 respectiva-
UNIT TEST: Pruebas Unitarias mente.
<?php
$fixture = Array();
Existe una diferencia entre realizar pruebas, es decir, comprobar que tu
echo sizeof($fixture).”\n”;
programa se comporta como se espera, y ejecutar una batería de tests,
$fixture[] = “elemento”; echo sizeof($fixture).”\n”;
es decir, fragmentos de código ejecutable que prueban automáticamen-
?>
56
En este tipo de test somos nosotros los que tenemos que interpretar los Ahora ya podemos hablar de tests automatizados. El objetivo de tener
valores que se muestran por pantalla. Para que tanto nosotros como automatizados los tests es el de cometer menos errores.
otros programadores puedan entender directamente lo que se expresa
con estas pruebas sería útil automatizar su salida mostrando por ejem- Aunque podemos desarrollar nuestra propia infraestructura para realizar
plo ‘ok’ cuando la prueba sea correcta y ‘not ok’ cuando algo va mal en las pruebas unitarias, en la actualidad existen diversos frameworks que
los tests. proporcionan todas las utilidades y extensiones necesarias para ejecutar
pruebas y ver sus resultados. Este tipo de frameworks se conocen como
<?php xUnit ya que todos se derivan del creado originalmente por Kent Beck
$fixture = Array(); para Smalltalk: SUnit. A partir de éste se crearon JUnit para Java, NUnit
echo sizeof($fixture) == 0 ? ”ok\n” : “not ok\n”;
para .Net, etc. En PHP los frameworks más utilizados son PHPUnit,
$fixture[] = “elemento”;
Simple Test y PHPT.
echo sizeof($fixture) == 1 ? ”ok\n” : “not ok\n”;
$fixture = Array(); PHPUnit puede ejecutarse desde una consola o shell y está implementa-
assertTrue( sizeof($fixture) == 0 ); do en la mayoría de los 3 IDE que soportan PHP, ya sea en forma de plu-
$fixture[] = "elemento"; gin o de forma nativa.
assertTrue( sizeof($fixture) == 1 );
function assertTrue($condition){ Llamamos Casos de Prueba a las clases que contienen la lógica necesa-
if(!$condition){ ria para probar a otras clases.
echo "Assertion failed.";
Para tener un mayor control sobre estos casos de prueba se suelen dis-
} else {
tribuir en el sistema de ficheros de la misma manera que los archivos de
echo "Assertion OK.";
nuestro proyecto que contienen las clases a probar. Para ello lo más ha-
}
bitual es tener un directorio /tests en nuestro directorio de proyecto, que
echo "<br/>";
a su vez tiene la misma distribución de archivos de clases y directorios
}
que nuestro proyecto. La diferencia es que estas clases son los casos
?>
de prueba que utilizaremos para probar las clases del proyecto.
57
Cuando utilizamos PHPUnit las clases que actúan como casos de prue- nerado por nosotros. Lo normal es tener nuestro código en una clase de-
ba heredan de la clase PHPUnit_Framework_TestCase o de subclases finida en un fichero independiente que llamamos MyOwnArray.php:
de ésta.
<?php
1. Los tests de una clase llamada Class se incluyen en una clase llama- class MyOwnArray
{
da ClassTest.
private $myArray;
2. En la mayoría de las ocasiones esta clase ClassTest hereda de PHPU- function __construct()
nit_Framework_TestCase. {
$this->myArray = Array();
3. Los test son métodos públicos, sin parámetros y con el prefijo test* en }
su nombre. public function addElementToMyArray($element)
{
4. En los tests se utilizan afirmaciones (asserts), como assertEquals(),
$this->myArray[] = $element;
para comprobar que el valor obtenido tiene la relación indicada con el
}
valor esperado, en este caso que ambos sean iguales.
public function getMyArray()
return $this->myArray;
Es posible ejecutar los tests llamando al fichero que contiene los tests o }
a una clase en particular. Si llamamos a la clase, ésta debe existir en el }
mismo directorio en el que ejecutamos PHPUnit y en un fichero con el ?>
mismo nombre que la clase. El test para el código siguiente quedaría de esta forma:
Si llamamos al fichero, éste debe contener una clase con el nombre <?php
$this->assertEquals(0, sizeof($myOwnArray->getMyArray()));
58
} A continuación se indica el tiempo empleado para realizar los tests así
public function testArrayContainsAnElement(){ como la memoria que ha sido necesaria. Esta información es muy útil ya
$myOwnArray = new MyOwnArray();
que nos permite controlar la calidad de nuestros tests.
$myOwnArray->addElementToMyArray("elemento");
$this->assertEquals(1, sizeof($myOwnArray->getMyArray())); Finalmente, se nos muestra un resumen cuantitativo de los tests realiza-
} dos, indicando el estado general de los tests, el número de tests y el nú-
} mero de afirmaciones o asserts cumplidos.
?>
Este caso es el más favorable, donde todos los tests han pasado correc-
Para lanzar los tests utilizaremos el comando:
tamente. Vamos a modificar el ejemplo para provocar que alguno de los
phpunit --colors /Test tests nos muestre que nuestro código no funciona como se espera.
Nota: Al ser PHP 5.5 una versión RC, la biblioteca de PHPUnit no fun-
ciona muy bien y a efectos prácticos se ha descargado la última ver-
sión de PHPUnit en formato paquete ejecutable para facilitar la tarea.
59
sa a describirlo. Si hubiese más fallos se nos mostraría una lista enume- • Assert o Afirmar. Se comprueba que el resultado de la acción ante-
rada con la información específica. rior coincide con el valor esperado.
<?php
require_once "ejemplo6/MyOwnArray2.php";
! ! // Arrange
! }
En la línea siguiente muestra la afirmación o assert no cumplido que en
nuestro caso nos dice que se nos devuelve el valor 2 en la operación
! public function testArrayContainsAnElement() {
realizada cuando esperábamos 1. Más abajo nos indica el fichero de
! ! // Arrange
test concreto donde se produce el error así como la línea de código que
! ! $myOwnArray = new MyOwnArray2();
la provoca.
! ! // Act
! }
Los tests unitarios suelen seguir un patrón de diseño para tests que se }
resumen en realizar tres acciones bien diferenciadas llamadas con el ?>
acrónimo AAA:
Nota: Los comentarios solo son orientativos, no es necesario utilizar-
• Arrange o Preparar. En esta fase se inicializa y configura el SUT. los.
En nuestro caso la preparación del test se corresponde con la inicializa- protected function setUp(){
ción de la variable $myOwnArray que es el objeto en el que vamos a rea- $this->myOwnArray = new MyOwnArray();
61
$this->assertEquals(0, sizeof($this->myOwnArray->getMyArray())); características de nuestros tests debe ser la independencia con respec-
} to a recursos externos y a otros tests. Para ello existen lo que se denomi-
nan Dobles de Prueba, en el sentido de los dobles que aparecen en las
public function testArrayContainsAnElement(){
películas en las acciones peligrosas sustituyendo a los actores y actri-
$this->myOwnArray->addElementToMyArray("elemento");
ces.
$this->assertEquals(1, sizeof($this->myOwnArray->getMyArray()));
} Veamos un ejemplo de clase y de una dependiente de ésta:
La mayoría de los componentes de nuestra aplicación serán dependien- ! private $operands = array();
tes de otros componentes, no operará de forma totalmente independien- ! public function getCalculator() {
te. Podemos tener clases que se componen de objetos de otras clases y ! ! if (empty($this>calculator)) {
éstas últimas de objetos de otras clases a su vez. Este hecho puede pro- ! ! ! $this>calculator = new My_Calculator;
POO, estén muy acoplados con las objetos que componen el SUT. Sin ! return $this>calculator;
! }
embargo, como veremos en la siguiente sección, una de las principales
62
private $totaller;
! public function setCalculator(My_Calculator $calculator) { protected function setUp()
! ! $this>calculator = $calculator; {
! } $this->calculator = $this->getMock('My_Calculator');
! } }
public function testCalculateTotal()
->expects($this->at(0))
! ! foreach ($this>operands as $operand) {
->method('add')
! ! ! $total = $calculator>add($total, $operand);
->with(0, 1)
! ! }
->will($this->returnValue(1));
! return $total;
$this->calculator
! }
->expects($this->at(1))
}
->method('add')
?>
->with(1, 2)
Los dobles de test son objetos que utilizamos en lugar de los objetos de
->will($this->returnValue(3));
los que depende la clase que queremos probar. Veamos cómo trabaja
$this->calculator
PHPUnit con los dobles de prueba también llamados mocks o stubs.
->expects($this->at(2))
Más adelante veremos cual es la diferencia entre ambos.
->method('add')
->with(3, 3)
Fichero de test My_TotallerTest.php:
->will($this->returnValue(6));
<?php $this->totaller->addOperand(1);
require_once 'lib/Totaller.php'; $this->totaller->addOperand(2);
$this->totaller->addOperand(3);
class My_TotallerTest extends PHPUnit_Framework_TestCase $this->assertEquals(6, $this->totaller->calculateTotal());
{ ! }
private $calculator;
63
} si la definición del test doble llama al método with() que, como veremos
?> más adelante, indica los parámetros y su orden al llamar al método con-
En PHPUnit se utiliza el método getMock()para crear un doble de prue- creto del doble de prueba.
ba. Este método necesita como único parámetro el nombre de la clase
para la que se necesita crear un mock. El objeto devuelto por este méto- El método expects() acepta como parámetro un buscador o matcher,
do es una instancia de una subclase creada de forma dinámica desde la que es un objeto que representa una expectativa con respecto a la llama-
clase original. De esta manera esa instancia puede utilizarse en lugar de da del método indicado en el doble de prueba. Esa expectativa puede
una instancia de esa clase original con la salvedad de que el mock so- ser el número de veces que ese método será invocado o una referencia
breescribe los métodos públicos de la clase. Lo que se hace es indicar a una invocación específica a un método. En nuestro ejemplo, estamos
con el mock los parámetros que se tienen que pasar a los métodos que indicando el orden de ejecución ($this>at($orden)) en la cadena de lla-
vayan a usarse en las pruebas así como los valores que se devuelven, madas a ese método desde nuestro test. Es decir, que qué parámetros
simulando los objetos reales. Sin embargo, en ningún momento se es- espera y qué devuelve cuando se ejecute la primera vez ($this>at(0)),
tán utilizando instancias de las clases reales. cuando se llame la segunda ($this>at(1)) y la tercer ($this>at(2)).
Si analizamos el código de nuestro test, podemos observar que en el En el método method() indicamos el método que va a ser sustituido. Si a
método setup creamos un doble de prueba con getMock()y lo inyecta- continuación utilizamos el método with(), que es opcional, podemos aña-
mos en la clase a probar o SUT, My_Totaller a través del método setCal- dir restricciones a los parámetros del método nombrado en method(). Ca-
culator(). Posteriormente, el método testCalculateTotal() llama a calcula- da parámetro pasado en with()puede ser un matcher o un valor escalar
teTotal()de My_Totaller que internamente llama a getCalculator(), que es y se tiene que corresponder con cada parámetro y en el mismo orden
el que devuelve el doble de prueba. Aunque, si se observa detenidamen- del método sustituido. Pasar un valor escalar es equivalente a pasar ese
te, el comportamiento de nuestro mock lo estamos definiendo en el mis- valor encapsulado en una llamada a $this>equalTo(), que devuelve un
mo método que realiza el test. matcher que comprueba la equivalencia con el valor especificado. En el
apéndice D de este capítulo puede consultar más matchers para with().
Cuando definimos estos métodos, por defecto, si los creamos sin pará-
metros y sin asignarle una lógica siempre devuelven null. Cuando crea- El método will() se utiliza para especificar el resultado de la llamada al
mos un doble de prueba en cuya lógica hacemos ciertas comprobacio- método. En nuestro ejemplo usamos $this->returnValue() para devolver
nes como el número de parámetros, su tipo, valores esperados
un valor concreto. También podemos usar varios métodos para jugar
de los parámetros, etc.; estamos hablando de un mock. En caso contra- con los valores devueltos, como por ejemplo, devolver valores diferentes
rio, cuando la lógica no realiza ninguna comprobación, se trata de un en una secuencia de llamadas utilizando el método $this->onConsecuti-
stub. Para detectar rápidamente si se trata de uno u otro basta con ver veCalls(). También podemos retornar uno de los parámetros pasados al
64
método con $this>returnArgument()o lanzar una excepción con $this- De hecho, cuanto más rápido sean nuestros tests, más a menudo los
>throwException(). lanzaremos porque no nos supondrá un gasto considerable de tiempo
de desarrollo.
Si el sistema de test dobles de PHPUnit le resulta limitado puede probar
con otros frameworks dedicados completamente a este apartado como También es posible ejecutar grupos de tests de forma independiente pa-
son Mockery (https://github.com/padraic/mockery) y Phake ra acortar los tiempos de prueba, pero siempre sin olvidar que todos los
(https://github.com/mlively/Phake). tests tienen que pasar a verde para asegurarnos de que no tenemos nin-
gún error en nuestro código. Es decir, que aunque a menudo ejecute-
Unit Test are FIRST mos grupos separados de test para ir más rápido, también es necesario,
Para realizar pruebas unitarias efectivas se han de seguir los criterios aunque con menor frecuencia, pasar todos los tests.
agrupados con el acrónimo FIRST (en inglés Fast, Isolated, Repeatable,
Selfverifying, Timely). Este acrónimo resalta una característica principal Acciones que pueden ralentizar los tests son accesos al sistema de fi-
de TDD (TestDriven Development), una forma de hacer pruebas unita- cheros, ya sea para leer o escribir; obtener información de páginas web
rias (que veremos más adelante) donde se crean los test antes que el u otros recursos remotos con su correspondiente latencia, etc. La solu-
código. ción pasa por simular este tipo de acciones utilizando una especie de
sustitutos ligeros que devuelvan los datos esperados por el SUT, ya que
Fast (Rápido) no evaluamos esas acciones, sino lo que el SUT hace con la informa-
El número de test que podemos llegar a tener puede ser considerable. ción obtenida con esas acciones.
Si los tests tardan en ejecutarse, pasaremos más tiempo ejecutando
tests que desarrollando nuestra aplicación. Cada test debe ser muy rápi-
Isolated (Independientes)
do para que la suma de todos ellos no suponga una pérdida de tiempo Cada tests debe tener una única razón para fallar. Debemos diseñar ca-
respecto a la ganancia que obtenemos por el uso de estos tests. da test para que se independiente no solo de factores externos sino tam-
bién entre ellos, un test no puede depender de ningún otro test.
Si la media de ejecución de nuestros tests es de un minuto y tenemos 5
tests, cada vez que ejecutemos los tests, algo que pasará a menudo, es- Cuando los tests son dependientes entre ellos puede suceder que reali-
taremos 5 minutos de media esperando a que terminen. Si llegamos a zar un cambio en uno de ellos genere una cadena de fallos en el resto
construir 30 tests, será nuestra vida la que pasa mientras esperamos a de tests. Un ejemplo de pobre desacoplamiento es cuando tenemos que
que las pruebas nos muestren el color verde. Si construimos tests que ordenar los tests para optimizar su ejecución. Cada test unitario debe
tardan demasiado en ejecutarse es que estamos haciendo mal esos estar contenido en sí mismo, como un caso completo que documenta un
tests. comportamiento concreto. Consideremos el siguiente ejemplo en el que
se comprueba la disponibilidad de un libro en una gestión de biblioteca.
65
public function testAvailability() {
• La volatibilidad de los recursos externos (sistemas de ficheros, bases
$this->lordOfTheRings.checkOut(TODAY); d e d a t o s , s e r v i c i o s w e b , l l a m a d a s a A P I , e t c . )
$this->assertFalse($this->lordOfTheRings->isAvailable());
Comportamiento no determinista debido a un uso incorrecto de hilos o
$this->lordOfTheRings->checkIn();
procesos.
$this->assertTrue($this>lordOfTheRings>isAvailable());
qué es lo que nuestro código tiene que hacer y creando el código nece-
$catalog->add(new Book(BookTestData->AGILE_JAVA));
$catalog->add(new Book(BookTestData->JAWS));
sario para satisfacer esos tests, es decir, conseguir un verde.
$catalog->add(new Book(BookTestData->THE_TRIAL));
Además, al ajustarnos al test que tiene que satisfacer sabemos fácilmen-
$report = new InventoryReport($catalog);
te que nuestro requisito está implementado y no es necesario generar
echo $report->allBooks(); más código con mejoras inútiles no solicitadas por el usuario. Esto no
} significa que no haya que mejorar nuestro código una vez haya pasado
Si el sistema no se comporta correctamente tenemos que tener la espe- los tests, como ya veremos más adelante en los pasos a seguir cuando
ranza de que algún programador esté mirando la salida de nuestro códi- aplicamos TDD.
go para comprobar que se trata de la esperada.
Con TDD los tests son los primeros clientes de nuestro código. Al escri-
Los test automatizados deben verificar por sí mismos este tipo de condi- bir los tests antes que el código implica que tenemos que pensar en el
ciones: uso de nuestro código ante que en su implementación. Como resultado
nuestro código será más limpio al detectar más rápidamente las optimi-
public function testCreateReport() { $catalog = new Catalog();
zaciones de nombres de métodos, de variables, de listas de parámetros
... $this>assertTrue($report>contains(
y del resultado de aplicar los principios SOLID ya tratados con anteriori-
...
dad.
BookTestData.AGILE_JAVA.getClassification());
67
Los programadores que prueban el código después de su implementa- 1. La implementación de las funciones justas que el cliente necesita y
ción tienen poco o nulo interés en que sus pruebas sean especificacio- no más.
nes mediante ejemplos. Para ellos su único objetivo es comprobar si di-
versos aspectos de su código funciona o no. Estudios realizados indican 2. La minimización del número de defectos que llegan al software en fa-
que este tipo de tests unitarios cubren hasta dos terceras parte del códi- se de producción. 3. La producción de software modular, altamente
go. Al final, implica una llamada a elegir una de las dos opciones. Reali- reutilizable y preparado para el cambio.
zar test antes del código implica un sistema robusto, limpio, cubierto al
Cuando utilizamos TDD seguimos dos reglas básicas:
100% y donde la detección y corrección de errores es muy rápida. Tam-
bién implica la necesidad de invertir esfuerzo y parte de nuestro tiempo 1. Escribimos nuevo código sólo si un test automático ha fallado.
en escribir buenos tests, algo que puede implicar un aumento del tiempo
de un 15% a un 35%. 2. Eliminamos el código duplicado.
Escribir los tests después del código evita esa pérdida de tiempo que en Estas reglas se traducen en las siguientes tareas de programación:
la mayoría de las ocasiones es muy apreciada por los desarrolladores
1. Rojo. Escriba un pequeño test que, en principio, no va a funcionar.
que se resisten a adoptar TDD. Pero es una ventaja a corto plazo. A lar-
De ahí el color rojo indicando que ese test no pasa. Para ello, imagine
go plazo es un inconveniente. La detección y corrección de fallos pasa a
por un momento como le gustaría que la solución que ha pensado para
ser una labor tediosa, manual con largas sesiones de depuración y nun-
un requisito concreto aparezca en forma de código. Intente pensar en la
ca tendremos la certeza de que nuestros tests abarcan todo nuestro có-
interfaz, los métodos con sus parámetros, que le gustaría tener para ese
digo.
código. Está imaginando una historia. Incluya todos los elementos de la
TDD: TestDriven Development (Desarrollo guiado por pruebas) historia que está imaginando y que crea que son necesarios para obte-
ner las respuestas correctas.
Como dice Kent Beck en su libro “TestDriven Development by Example”,
el objetivo de TDD es código limpio que funcione, ya que se convierte en 2. Verde. Escriba el código necesario para que el test se ponga en ver-
un modo predecible de desarrollar, es decir, sabemos cuando hemos ter- de, sin tener en cuenta si su código es o no óptimo. Hágalo funcionar.
minado de implementar un requisito sin tener que sufrir largas jornadas L o i m p o r t a n t e e s q u e s u s t e s t s p a s e n a v e r d e . S i
de depuración. ya de inicio tiene una solución limpia y simple, escríbala. Si esta solu-
ción, limpia y simple, es obvia pero le llevará unos minutos, apúntela en
TDD es una técnica de diseño e implementación de software incluida alguna nota para retomarla más tarde y céntrese en el problema princi-
dentro de la metodología XP. Esta técnica tiene tres características fun- pal. Este paso suele ser duro para los programadores experimentados,
damentales:
68
que suelen tener más recursos al optimizar el código. • La jornada se hace mucho más amena.
3. Refactorice. Elimine todo el código duplicado resultado de hacer que • Uno se marcha a casa con la reconfortante sensación de que el traba-
el test pase a verde. Repase las notas generadas al escribir el código jo está bien hecho.
para que pase los tests y optimice su código.
Un ejemplo nos permitirá conocer más de cerca la forma de proceder de
Carlos Blé en su libro “Diseño Ágil con TDD”, nos señala las ventajas de TDD. Nos basamos en los siguientes requerimientos:
utilizar TDD:
Necesitamos una clase a la que se le pase dos cadenas de caracteres y
• La calidad del software aumenta. me devuelva la longitud de la cadena más larga.
• Conseguimos código altamente reutilizable. Si uno de los parámetros no es un string nos devolverá la longitud del
que sí lo es.
• El trabajo en equipo se hace más fácil, une a las personas.
Nos permite confiar en nuestros compañeros aunque tengan menos Si ninguno de los dos parámetros es un string nos devolverá false.
experiencia.
Antes de empezar haremos una lista de las características que ha de te-
• Multiplica la comunicación entre los miembros del equipo.
ner nuestra aplicación desde un análisis inicial, ampliando los requisitos
Las personas encargadas de la garantía de calidad adquieren un rol iniciales. Esto nos permite barajar de entrada muchas posibilidades y ob-
más inteligente e interesante. tener todos los casos de prueba que pueden ser útiles para nuestro códi-
go, e incluso desechar aquellos que no aportan nada.
• Escribir el ejemplo (test) antes que el código nos obliga a escribir el
mínimo de funcionalidad necesaria, evitando sobrediseñar.
Podemos empezar haciéndonos algunas preguntas antes de especificar
Cuando revisamos un proyecto desarrollado mediante TDD, nos da- qué acciones llevar a cabo:
mos cuenta de que los tests son la mejor documentación técnica que
podemos consultar a la hora de entender qué misión cumple cada pie- ¿ N e c e s i t a m o s c r e a r u n o b j e t o ? ¿ D e q u é c l a s e ?
za del puzzle. ¿ Te n e m o s q u e l l a m a r a a l g ú n m é t o d o d e e s e o b j e t o ?
¿Ese método tiene parámetros? ¿Son opcionales u obligatorios?
• Incrementa la productividad. ¿ Q u é p a s a s i n o l e p a s a m o s n i n g ú n p a r á m e t r o ?
¿Qué sucede si le pasamos un único parámetro de tipo distinto a string?
• Nos hace descubrir y afrontar más casos de uso en tiempo de diseño.
- ¿Y si es de tipo string?
69
22 7. Pasarle dos cadenas de diferente longitud y comprobar que devuelve
longitud de la más larga.
¿Y si pasamos los dos parámetros con tipos diferentes a string? ¿Y si
s o l o l o e s u n o d e l o s d o s ?
E m p e z a m o s c r e a n d o n u e s t r o s p r i m e r t e s t : E j e m p l o 1 0 .
Si le paso de forma correcta dos parámetros de tipo string, ¿me devolve- Fichero StringManagerTest.php:
rá la longitud correcta? ¿Y si tienen la misma longitud?
<?php
De estas preguntas podemos ampliar los requisitos iniciales y definir class StringManagerTest extends PHPUnit_Framework_TestCase {
una serie de acciones para iniciar el diseño de nuestros tests. ! private $stringManager;
}
2. Llamaremos a un método de ese objeto que será el que me devuelva
?>
la longitud de la cadena de caracteres más larga de las dos que le pa-
saremos como parámetros. Veamos qué salida nos proporciona PHPUnit:
3. S i n o l e p a s a m o s n i n g ú n p a r á m e t r o d e v u e l v e f a l s e .
4. Si le pasamos un único parámetro de tipo distinto a string devuelve
false.
70
Fichero StringManagerTest.php
<?php
! private $stringManager;
! }
function testGetMaxLengthForEmptyParameters() {
! $result = $this->obj->getMaxLength();
! $this->assertFalse($result);
! }
}
El error es evidente: la clase que acabamos de crear no tiene el método
?>
que estamos probando en nuestro primer test, getMaxLength(). Lo crea-
Ya tenemos nuestro primer indicador desde las pruebas de que necesita- mos en StringManager.php:
mos escribir el código para probar. En este caso, de que no existe la cla-
<?php
se del objeto que queremos instanciar en las pruebas. Vamos a escribir
class StringManager {
el mínimo código necesario para que el test pase. Creamos un fichero
! public function getMaxLength() {
en nuestro proyecto para definir la clase que queremos probar.
! }
Fichero StringManager.php }
?>
<?php
Lanzamos los tests:
class StringManager {
}
?>
71
que realmente no hace nada. Pero veremos cómo a medida que crea-
mos test y código para pasarlos se va generando el código correcto y
optimizado.
! $notStringParameter = 1;
<?php
! $result = $this->stringManager->getMaxLength($notStringParameter);
class StringManager {
! $this->assertFalse($result);
! public function getMaxLength() {
! }
! ! return false;
72
function testGetMaxLengthForOneParameterNotString() { <?php
! $stringParameter = "Test"; class StringManager {
} ! ! return $stringLength;
?>
Este test señala que ya no nos vale con que la función siempre devuel-
va false, ya esperamos un resultado correcto, la longitud del parámetro
que le pasamos. ¿Cómo afecta esto a nuestro código? Primero tiene
que pasar el test. Podríamos caer en la tentación de hacer que nuestro
método retornase el número 4 para que pase el test. Pero el resultado
es obvio.
Si nos paramos a analizar un poco, para hacer que pasen tenemos que
Haríamos pasar ese último test, pero fallarían los anteriores. La idea es realizar varias acciones. Al no pasar ningún parámetro el primer test nos
que pasen todas las pruebas. Esto implica que el código SUT tiene que señala que ese parámetro es obligatorio según la definición actual de
contener la lógica que estamos buscando. Aun así, si implementamos nuestro método. En el segundo test, al pasar un número en lugar de una
en un primer intento una lógica simple como esta: cadena de texto, éste es interpretado como una cadena de longitud 1.
Ejemplo 10.
73
Vamos a modificar el código de nuestros test para corregir estas deficien- 7. Si le pasamos uno de los dos parámetros de tipo distinto a string y
cias: otro de tipo string devuelve la longitud del tipo string, independiente-
mente de su orden.
<?php
class StringManager { 8. Pasarle dos cadenas de igual longitud y comprobar que devuelve su
! public function getMaxLength($string = null) {
longitud.
! ! $stringLength = (is_null($string) || is_numeric($string)) ?
! ! false : strlen($string); 9. Pasarle dos cadenas de diferente longitud y comprobar que devuelve
! ! return $stringLength; longitud de la más larga.
! }
}
Pasamos a tratar el elemento número 6: si le pasamos los dos paráme-
?>
tros de tipo distinto a string devuelve false.
! $notStringParameter1 = 1;
1. Necesitamos crear un objeto de una clase que llamaremos StringMa- ! $notStringParameter2 = 2;
nager. ! ! $result = $this->stringManager
! ! ! ! ->getMaxLength($notStringParameter1,
2. Llamaremos a un método de ese objeto que será el que me devuelva
! ! ! ! $notStringParameter2);
la longitud de la cadena de caracteres más larga de las dos que le pa-
! $this->assertFalse($result);
saremos como parámetros.
! }
3. Si no le pasamos ningún parámetro devuelve false. Este último test puede causar sorpresa por el hecho de que pasa sin pro-
blemas con el código del SUT actual. No es ningún inconveniente, como
4. Si le pasamos un único parámetro de tipo distinto a string devuelve veremos a continuación, los tests posteriores provocarán que este test
false. falle y nuestro código tenga que tener en cuenta este caso en particular.
5. Si le pasamos un único parámetro de tipo string devuelve su longitud. El séptimo caso indica que si le pasamos uno de los dos parámetros de
tipo distinto a string y otro de tipo string, devuelve la longitud del tipo
6. Si le pasamos los dos parámetros de tipo distinto a string devuelve
string, independientemente de su orden. Los tests nos quedan así:
false.
74
public function testGetMaxLengthForTwoParametersStringAndNotString() { ! $string2Len;
! $stringParameter = "Test"; ! return $stringLength;
! $notStringParameter = 1; ! }
! $result = $this->stringManager }
! $this->assertEquals($result, 4); Con estos cambios ya pasan todos nuestros tests. Nos faltan aún los
! } puntos 8 y 9. Para el octavo, le pasamos dos cadenas de igual longitud
y comprobamos que devuelve esa longitud. Esa condición ya se cumple
public function testGetMaxLengthForTwoParametersNotStringAndString() {
en la comparación de cadenas (incluso sin el comparador ‘=’) por lo que
! $notStringParameter = 1;
siguen pasando nuestros tests, aun así añadimos ese tests para tener
! $stringParameter = "Test";
cubierta esa posibilidad.
! $result = $this->stringManager
! } ! $result = $this->stringManager->getMaxLength($stringParameter1,
no está teniendo en cuenta el segundo parámetro del método. Vamos a ! $this->assertEquals($result, 4);
hacer pasar esta prueba. Como ya le estamos pasando los dos paráme- ! }
tros añadiremos el código necesario para devolver la longitud de la cade- El último punto y el más importante lo tratamos con el último test, donde
na de texto mayor . comprobamos que realmente se nos da la longitud de la cadena más lar-
ga.
<?php
75
Una vez cubiertos todos los casos posibles, vamos a ver qué posibilida- El siguiente comando genera un estudio sobre el código cubierto por
des existen de optimizar el código del SUT. En nuestro caso podemos test:
mejorar un poco las comprobaciones de los valores de los parámetros
del método. ./phpunit.phar --colors --coverage-html Coverage/ Test/
Podemos empezar simplificando las comparaciones de la clase String- Ejercicio: Generar el documento de cobertura para la clase DNI.
Manager.
Coverage
Este concepto trata de dar una métrica del código cubierto por Test. Si el
porcentaje de código cubierto por los test es muy alto, podemos afirmar
que es un buen código y darlo por apto en un entorno de PRODUC-
CIÓN.
76
Calidad
6
phploc
phpcpd
phpmd
phpcs
phpDocumentor
Capítulo 6
phploc
PHP Lines of Code nos da una información muy interesante acerca de la
topología del proyecto y su tamaño.
phploc wordpress/
Introducción
Esta última parte trata de cubrir la evolución natural del código después
de la utilización de una metodología basada en Test. Veremos algunas
utilidades que permiten alcanzar una buena calidad en nuestros proyec-
tos.
Un análisis de este tipo nos dará una foto de alto nivel de cómo es nues-
tro código fuente, como va incrementándose y como es de complejo. Es
una herramienta crucial, pero sólo cuando se ejecuta periódicamente.
78
La salida de phploc muestra información muy interesante y también pue- La detección del código repetido es muy importante si deseamos tener
de exportarse a XML para ser utilizada por sistemas de Integración Con- un software robusto, sin dependencias y DRY.
tinua.
phpmd
Nota: La Complejidad Ciclomática es una medida de cuántos caminos PHP Project Mess Detector ayuda a detectar código “que apesta” (code
posibles tiene una función o un método, o cómo de complejo es en tér- smells). Usa una serie de métricas para encontrar elementos dentro de
minos del número de test necesarios para cubrir el total de la función. un proyecto que se salen de un patrón de buenas prácticas:
Un número alto indica que debería refactorizarse.
phpmd wordpress/ text naming, codesize, unusedcode
phpmd encontrará:
phpcpd
PHP Copy Paste Detector busca en el código fuente patrones similares • Posibles errores.
de código con el ánimo de identificar donde se ha copiado y pegado (Es-
to no es DRY). Es una herramienta muy útil para incluir en un proceso • Código poco optimizado
regular de refactorización.
• Expresiones muy complicadas
El comando es muy sencillo:
• Parámetros, métodos y propiedades sin usar
phpcpd wordpress/
Estándares de codificación
Existen varias recomendaciones para el desarrollo de scripts de forma
estándarizada. Es recomendable que cuando se inicia el aprendizaje de
un nuevo lenguaje de programación se adquieran buenas costumbres a
la hora de codificar.
PSR-0
Este es el primero de los estándares y nos especifica las siguientes re-
Como la herramienta anterior, también es capaz de exportar los datos a glas:
XML para añadirlos a una aplicación de Integración Continua.
Puntos Obligatorios
79
• Los namespaces y las clases deben tener la siguiente estructura • Las constantes deben ser definidas en MAYÚSCULAS y utilizando
\<Vendor name>\(<Namespace>)*<Class Name> guion bajo (_) cómo separador.
• Cada namespace debe tener un namespace superior ("Vendor na- • Métodos y funciones deben ser escritos utilizando la técnica camelCa-
me"). se.
• Cada namespace puede tener tantos sub-namespaces como se quie- • Debemos de validar que la función que vamos a crear no exista utili-
ra. zando la función function_exists().
• Los nombres de los namespaces o clases deben ser separados por • Las llaves deben de estar abajo sólamente en las clases y métodos.
un guion bajo (_).
• La identación debe ser con un tabulador establecido a 4 espacios.
• Todos los archivos deben tener la extensión .php.
• Las constantes true, false y null deben ser escritos en minúsculas.
• $ Los nombres de los namespaces o clases deben ser ordenadas
alfabéticamente. • El número de caracteres por línea deben ser de 80 columnas aunque
también esta aceptado que sean hasta 120.
PSR-1, PSR-2 y PSR3
• Ya no debes utilizar la palabra reservada var para declarar una propie-
Convenciones
dad, debes utilizar public, private, static o protected.
• Los archivos deben utilizar solamente <?php y las short tags <?=
• Debe haber un espacio después de cada estructura de control (if, for,
foreach, while, switch, try...catch, etc.).
• Los archivos sólo deben utilizar una codificación UTF-8.
Phpcs también es capaz de examinar ficheros Javascript, CSS y están- Además, los comentarios suelen quedar obsoletos en la evolución de
dares personalizados. los proyectos y pueden inducir a errores. Como el texto no es compilado
Ejercicio: Corregir la clase StringManager para que pase el estándar La idea es escribir la documentación del proyecto mientras se genera el
PSR-2 código de la aplicación. Las herramientas de estandarización comproba-
rán que la documentación se está escribiendo y es acorde a la funcionali-
dad.
Documentar el código
Escribir la documentación del código fuente siempre es una tarea ardua
y pesada. Los comentarios del código fuente suelen ser justificaciones
81
}
phpDocumentor
/**
Utilizaremos esta aplicación para generar la documentación de la clase
* Move the character by a random amount
Robot, que sigue la norma de creación de comentarios.
*
/** */
* {
* @link http://sitepoint.com }
*/ return true;
class Robot }
{ ?>
82
83