Stockbyte (CD-DVD Num. IE008.)(Uso educativo no comercial para plataformas públicas de Formación Profesional a distancia.)
Carlos empieza a darse cuenta de que, con lo que lleva aprendido de PHP, ya es capaz de hacer bastantes cosas. Ha acabado de programar la aplicación web para gestionar su colección de comics, y está satisfecho con el resultado obtenido. De vez en cuando ha tenido que echar mano de la documentación del lenguaje, para buscar información sobre cómo hacer algo, o los parámetros que requiere cierta función, pero siempre ha podido solucionarlo por sí mismo.
Sin embargo, cuando revisa el código de su aplicación, se da cuenta de que está muy desorganizado. Cada vez que necesita hacer algún cambio, o introducir un añadido, tiene que rebuscar entre las páginas para encontrar el código a reprogramar.
Y si eso le pasa con su pequeña aplicación, no se imagina lo que sucederá cuando tenga que programar una aplicación más compleja. —¡Tiene que haber algún modo de obtener un código más limpio y estructurado!
En esta unidad aprenderás los principios de la programación orientada a objetos en PHP, algo fundamental hoy día en la programación de aplicaciones web en el entorno de servidor. Aprenderemos conceptos tales como creación de clases, uso de clases anónimas, interfaces, herencia, etc., pero desde la perspectiva de PHP.
La programación orientada a objetos en PHP es similar conceptualmente a como se realiza en lenguajes como Java, sin embargo tiene algunas diferencias importantes en la práctica, de ahí que es importante que leas esta unidad con atención aunque ya sepas programar en Java. En PHP existen algunos conceptos que no están presentes en otros lenguajes, como por ejemplo los rasgos, y otros que aún estando presentes son diferentes.
Después de abordar como se programa con orientación a objetos en PHP revisaremos como persistir la información de objetos, es decir, como almacenar la información de los objetos y luego recuperarla en caso necesario. Aprenderemos algunos conceptos de organización de proyectos en PHP que son importantes, tales como el uso de espacios de nombres, carga automática o uso del gestor de dependencias Composer.
Finalmente abordaremos los conceptos relacionados con la programación por capas en el entorno web, ahondando en el diseño básico de aplicaciones que siguen el patrón MVC.
Stockbyte.(CD-DVD Num. IE008.)(Uso educativo no comercial para plataformas públicas de Formación Profesional a distancia.)
Carlos comenta a Juan su problema, y éste le dice que seguramente gran parte del problema se arregle si utiliza programación orientada a objetos en sus aplicaciones. Le explica por encima de qué se trata y cuáles son sus ventajas, pero no puede ayudarlo mucho más. La última vez que programó en PHP, intentó utilizar objetos en su aplicación, pero las características de orientación a objetos del lenguaje dejaban mucho que desear, por lo que acabó haciéndola como siempre.
Sin embargo, por lo que ha oído, parece ser que en las últimas versiones de PHP eso ha cambiado considerablemente. El lenguaje evoluciona, y él lleva bastante tiempo sin utilizarlo, así que tendrá que actualizarse.
Mientras tanto, Carlos ya se ha puesto un nuevo reto: debe aprender a utilizar objetos en PHP. Y cuando tenga cierta soltura, lo primero que hará es modificar el código de su aplicación de comics.
La programación orientada a objetos (POO, u OOP en inglés), es una metodología de programación basada en objetos. Un objeto es una estructura que contiene datos y el código que los maneja.
La estructura de los objetos se define en clases. En las clases se escribe el código que define el comportamiento de los objetos y se indican los miembros que formarán parte de los objetos de dicha clase. Entre los miembros de una clase puede haber:
Métodos. Son los miembros de la clase que contienen el código de la misma. Un método es como una función. Puede recibir parámetros y devolver valores.
Atributos o propiedades. Almacenan información acerca del estado del objeto al que pertenecen, y por tanto, su valor es normalmente distinto para cada uno de los objetos de la misma clase.
A la creación de un objeto basado en una clase se le llama instanciar una clase y al objeto obtenido también se le conoce como instancia de esa clase.
Salvador Romero Villegas (Elaboración Propia)(CC BY-SA)
Los pilares fundamentales de la POO son:
Abstracción. Hace referencia a la capacidad de nombrar una acción compleja de forma simple ocultando las peculiaridades de su implementación o realización. Por ejemplo, la acción "pilotar un avión" es una acción compleja cuyas peculiaridades escapan a la mayoría de las personas, pero si hablamos con alguien sobre "pilotar un avión" seguramente sabrá a lo que nos referimos aunque no sepa pilotar. En PHP pasa igual, la función isset por ejemplo sabemos que hace y para que se usa pero realmente no sabemos como funciona por dentro (y posiblemente tampoco nos interese). La programación orientada a objetos se fundamenta en la abstracción pero a nivel de objeto: por ejemplo, sabemos que hace la clase PDO, qué métodos tiene, pero desconocemos como funciona por dentro. Los objetos presentan al exterior una serie de métodos (interface) cuyo comportamiento está bien definido y visto desde el exterior cada objeto es un ente abstracto que realiza un trabajo.
Herencia. Es el proceso de crear una clase ampliando o redefiniendo otra existente de la cual se hereda su comportamiento y características (métodos y atributos). Por ejemplo, podríamos crear diferentes tipos de productos: productos por peso, productos por metros cuadrados y productos por litros. Todos los tipos de productos comparten características entre si, pero cada uno modela aspectos diferentes, por ejemplo: la harina podría ser un producto por peso pero no podría ser un producto por litros, y un papel pintado para la pared podría ser un producto por metros cuadrados pero no un producto por peso.
Polimorfismo. Un mismo método puede tener comportamientos distintos en función del objeto con que se utilice. Por ejemplo, para calcular el precio de un producto cualquiera podríamos tener un método o una función que siempre tiene el mismo nombre (calcularPrecio), dicho método o función calcularía el precio de forma diferente en función de si es un producto por peso, producto por litros o producto por metros cuadrados.
Encapsulación. Es la capacidad de unir en un mismo lugar los datos y el código que los manipula, algo que en la programación orientada a objetos se realiza de forma natural al escribir el código de una clase.
Las ventajas más importantes que aporta la programación orientada a objetos son:
Modularidad. La POO permite dividir los programas en partes o módulos más pequeños, que son independientes unos de otros pero pueden comunicarse entre ellos.
Extensibilidad. Si se desean añadir nuevas características a una aplicación, la POO facilita esta tarea de dos formas: añadiendo nuevos métodos al código, o creando nuevos objetos que extiendan o redefinan el comportamiento de los ya existentes.
Mantenimiento. Los programas desarrollados utilizando POO son más sencillos de mantener, debido a la modularidad antes comentada. También ayuda seguir ciertas convenciones al escribirlos, por ejemplo, escribir cada clase en un fichero propio. No debe haber dos clases en un mismo fichero, ni otro código aparte del propio de la clase.
En este módulo no se pretende realizar un estudio profundo de las ventajas y características de la POO, sino simplemente recordar conceptos que ya se han aprendido en el módulo de programación. Aquí profundizaremos en la POO en PHP. Si tienes dudas sobre algo de lo que acabamos de repasar, puedes consultar el siguiente tutorial:
Stockbyte (CD-DVD Num. IE008.)(Uso educativo no comercial para plataformas públicas de Formación Profesional a distancia.)
Seguramente los aspectos nombrados en el apartado anterior, o la mayoría de ellos, ya lo conocías, así que vamos a ver directamente las peculiaridades propias de PHP en lo que hace referencia a la POO.
Como ya has visto en las unidades anteriores, especialmente con las extensiones para utilizar bases de datos, con PHP puedes utilizar dos estilos de programación: estructurada y orientada a objetos. Por ejemplo:
A diferencias de otros lenguajes, las primeras versiones del lenguaje PHP no contemplaban el paradigma de programación orientada a objetos. Fue a partir de la versión 3 de PHP cuando se empezaron a introducir algunos rasgos de POO en el lenguaje. Las capacidades de programación orientada a objetos fueron aumentando versión tras versión y fue con la salida de PHP 5 cuando el soporte de orientación a objetos del lenguaje empezó a ser suficientemente completo y eficiente, a partir de aquel momento usar POO en PHP compensaba por rendimiento, por facilitar la estructuración del código y por reducir el tiempo de desarrollo.
Una característica importante incorporada en PHP 5 es que los objetos se empiezan a pasar por referencia y no por valor. Es decir, a partir del PHP 5, como ocurre en otros lenguajes, no se crea una copia del objeto al pasarlo a una función o método. Por ejemplo, el siguiente código funciona de una forma diferente en PHP 4 y PHP 5:
<?php
class A {
var $valor;
}
function modificar ($a)
{
$a->valor=10;
}
$instanciaDeA=new A();
$instanciaDeA->valor=5;
modificar ($instanciaDeA);
echo $instanciaDeA->valor;
El código anterior en PHP 4 mostrará 5, pero en PHP 5 y posteriores mostrará 10.
En general, PHP tiene las siguientes capacidades a nivel de programación orientada a objetos:
Clases.
Clases abstractas y finales.
Clases anónimas (desde PHP 5.3).
Métodos estáticos.
Métodos constructores y destructores.
Visibilidad (private, public y protected).
Herencia.
Interfaces.
Autoloading.
Rasgos (traits) (desde PHP 5.4)
Otras características de PHP que son importantes de cara al uso de objetos:
Espacios de nombres (Namespaces) (desde PHP 5.3).
Enumeraciones
Entre las características de programación orientada a objetos que no están disponibles en PHP y que están presentes en otros lenguajes de programación tenemos:
Una de las características más apreciadas de los lenguajes de programación es la sobrecarga de métodos. En PHP normalmente se dice que no es posible realizar sobrecarga de métodos, pero la realidad es que si que es posible, pero se realiza de una forma muy diferente a otros lenguajes dado que en PHP hay que usar lo que se denomina métodos mágicos.
Se refiere a la posibilidad de tener dos o más métodos con el mismo nombre pero funcionalidad diferente, incluidos los métodos constructores.
Es la capacidad de algunos lenguajes de programación (como C++) de redefinir algunas operaciones matemáticas. Por ejemplo, la operación matemática (+) puede significar una cosa diferente si se suman números o si se suman dos objetos de una determinada clase. En PHP esto no es posible.
Salvador Romero Villegas (Elaboración Propia)(CC BY-SA)
Una clase es, como sabrás, un molde a través del cual podemos crear objetos. El concepto de clase como tal es un mecanismo del lenguaje de programación que nos va a permitir definir que elementos va a tener los objetos de un tipo dado. Por ejemplo, los objetos tipo Producto tendrán una serie de atributos concretos y una serie de métodos pensados para trabajar con los atributos que tiene un producto.
Para crear una clase en PHP lo más adecuado es comenzar creando un archivo .php del mismo nombre que tendría la clase. Esto no es obligatorio en PHP, donde podemos declarar clases en múltiples lugares, e incluso, múltiples clases en un mismo archivo .php. Sin embargo, es un aspecto recogido en la recomendación PSR-4 además de una estrategia recomendada. La idea es que si vamos a crear una clase llamada Producto, metamos su código en un archivo llamado Producto.php.
Una vez creado el archivo .php en cuestión, usaríamos la palabra reservada class para definir la clase:
<?php
class Producto {
}
La clase anterior no tiene definido ningún atributo. El siguiente paso sería agregarle atributos. Para ello debemos usar las siguientes palabras reservadas seguidas del nombre del atributo:
var $atributo: estilo de creación de atributos propio de PHP 4 cuyo uso está en detrimento. Es equivalente a usar public.
public $atributo: define un atributo que es accesible desde código exterior a la clase.
private $atributo: define un atributo que no es accesible desde código exterior a esta clase. Un intento de acceso a este atributo provocaría un error fatal que terminaría la ejecución del código.
protected $atributo: define un atributo que no es accesible desde código exterior a esta clase, excepto clases que extiendan esta clase (lo que normalmente se conoce como clases hija en términos de herencia).
Una vez explicado lo anterior, veamos un ejemplo de clase:
<?php
class Producto {
private $id;
protected $cod;
var $precio;
public $nombre;
}
En el ejemplo anterior los atributos $id y $cod no serían accesible externamente, mientras que $precio y $nombre si. Veamos un ejemplo de como podríamos usar la clase Producto anterior. En primer lugar empecemos por la creación de la instancia. Para crear una instancia de la clase Producto usamos la palabra reservada new:
<?php
require_once 'Producto.php'; //Incluimos el archivo donde se define la clase
$producto=new Producto(); //Creación de una instancia de la clase
Después, para hacer uso de los atributos (y también de los métodos como veremos más adelante) haremos uso del operador flecha (->):
<?php
require_once 'Producto.php'; //Incluimos el archivo donde se define la clase
$producto=new Producto(); //Creación de una instancia de la clase
$producto->nombre='Macetero Gris'; //Asignación de un valor al atributo
echo $producto->nombre; //Uso del valor de un atributo
Pero, ¡cuidado! Si intentas modificar un atributo privado o protegido, al ser código externo a la clase, obtendrás un mensaje de error y se detendrá la ejecución del código:
<?php
require_once 'Producto.php'; //Incluimos el archivo donde se define la clase
$producto=new Producto(); //Creación de una instancia de la clase
$producto->id=4; //Uso indebido de un atributo privado
/* Generará un error del estilo siguiente:¨
PHP Fatal error: Uncaught Error: Cannot access private property Producto::$id
*/
A partir de PHP 7.4 puede indicarse el tipo en el atributo. Los tipos básicos en PHP son:
Nombre de una clase existente o de creación propia, como por ejemplo: PDO, Producto, etc.
mixed (cualquier tipo, existe a partir de PHP 8.0)
int (valor entero)
bool (valor booleano)
float (valor número en coma flotante)
string (valor cadena de texto)
array
object
A continuación tienes un ejemplo de su uso:
class Producto {
private int $id;
protected string $cod;
var float $precio;
public ?string $nombre;
}
El uso de ? antes del tipo permite indicar que ese atributo puede tomar el valor null, como es el caso de public ?string $nombre;. En caso de que se intente asignar un valor inadecuado se producirá un Fatal Error y se abortará la ejecución del código.
Stockbyte. (CD-DVD Num. IE008.)( Uso educativo no comercial para plataformas públicas de Formación Profesional a distancia.)
Los métodos en las clases son un mecanismo para acceder o modificar el estado de un objeto. Esto significa, en la práctica, poder acceder a los atributos y trabajar con ellos atendiendo a la lógica con la que debe funcionar la clase y la relación entre sus atributos.
Los atributos públicos son accesibles externamente, pero los atributos protegidos y privados no. Lo más habitual es que los atributos sean privados, de tal forma que una modificación externa de los atributos no haga que el objeto tenga un estado inconsistente. A través de los métodos establecemos las reglas que rigen el funcionamiento interno de un objeto de forma que no llegue a tener un estado inconsistente. Imagina que en un momento dado, alguien escribe un código como el siguiente:
<?php
class Producto {
private $id;
protected $cod;
var $precio;
public $nombre;
}
$precioDeForm=filter_input(INPUT_POST,'precio',FILTER_VALIDATE_FLOAT);
if ($precioDeForm!==false && $precioDeForm!==null)
{
$producto=new Producto();
$producto->precio=$precioDeForm;
}
Aparentemente el ejemplo es correcto, pero, ¿qué ocurre si a través del método POST se recibe un precio negativo? La respuesta es que se establecería un precio negativo para el producto, dando lugar a un estado inconsistente. Lo más correcto hubiera sido marcar los atributos como privados y crear un método que permitiera establecer el precio y controlar a su vez que tiene un valor adecuado. La sintaxis básica para crear un método en una clase PHP es la siguiente:
class NombreClase {
public|private|protected function nombreMetodo ($param1, $param2, ...)
{
}
}
Al igual que ocurría con los atributos de una clase, los métodos pueden tener visibilidad pública (public), privada (private) y protegida (protected), indicaríamos solo una de ellas obviamente. Esto hará que un método pueda ser usado desde el exterior si es público, solo desde la misma clase donde está el método si es privado, o desde otras clases que extienden esta clase si es protegido. Veamos un ejemplo de método que permitiría establecer el valor del precio controlando que es correcto:
class Producto {
private $id;
private $cod;
private $precio;
private $nombre;
public function setPrecio($precio)
{
$precio=round($precio,2);
$precioCorrecto=$precio>0;
if ($precioCorrecto) {
$this->precio=$precio;
}
return $precioCorrecto;
}
}
En el código anterior es importante que te fijes bien en el fragmento de código $this->precio=$precio;, dado que es la forma de acceder o modificar un atributo desde un método definido dentro de la clase.
$this es una pseudovariable que contiene una autoreferencia a la instancia de la clase (también denominado contexto). Usamos $this en los métodos que creamos dentro de la clase para acceder a los atributos y métodos de la instancia.
Imagina ahora que quieres que el método setPrecio acepte una cadena de texto con el precio, que pueda tener el siguiente formato: 3.44€ o 3,44€ o simplemente 3,44. ¿Cómo lo harías? Podríamos modificar el método setPrecio añadiendo en él todo el código necesario, pero tenemos otra alternativa que hará nuestro código más manejable y fácil de entender: implementar un método privado que realice la conversión de la cadena que contiene el precio a número flotante, dividiendo el código en diferentes métodos. De esa forma simplificamos la complejidad del código:
class Producto {
private $id;
private $cod;
private $precio;
private $nombre;
public function setPrecio($precio)
{
if (is_string($precio))
$precio=$this->precioDesdeCadena($precio);
$precio=round($precio,2);
$precioCorrecto=$precio>0;
if ($precioCorrecto) {
$this->precio=$precio;
}
return $precioCorrecto;
}
private function precioDesdeCadena($precio)
{
if (preg_match('/^[0-9]+(?:(?:\.|,)[0-9]+)?€?$/',$precio))
{
return floatval(str_replace(',','.',$precio));
}
else
return 0;
}
}
En el ejemplo anterior es importante que te fijes en como se invoca el método precioDesdeCadena usando $this: $precio=$this->precioDesdeCadena($precio);. Como se comentó antes, usaremos $this para acceder tanto a atributos como a métodos de la clase.
En la programación orientada a objetos es habitual usar métodos denominados getters y setters. Estos son métodos que siguen el nombrado getAtributo o setAtributo (donde Atributo sería el nombre del atributo en sí). Estos métodos están dirigidos a obtener el valor del atributo (get) o establecer el valor (set) de atributos privados o protegidos, controlando así en todo momento el valor que toma dicho atributo para evitar un estado inconsistente.
En PHP5 se introdujeron los llamados métodos mágicos, entre ellos __set y __get. Si se declaran estos dos métodos en una clase, PHP los invoca automáticamente cuando desde un objeto se intenta usar un atributo no existente o no accesible. Por ejemplo, el código siguiente simula que la clase Producto tiene cualquier atributo que queramos usar.
<?php
class Producto {
private $atributos = array();
public function __get($atributo) {
return $this->atributos[$atributo];
}
public function __set($atributo, $valor) {
$this->atributos[$atributo] = $valor;
}
}
$producto=new Producto();
$producto->iva=0.21;
echo $producto->iva;
Los métodos mágicos __get y __set hay que usarlos con sumo cuidado y solo en situaciones excepcionales, si puedes evitarlo intenta no usarlos. En la documentación de PHP tienes más información sobre los métodos mágicos.
En las versiones actuales de PHP (a partir de PHP 5) se puede declarar el tipo de dato de un parámetro de una función o de un método.
class Producto {
private $cod;
private $precio;
private $nombre;
private $descripcion;
private $fechaAlta;
public function setFechaAlta(DateTime $fechaAlta)
{
$this->fechaAlta=$fechaAlta;
}
public function setCod (string $cod)
{
$this->cod = $cod;
}
}
Fíjate que el parámetro $cod del método setCod se ha indicado el tipo string: public function setCod (string $cod). Y luego fíjate también en el parámetro $fechaAlta del método setFechaAlta, el cual se ha declarado del tipo DateTime. En el primer caso string es un tipo básico, y en el segundo caso DateTime es una clase que forma parte del núcleo de PHP (Información sobre la clase DateTime ). Los tipos disponibles en PHP que podemos usar como parámetro son los siguientes (fíjate que algunos tipos de datos están solo disponibles a partir de ciertas versiones de PHP):
nombre de clase: se espera que el parámetro contenga una instancia de una clase determinada, por ejemplo: DateTime $fecha.
interfaz: se espera que el parámetro contenga una instancia que implemente la interfaz dada.
self: se espera que el parámetro contenga una instancia de esta misma clase
int: se espera que el parámetro contenga un entero (desde PHP 7.0)
float: se espera que el parámetro contenga un número flotante (desde PHP 7.0)
string: se espera que el parámetro contenga una cadena (desde PHP 7.0)
bool: se espera que el parámetro contenga un valor booleano (desde PHP 7.0)
array: se espera que el parámetro contenga un array.
object: se espera que el parámetro contenga un objeto (desde PHP 7.2)
callable: se espera que el parámetro contenga una función o método.
A partir de PHP 7.0 aquellos atributos tipados cuyo valor pueda ser también null puedes marcarlos añadiendo ? delante del tipo. Por ejemplo, si quieres que el parámetro $fechaAlta, de la función setFechaAlta pueda ser también null, puedes escribir lo siguiente:
public function setFechaAlta(?DateTime $fechaAlta)
{
$this->fechaAlta=$fechaAlta;
}
A partir de PHP 8.0 puedes también usar la unión de tipos. Por ejemplo, podríamos indicar que la función anterior aceptaría varios tipos de datos para el argumento $fechaAlta de la siguiente forma:
public function setFechaAlta(DateTime|string|null $fechaAlta)
{
...
}
En el caso anterior, la función aceptaría también string como tipo de dato para el argumento (aparte de DateTime y null), lo cual requeriría un procesamiento extra.
Por último, señalar que a partir de PHP 7.0 es también posible indicar el tipo de dato que retornará una función o método usando la siguiente sintaxis:
<?
class Producto {
/* ... */
private $fechaAlta;
public function getFechaAlta():DateTime
{
return $this->fechaAlta;
}
}
En el caso anterior podemos también usar el operador ? para indicar que el valor retornado puede ser null: public function getFechaAlta():?DateTime, y a partir de PHP 8 podemos también usar el operador union de tipos para indicar que un método puede retornar datos de diferentes tipos, por ejemplo: public function getFechaAlta():DateTime | null.
En PHP es posible indicar que un parámetro de un método puede tener un valor por defecto:
<?php
class Producto {
private $cod;
private $precio;
private $nombre;
//Resto del código de la clase
function modificarPrecio ($porcentaje=0.05)
{
$this->precio=$this->precio+$this->precio*$porcentaje;
}
}
A la hora de invocarlo el argumento puede omitirse:
Una de las posibilidades de los métodos mágicos de PHP 5 es utilizar __call para capturar llamadas a métodos que no estén implementados en la clase. Entonces, en función del nombre del método y del número de parámetros que se pasen, se podrían realizar unas acciones u otras.
Para evitar código repetitivo como el anterior, se suelen utilizar constructores. Los constructores son un método especial que se invoca justo cuando se crea la instancia de la clase, permitiendo inicializar algunos atributos con un valor recibido por parámetro. En PHP crear un constructor es simplemente crear una función con el nombre especial __construct. Veamos un ejemplo:
class Producto {
private $cod;
private $precio;
private $nombre;
/* ... resto de atributos ... */
public function __construct($cod, $nombre, $precio)
{
$this->cod=$cod;
$this->precio=$precio;
$this->nombre=$nombre;
}
}
De esta forma, al crear la instancia de producto le podremos indicar los valores con los que se inicializan algunos atributos:
$p = new Producto('A10E0012', 'Switch A10', 10.40);
En PHP a diferencia de otros lenguajes de programación solo puede haber un constructor por clase. Un mecanismo para aliviar este problema puede ser usar valores por defecto en los atributos. Por ejemplo:
class Producto {
private $cod;
private $precio;
private $nombre;
public function __construct($cod, $nombre=null, $precio=50)
{
$this->cod=$cod;
$this->precio=$precio;
//Si $nombre es null lo rellenamos con el código de producto
$this->nombre=$nombre??$cod; //Operador de coalescencia nula (PHP >=7.0)
}
}
A partir de PHP 8.0 es posible nombrar los parámetros al realizar una llamada a una función o método, seleccionando solo aquellos argumentos que nos interesen. Esto es especialmente útil en constructores con muchos argumentos:
$p = new Producto('A10E0012', precio: 10.40);
En el ejemplo anterior estamos indicando que se usen todos los valores por defecto ($nombre=null) excepto para el parámetro $precio (fíjate que en la invocación no se pone el símbolo de $).
También a partir de PHP 8.0 es posible indicar la creación de un atributo en el mismo constructor:
class Producto {
//Omitimos la declaración de los atributos $cod, $nombre y $precio, aunque podemos declarar otros si es necesario
public function __construct(public string $cod, public ?string $nombre=null, public float $precio=50)
{
//Si $this->nombre es null lo rellenamos con el código de producto
if (is_null($this->nombre)) $this->nombre=$cod;
}
}
El código anterior (solo válido para PHP 8.0 o superior) en el mismo constructor se indican algunos o todos los atributos de la clase a crear. Además, se les asigna automáticamente el valor que reciben como parámetro, con lo que no es necesario código del estilo $this->cod=$cod; como en ejemplos anteriores. Solo incluiríamos código para tratar situaciones excepcionales como la mostrada en el ejemplo.
En PHP es posible definir un método destructor, es decir, un método que se invocará cuando la instancia de la clase sea destruida, bien a través de unset o bien a través del recolector de basura. Para construir este método debe crearse un método llamado __destruct sin argumentos. Generalmente se utiliza para liberar recursos, tales como archivos que podamos tener abiertos o conexiones a la base de datos.
En el siguiente ejemplo se ilustra el uso del método destructor en la lectura de un archivo de texto donde se reemplazan los saltos de línea por etiquetas <br>:
<?php
class LectorArchivo
{
private $fp=null;
public function __construct(string $nombreArchivo)
{
$this->fp=fopen($nombreArchivo,'r');
if ($this->fp===false) throw new Exception('No se pudo abrir el recurso');
}
public function leerLinea()
{
$line=fgets($this->fp);
if ($line!==false) echo nl2br($line);
return $line!==false;
}
public function __destruct()
{
fclose($this->fp);
}
}
try {
$lector=new LectorArchivo('ejemplodearchivo.txt');
while ($lector->leerLinea());
unset($lector);
} catch (Exception $e) {
echo $e->getMessage();
}
El código anterior realiza unset($lector); y al realizar dicha operación cerrará el archivo. Si por algún casual olvidamos hacer el unset, no pasa nada, cuando termine la ejecución del script se borrará la instancia de la clase y se cerrará el archivo de forma segura.
Los métodos constructores también existen en PHP 4, pero en lugar de llamarse __construct, se deben llamar del mismo modo que la clase. Los métodos destructores son nuevos en PHP 5; no existían en versiones anteriores del lenguaje.
Stockbyte (CD-DVD Num. IE008.)(Uso educativo no comercial para plataformas públicas de Formación Profesional a distancia.)
Al igual que ocurre en otros lenguajes de programación, en las clases que creamos en PHP podemos también añadir atributos estáticos, también denominados atributos de clase. Estos atributos tienen la peculiaridad de que su valor no está asociado a una instancia concreta, sino que su valor está asociado a la clase en sí. No es por tanto necesario crear una instancia de la clase para acceder al valor de un atributo estático. Para crear un atributo estático usamos la palabra reservada static, fíjate en el siguiente ejemplo:
<?php
class DB {
public static PDO $dbconn=null;
}
El ejemplo anterior crearía un atributo estático llamado $dbconn de tipo público, es decir, que sería accesible desde fuera de la clase. Para poder hacer uso de dicho atributo estático deberíamos hacer uso del operador de resolución de ámbito (::). Por ejemplo:
Un atributo estático podría ser también privado (private), de forma que solo fuera accesible desde dentro de la misma clase, por ejemplo:
<?php
class DB {
private static PDO $dbconn=null;
}
En este caso, el atributo estático solo es accesible desde el interior de la clase donde ha sido definido. Esto es especialmente útil cuando tenemos también métodos estáticos que hacen uso de esos atributos estáticos. Los métodos estáticos, también llamados métodos de clase, son métodos que podemos usarlos sin necesidad de crear una instancia de la clase. Para definirlos debemos usar la palabra reservada static:
class DB {
private static $dbconn=null;
public static function conectar()
{
$dbdriver=DB_DRIVER;
$dbname=DB_NAME;
$user=DB_USER;
$password=DB_PASSWORD;
$host=DB_HOST;
$port=DB_PORT;
if (is_null(DB::$dbconn))
{
DB::$dbconn=new PDO("{$dbdriver}:host={$host};dbname={$dbname};port={$port}",$user,$password);
DB::$dbconn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
}
return DB::$dbconn;
}
}
En el código anterior el método conectar hace uso del atributo estático DB::$dbconn para almacenar la conexión a la base de datos. Si se invoca dos veces el método conectar no se crea una nueva conexión, sino que se reutiliza la conexión existente, haciendo nuestro código más eficiente. Nota: DB_DRIVER, DB_NAME, etc. serían constantes definidas previamente con define.
En el resto de nuestro código para usar el método estático conectar anterior se necesitaría también hacer uso del operador de resolución de ámbito (::):
<?php
require_once 'DB.php';
$pdo=DB::conectar();
Si quisiéramos hacer uso ahora del método conectar desde dentro del a misma clase DB, podemos también hacer uso del nombre de la clase (por ejemplo, DB::conectar()) pero también de las palabras reservadas static o self cuyo uso se verá en la sección de herencia:
class DB {
private static $dbconn=null;
public static function conectar()
{
//... código de la función conectar...
}
public static function obtenerTablas()
{
$pdo=static::conectar();
//$pdo=DB::conectar(); //Alternativa válida
try {
$stmt=$pdo->query("SHOW TABLES;");
return $stmt?$stmt->fetchAll(PDO::FETCH_COLUMN):$stmt;
}
catch (PDOException $e)
{
return -1;
}
}
}
Es importante que tengas en cuenta que un método de clase o estático no va a poder acceder a atributos de instancia, dado que $this en si mismo no estará disponible en métodos estáticos al no haber una instancia asociada. Por tanto, código como el siguiente sería erróneo:
<?php
class Ejemplo {
private $i=0;
public static function incrementar()
{
$this->i++;//ERROR
}
}
Sin embargo, el método estático si podría acceder al valor del atributo de una instancia de la misma clase creada dentro del método o pasada por parámetro:
<?php
class Ejemplo {
private $i=0;
public static function incrementar(Ejemplo $e=null)
{
if ($e==null) $e= new Ejemplo();
$e->i++; //CORRECTO
return $e;
}
}
Además de métodos y propiedades, en una clase también se pueden definir constantes, utilizando la palabra const. Es importante que no confundas los atributos con las constantes. Son conceptos distintos: las constantes no pueden cambiar su valor (obviamente, de ahí su nombre), no usan el carácter $ y, además, su valor está asociado a la clase, es decir, no existe una copia del mismo en cada objeto. Por tanto, para acceder a las constantes de una clase, se debe utilizar el nombre de la clase y el operador de resolución de ámbito ::. Veamos un ejemplo de su uso:
class DB {
private static $dbconn=null;
const DEFAULT_DB_DRIVER='mysql';
const DEFAULT_DB_HOST='localhost';
const DEFAULT_DB_PORT='3306';
public static function conectar()
{
//... código de la función conectar ...
}
}
Es importante resaltar que no es necesario que exista ninguna instancia de una clase para poder acceder al valor de las constantes definidas en la clase. Además, es una buena práctica definir el nombres de las constantes en mayúsculas (por ejemplo: DEFAULT_DB_DRIVER).
A continuación tienes un ejemplo de uso de las constantes, donde como puedes observar hay que usar el operador de resolución de ámbito (líneas 13, 15, y 16):
class DB {
private static $dbconn=null;
const DEFAULT_DB_DRIVER='mysql';
const DEFAULT_DB_HOST='localhost';
const DEFAULT_DB_PORT='3306';
public static function conectar()
{
if (defined('DB_DRIVER')) {
$dbdriver=DB_DRIVER;
}
else {
$dbdriver=DB::DEFAULT_DB_DRIVER;
}
$host=defined('DB_HOST') ? DB_HOST : DB::DEFAULT_DB_HOST;
$port=defined('DB_PORT') ? DB_PORT : DB::DEFAULT_DB_PORT;
$dbname=DB_NAME;
$user=DB_USER;
$password=DB_PASSWORD;
if (is_null(DB::$dbconn))
{
try {
DB::$dbconn=new PDO("{$dbdriver}:host={$host};dbname={$dbname};port={$port}",$user,$password);
DB::$dbconn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
}
catch (PDOException $e)
{
DB::$dbconn=null;
}
}
return DB::$dbconn;
}
}
En el código anterior se usarán las constantes DB::DEFAULT_DB_DRIVER, DB::DEFAULT_DB_HOST y DB::DEFAULT_DB_PORT, cuando DB_DRIVER, DB_HOST o DB_PORT no hayan sido definidas.
¿Para qué usarías un atributo estático o un método estático?
Los atributos estáticos de una clase se utilizan para guardar información general sobre la misma, como puede ser el número de instancias de la clase que se han creado o un valor reutilizable desde muchos puntos del código. Ten en cuenta que los atributos estático solo tienen un valor y este se almacena a nivel de clase.
Los métodos estáticos suelen realizar alguna tarea específica independiente de cualquier instancia, como por ejemplo realizar un cálculo matemático o construir una instancia de una clase. Por ejemplo, las clases matemáticas suelen tener métodos estáticos para realizar logaritmos, raíces cuadradas, cálculos vectoriales y matriciales, etc. No tiene sentido crear una instancia de la clase para operaciones genéricas donde todos los datos pueden ser proporcionados a través de parámetros.
Antes de crear un método estático piensa con calma si realmente es un método que implementa una función genérica de la clase o si es un método que necesita trabajar con datos almacenados dentro de cada instancia.
En determinadas ocasiones es necesario usar un objeto para encapsular datos, ya sea por comodidad o por requerimientos de la aplicación. Imagina que tienes un método que recibe un objeto con los datos necesarios para hacer una operación concreta, como por ejemplo, modificar los datos de un producto:
class Producto
{
private $id;
private $cod;
private $precio;
private $nombre;
public function modificar ($nuevosDatos)
{
if (isset($nuevosDatos->cod))
$this->cod=$nuevosDatos->cod;
if (isset($nuevosDatos->precio))
$this->precio=$nuevosDatos->precio;
if (isset($nuevosDatos->nombre))
$this->nombre=$nuevosDatos->nombre;
}
}
El ejemplo anterior muestra un método que recibe una instancia de una clase con datos para modificar el producto. Esa instancia podría ser otra instancia de Producto, pero también un objeto creado a medida, para proporcionar aquellos datos que solo sea necesario modificar. Para crear ese objeto ad-hoc podemos crear una instancia de la clase stdClass, lo que permitirá crear un objeto al cual podemos agregar atributos a demanda:
En el ejemplo anterior se puede ver como se añaden los atributos precio y nombre sin necesidad de definirlos antes como ocurre con una clase normal. Esto permitiría por ejemplo hacer uso del método anterior modificar, el cual necesita una instancia de un objeto, sin necesidad de volver a crear una instancia de la clase Producto.
A partir de PHP 7.0 es posible crear instancias de clases anónimas, es decir, clases sin nombre que solo tienen una instancia. Es algo parecido al uso de stdClass pero que permite hacer uso de otras características de la programación orientada a objetos en PHP. Crear una instancia de una clase anónima es tan fácil como hacer lo siguiente:
$datos=new class {
public $precio;
public $nombre;
};
$datos->precio=20;
$datos->nombre='Switch A02X41';
$producto=new Producto();
$producto->modificar($datos);
A diferencia de stdClass en una clase anónima como la anterior podemos definir atributos, métodos y usar otras características de la orientación a objetos. Por ejemplo:
$datos=new class {
public $precio;
public $nombre;
public function incrementarPrecio($incremento)
{
$this->precio+=$incremento;
}
};
$datos->precio=20;
$datos->nombre='Switch A02X41';
$datos->incrementarPrecio(5);
Stockbyte (CD-DVD Num. IE008.)(Uso educativo no comercial para plataformas públicas de Formación Profesional a distancia.)
La herencia es un mecanismo de la POO que nos permite aprovechar clases existentes para crear clases nuevas que amplían o modifican el comportamiento de la clase heredada. La herencia permite conformar una jerarquía de clases que va desde clases más genéricas (clase base o superclase) a clases más específicas (subclases).
Un ejemplo claro de esto lo tenemos cuando pensamos en el concepto de producto. Un producto, como tal, es un algo genérico y si miras en cualquier página de venta de productos informáticos podrás ver que existen distintos tipos de productos. En una tienda de informática por ejemplo diferenciar entre productos físicos (algo que se puede transportar) y productos no físicos (como por ejemplo una subscripción o un producto software). En cualquier caso se trata de productos con características diferentes.
Salvador Romero Villegas (Elaboración Propia)(CC BY-SA)
El mecanismo para implementar la herencia en PHP es extendiendo el comportamiento de la clase base en las subclases, por ello se usa la palabra reservada extends para indicar que una clase determinada amplia o modifica el comportamiento de una clase base. En PHP solo se puede extender una única clase, dado que la herencia múltiple no está soportada. En el siguiente ejemplo la clase Software extiende la clase Producto original.
Al implementar la herencia la subclase contendrá los atributos y métodos de la clase base, si creamos una instancia de la clase Software esta también contendrá los atributos y métodos de la clase Software, y podrán ser accedidos y manipulados dependiendo de la visibilidad, como se verá en los siguientes apartados. Fíjate en el siguiente ejemplo:
Clase base Producto.
<?php
class Producto {
private $cod;
private $precio;
private $nombre;
private $descripcion;
public function setNombre($nombre)
{
$this->nombre=$nombre;
}
public function getNombre()
{
return $this->nombre;
}
}
Subclase Software.
<?php
require_once 'Producto.php';
class Software extends Producto {
private $peso;
private $sistemaOperativo;
public function setPeso($peso)
{
$this->peso = $peso;
}
public function getPeso()
{
return $this->peso;
}
}
Ejemplo de uso
<?php
require_once 'Software.php';
$s = new Software();
$s->setNombre('Ubuntu Linux 22.04');
$s->setPeso('3.6 GB');
echo $s->getNombre(); //Definido en clase Producto
echo "<br>";
echo $s->getPeso(); //Definido en clase Software<br />
Es importante que recuerdes que en PHP se puede sobrescribir un método siempre que no se cambie el número de parámetros haciendo que tenga un comportamiento diferente:
class A {
public function methodA($param1) { echo "1"; }
}
class B extends A {
public function methodA($param1) { echo "2"; } //CORRECTO
}
En el ejemplo anterior el método methodA de la clase B es un método sobrescrito con respecto a la implementación del mismo en la clase A.
Una vez conocido como se implementa la herencia en PHP es importante revisar un concepto fundamental en la programación orientada a objetos. Este concepto es el concepto de polimorfismo.
En PHP como ocurre en otros lenguajes, podemos hacer uso del polimorfismo de inclusión. En este tipo de polimorfismo un atributo o parámetro que contenga una clase concreta puede también almacenar instancias de cualquiera de sus subclases. Por ejemplo:
Ejemplo de clase que usa la clase Producto.
En el siguiente código el atributo $p y el constructor indican expresamente que se debe usar una instancia de la clase Producto.
class LineaDePedido
{
private Producto $p;
private $unidades;
public function __construct (Producto $p, $unidades)
{
$this->p=$p;
$this->unidades=$unidades;
}
}
Ejemplo de uso 1.
En el siguiente ejemplo se usa la clase Producto como argumento del constructor de la clase LineaDePedido. El funcionamiento es el esperado, dado que $p es una instancia de Producto.
A diferencia del ejemplo anterior, en el siguiente ejemplo se usa la clase Software como argumento del constructor de la clase LineaDePedido. Este ejemplo también funciona, dado que la clase Software es una subclase de la clase Producto.
En el ejemplo anterior es importante que tengas en cuenta que el atributo $p de la clase LineaDePedido en el primer ejemplo almacenará una instancia de Producto, pero en el segundo caso almacenará una instancia de Software.
En el siguiente código se ha agregado el método mostrarPeso que hará uso del método getPeso solo definido en la clase Software. A pesar de que para almacenar el producto usaremos la clase base Producto, podemos acceder a dicho método cuando contiene una instancia de la clase Software:
class LineaDePedido
{
private Producto $p;
private $unidades;
public function __construct (Producto $p, $unidades)
{
$this->p=$p;
$this->unidades=$unidades;
}
public function mostrarPeso()
{
if ($this->p instanceof Software){
echo $this->p->getPeso();
}
}
}
En el método mostrarPeso se usa el operador instanceof para comprobar la clase o subclase que está almacenando el atributo $p. No siempre hay que hacer este tipo de operaciones, pero son sumamente útiles.
Otro hecho que debes observar es que en PHP, aunque existe la operación de conversión de tipos (type casting), no permite convertir entre clases definidas por el usuario, es decir, no se puede hacer lo siguiente:
if ($this->p instanceof Software){
$s=(Software)$this->p; //ERROR
echo $s->getPeso();
}
A continuación puedes ver un código que usaría el método mostrarPeso antes definido:
$s = new Software();
$s->setNombre('Ubuntu Linux 22.04');
$s->setPeso('3.6 GB');
$lineaPedido=new LineaDePedido($s,4);
$lineaPedido->mostrarPeso();<br /><br />
Como puedes ver, para definir una clase que herede de otra simplemente tienes que utilizar la palabra extends indicando la clase base o superclase. Los nuevos objetos que se instancien a partir de la subclase son también objetos de la clase base. Además, la clase que contiene un atributo, variable o parámetro se puede comprobar utilizando el operador instanceof.
En la siguiente tabla tiene también otras funciones que te serán útiles a la hora de determinar la relación de herencia entre clases:
Funciones de utilidad en la herencia en PHP 5
Función
Ejemplo
Significado
get_parent_class
echo "La clase padre es: " . get_parent_class($t);
Devuelve el nombre de la clase padre del objeto o la clase que se indica.
is_subclass_of
if (is_subclass_of($t, 'Producto') {
...
}
Devuelve true si el objeto o la clase del primer parámetro, tiene como clase base a la que se indica en el segundo parámetro, o false en caso contrario.
PHP admite sobrecarga de métodos y funciones a través de los métodos mágicos __call, __get y __set, pero no se puede implementar como en otros lenguajes. Una situación como la siguiente daría error:
class A {
public function methodA($param1) { }
public function methodA($param1, $param2) { } //ERROR: ya existe el método
}
En el caso de la herencia ocurre algo parecido, no se puede sobrecargar un método existente en otra clase:
class A {
public function methodA($param1) { }
}
class B extends A {
public function methodA($param1, $param2) { } //ERROR: ya existe el método
}
No obtante, recuerda que si está permitido sobrescribir un método siempre que no cambie el número de parámetros (sobreescribir sin sobrecarga):
class A {
public function methodA($param1) { }
}
class B extends A {
public function methodA($param1) { } //CORRECTO: no cambia el número de parámetros
}
Stockbyte (CD-DVD Num. IE008.)(Uso educativo no comercial para plataformas públicas de Formación Profesional a distancia.)
Como ya sabes, cuando creamos una nueva clase que hereda otra, la nueva clase hereda todos los atributos y métodos de la clase base. Sin embargo, existen ciertas limitaciones: la nueva clase no tendrá acceso directo a los atributos y métodos privados de la clase base. Imagina que implementas la clase Suscripcion comentada en apartados anteriores. La clase Suscripcion podría contener un producto como podría ser la suscripción a un servicio de mantenimiento de impresoras o a una publicación, por lo que podría ser de la siguiente forma:
class Producto {
private $cod;
private $precio;
private $nombre;
private $descripcion;
}
class Suscripcion extends Producto {
private $duracion; //Descripción de la suscripción en semanas
private $periodicidad; //quincenal, mensual, semestral, ... etc.
}
En el ejemplo anterior, el precio almacenado en la clase Producto podría ser el coste por cada semana de suscripción, así a mayor duración de la suscripción mayor coste. Esto implicaría realizar un método que permitiera calcular el precio de la suscripción dentro de la clase Suscripcion, podría ser un código como el siguiente:
class Suscripcion extends Producto {
private $duracion;
private $periodicidad;
public function getPrecio()
{
return $this->precio*$this->duracion; //ERROR $this->precio es privado
}
}
El código anterior sería problemático, porque $this->precio es privado en la clase Producto y por lo tanto, no es accesible desde la clase Suscripcion. Para resolver esto tenemos varias soluciones, pero de momento vamos a ver dos:
Poner el atributo precio de la clase Producto como público (public $precio). Esto puede suponer un problema, dado que la idea de poder ocultar los datos para que no sean manipulados fuera de la clase se contraviene.
Poner el atributo precio como protected. Esto permitiría que subclases pudieran acceder al atributo y a su vez que no fuera modificado desde otras partes del código. Esta es normalmente la mejor solución para estas situaciones.
Con la siguiente modificación en la clase Producto el ejemplo anterior no daría error:
El concepto de visibilidad pública (public), privada (private) o protegida (protected), donde la visibilidad se va limitando en diferentes escenarios, es también aplicable a métodos de instancia, fíjate en el siguiente ejemplo:
Ejemplo de métodos con visibilidad limitada y su efecto en la herencia
Situación
Ejemplo de clase con método con visibilidad limitada
Ejemplo de clase que usa el método con visibilidad limitada
Cuando el método azar es privado la función elegir de la clase B en no podrá usar dicho método, generando un error en tiempo de ejecución:
Fatal error: Uncaught Error: Call to private method A::azar() from scope B
class A
{
private function azar($max) //método azar privado
{
return random_int(1,$max);
}
}
class B extends A {
public function elegir ($n,$max)
{
$numeros=[];
if ($n>$max || $n<=0) return false;
while (count($numeros)<$n)
{
$v=$this->azar($max);
if (!in_array($v,$numeros)) $numeros[]=$v;
}
}
}
Cuando el método azar es protected la función elegir de la clase B puede acceder a dicho método sin problemas.
class A
{
protected function azar($max) //método azar protected
{
return random_int(1,$max);
}
}
Los métodos y atributos estáticos también son heredados y están sujetos a la visibilidad. Fíjate en el siguiente ejemplo:
class C {
protected static $i=10;
}
class D extends C {
public static function mostrar ()
{
echo D::$i;
}
}
D::mostrar();
La clase D puede acceder al atributo estático $i como si fuera propio debido a que en la clase C es protected. Lo mismo ocurre con los métodos estáticos.
Stockbyte (CD-DVD Num. IE008.)(Uso educativo no comercial para plataformas públicas de Formación Profesional a distancia.)
En múltiples ocasiones, cuando estamos haciendo uso de la herencia, es necesario usar métodos que están implementados en la clase base. En el apartado anterior vimos como se podía hacer uso de protected para acceder al atributo precio de la clase base Producto, pero existen otras soluciones, como por ejemplo usar los métodos de la superclase. Imagina que en la clase Producto tienes ya implementada una lógica para controlar el precio que no quieres que se altere, y por ello, decides mantener el precio como privado:
class Producto {
private $cod;
private $precio;
protected $nombre;
protected $descripcion;
public function getPrecio()
{
return $this->precio;
}
public function setPrecio($precio)
{
if (is_numeric($precio) && $precio>0)
$this->precio=$precio;
else throw new Exception("El precio no es válido.");
}
}
Si recuerdas el apartado anterior, queríamos implementar el método getPrecio en la clase Suscripcion de forma que se calculara el precio en función del número de semanas de la suscripción, de forma que a más semanas el precio fuera mayor. Si ahora queremos implementar el método getPrecio en la clase Suscripcion solo nos queda una opción y es usar el método getPrecio de la clase Producto. Para poder hacer referencia al método de la clase base es necesario usar la palabra reservada parent:
class Suscripcion extends Producto {
private $duracion=4;
private $periodicidad;
public function getPrecio()
{
return parent::getPrecio() * $this->duracion;
}
}
Con la palabra reservada parent podemos indicar que queremos invocar al método de la clase base (Producto), dado que si ponemos $this->getPrecio(), se hará referencia al método getPrecio de la clase instanciada (que en este caso es de tipo Suscripcion). De hecho, $this cuando hay un método sobrescrito, siempre hará referencia a la clase instanciada. Fíjate en el siguiente ejemplo:
Ejemplo de impacto de tener un método sobrescrito en otros métodos de la clase base.
Ejemplo de clase base
Ejemplo de clase que hereda
Ejemplo de uso
classProducto{private$cod;private$precio;protected$nombre;protected$descripcion;publicfunctiongetPrecio(){return$this->precio;}//... resto del código ...publicfunctionprecioMenorDe($p){return$this->getPrecio()<$p;}}
class Suscripcion extends Producto {
private $duracion;
private $periodicidad;
public function getPrecio()
{
return parent::getPrecio() * $this->duracion;
}
}
En el siguiente ejemplo, dada la implementación del método precioMenorDe y a pesar de que está declarado en la clase Producto, en la línea 16 de la clase Producto se hará uso del método getPrecio de la clase Suscripcion, dado que $s contiene una instancia de Suscripcion y en Suscripcion está sobrescrito el método getPrecio:
$s=new Suscripcion();
$s->setPrecio(12);
$s->setDuracion(4);
echo '¿Es el precio menor de 15€? ';
echo $s->precioMenorDe(15)?'Si':'No';
El ejemplo anterior mostraría por tanto "No", dado que el precio calculado es 12 por 4 (32).
Recuerda: cuando se sobrescribe un método se usa siempre la versión de la clase instanciada y no la versión de la clase padre, y esto ocurre incluso en los métodos de la clase base.
Para evitar el efecto anterior, es decir, para hacer que desde la clase Producto se haga uso de su mismo método getPrecio y no la versión sobrescrita en la clase Suscripcion, puedes hacer uso de la palabra reservada self.
La palabra reservada self permite acceder a atributos estáticos y métodos de la propia clase ignorando cualquier sobreescritura de los mismos.
En el siguiente ejemplo se puede ver como el uso de self cambia completamente el resultado:
Ejemplo de impacto de usar self en la clase base.
Ejemplo de clase base
Ejemplo de clase que hereda
Ejemplo de uso
classProducto{private$cod;private$precio;protected$nombre;protected$descripcion;publicfunctiongetPrecio(){return$this->precio;}//... resto del código ...publicfunctionprecioMenorDe($p){return self::getPrecio()<$p;}}
class Suscripcion extends Producto {
private $duracion;
private $periodicidad;
public function getPrecio()
{
return parent::getPrecio() * $this->duracion;
}
}
En esta segunda ocasión, dado que en la implementación del método precioMenorDe se usa self:: para hacer referencia al método de esta propia clase (y no el sobrescrito), el resultado es completamente diferente:
$s=new Suscripcion();
$s->setPrecio(12);
$s->setDuracion(4);
echo '¿Es el precio menor de 15€? ';
echo $s->precioMenorDe(15)?'Si':'No';
El ejemplo anterior mostraría "Si".
Como has podido observar, el uso de self cambia totalmente el efecto en el código y eso es importante tenerlo en cuenta. A partir de la versión de PHP 5.3.0 podemos usar la forma static:: en contraposición de self:
public function precioMenorDe($p)
{
return static::getPrecio()<$p;
}
A partir de la versión 5.3.0 usar static:: en una clase equivale a acceder a los métodos propios de la instancia creada en caso de que se hayan sobrescrito, por lo que es equivalente al uso de $this a la hora de invocar un método.
El uso de static::, parent:: y self:: es también útil en el caso de atributos y métodos estáticos. Por ejemplo:
<?php
class A {
public static $n;
public static function setN($n)
{
self::$n=$n;
}
}
class B extends A
{
public static $n;
public static function setN($n)
{
parent::setN($n/2);
self::$n=$n;
}
}
B::setN(10);
echo '¿Cuál es el valor de $n en A?'.A::$n;
echo '<BR>';
echo '¿Cuál es el valor de $n en B?'.B::$n;
Si ejecutas el código anterior en un caso mostrará 10 y en otro 5, debido a que las clases A y B mantienen valores distintos para cada uno de sus estáticos $n.
En PHP los constructores son métodos especiales con respecto al resto de métodos, dado que a diferencia de los demás, cuando se sobrescriben si puede variar el número de parámetros:
class Producto {
private $cod;
private $precio;
protected $nombre;
protected $descripcion;
public function __construct($cod,$nombre,$precio)
{
// ... resto del código ...
}
}
class Suscripcion extends Producto {
private $duracion;
private $periodicidad;
public function __construct($cod,$nombre,$precio,$duracion,$periodicidad)
{
// ... resto del código ...
}
}
Al implementar los constructores de esta forma, ampliando los parámetros del constructor de la clase base al constructor de la subclase, podemos hacer uso del constructor de la clase base gracias a parent::. Fíjate en el siguiente ejemplo:
class Producto {
private $cod;
private $precio;
protected $nombre;
protected $descripcion;
public function __construct($cod,$nombre,$precio)
{
$this->cod=$cod;
$this->nombre=$nombre;
$this->precio=$precio;
}
}
class Suscripcion extends Producto {
private $duracion;
private $periodicidad;
public function __construct($cod,$nombre,$precio,$duracion,$periodicidad)
{
parent::__construct($cod,$nombre,$precio);
$this->duracion=$duracion;
$this->periodicidad=$periodicidad;
}
}
$s=new Suscripcion('A12MIK','Mantenimiento Impresoras',23,12,2);
En el constructor de la clase Suscripcion se invoca el constructor de la clase base (Producto) gracias al uso de parent::. No obstante, es importante que sepas que si en la subclase no existiera constructor, se mantendría el constructor de la clase base, por ejemplo:
class Producto {
private $cod;
private $precio;
protected $nombre;
protected $descripcion;
public function __construct($cod,$nombre,$precio)
{
$this->cod=$cod;
$this->nombre=$nombre;
$this->precio=$precio;
}
}
class Suscripcion extends Producto {
private $duracion;
private $periodicidad;
}
$s=new Suscripcion('A12MIK','Mantenimiento Impresoras',23); //Uso obligatorio del constructor de la clase Producto
En PHP, tal y como ocurre en otros lenguajes orientados a objetos podemos definir clases que:
No puedan ser heredadas (clases finales). Se utiliza la palabra reservada final.
No puedan ser instanciadas (clases abstractas) pero si pueden ser heredadas. Se utiliza la palabra reservada abstract.
Clases finales y clases abstractas.
Ejemplo de clase final
Ejemplo de clase abstracta
Las clases finales no pueden ser heredadas y están destinadas a implementar clases de las que no debe existir una subclase.
final class Software extends Producto {
private $peso;
private $sistemaOperativo;
/*... resto de código ...*/
}
Las clases abstractas se usan para crear clases que sirven de base para otras clases y que no tienen sentido por si solas al estar incompletas. Una clase abstracta no se puede instanciar.
abstract class Producto {
private $cod;
protected $precio;
protected $nombre;
protected $descripcion;
/*... resto del código ...*/
}
Las clases abstractas pueden tener métodos abstractos. Los métodos abstractos son métodos que deben implementarse obligatoriamente en las subclases y sirven para completar el comportamiento que no puede realizarse en la clase base. En los métodos abstractos se indica abstract y la firma del método:
abstract function tipoIva();
Veamos un ejemplo de su uso:
Uso de métodos abstractos
Ejemplo de clase final
Ejemplo de clase abstracta
Los métodos abstractos representan la promesa de que van a ser implementados en las subclases, e incluso podemos usarlos (como en el método getPrecioFinal del ejemplo), dado que serán obligatoriamente creados en cualquier subclase.
abstract class Producto {
private $cod;
protected $precio;
protected $nombre;
protected $descripcion;
abstract function tipoIva();
public function getPrecioFinal()
{
$pf=static::getPrecio()*(1+$this->tipoIva());
}
/*... resto de código ...*/
}
Las clases abstractas se usan para crear clases que sirven de base para otras clases y que no tienen sentido por si solas al estar incompletas. Una clase abstracta no se puede instanciar.
final class Software extends Producto {
private $peso;
private $sistemaOperativo;
public function tipoIva() {
return 0.21;
}
}
Por otro lado, cualquier clase (normal, abstracta o final) puede definir métodos que no pueden ser sobrescritos en subclases. Para ello, simplemente se declara el método anteponiendo la palabra reservada final:
abstract class Producto {
private $cod;
protected $precio;
protected $nombre;
protected $descripcion;
final public function getNombre()
{
return $this->nombre;
}
final public function setNombre($nombre)
{
$this->nombre=$nombre;
}
/*... resto de código ...*/
}
Nota: aunque está soportado no tiene mucho sentido poner métodos finales en clases finales. Es redundante, dado que la misma clase ya no puede ser heredada y por tanto sus métodos no pueden ser sobrescritos.
Obviamente, no se puede declarar una clase como abstract y final simultáneamente. abstract obliga a que se herede para que se pueda utilizar, mientras que final indica que no se podrá heredar.
Stockbyte (CD-DVD Num. IE008)(Uso educativo no comercial para plataformas públicas de Formación Profesional a distancia.)
Una interfaz (interface, en inglés) es como un contrato que una clase debe cumplir, pero en la práctica una interfaz consiste básicamente en una recopilación de métodos que la clase debe implementar obligatoriamente. Para declararla usamos la palabra reservada interface:
interface VolumenFisico {
const LADO_MAX=300;
public function setDimensiones ($ancho, $alto, $largo);
public function getDimensiones(): array;
public function pesoVolumetrico($factorDeCubitaje);
}
La interfaz anterior podría representar el volumen físico de un producto de cara a calcular el peso volumétrico, necesario para saber el coste del envío del producto. Como puedes observar, en la interfaz solo se especifica la firma de los métodos y no se incluye el cuerpo del mismo. La implementación del método se hará en la clase.
Adicionalmente, una interfaz no podrá tener atributos, pero si podremos definir en ella las constantes que nos interese.
La pregunta ahora es, ¿cómo implementamos una interfaz en una clase? Para indicar que una clase implementa una interfaz concreta hacemos uso de implements:
class Hardware extends Producto implements VolumenFisico {
private $dimensiones=['ancho'=>0,'alto'=>0,'largo'=>0];
public function setDimensiones($ancho,$alto,$largo) {
$this->dimensiones['ancho']=$ancho;
$this->dimensiones['alto']=$alto;
$this->dimensiones['largo']=$largo;
}
public function getDimensiones(): array
{
return $this->dimensiones;
}
public function pesoVolumetrico($factorDeCubitaje)
{
$m1=array_reduce($this->dimensiones,fn ($c,$n)=>$n*$c,1);
return $m1*$factorDeCubitaje/1000000;
}
}
Al implementar todos los métodos declarados en el interfaz se asegura la interoperabilidad entre clases y se homogeniza el código, asegurando que métodos con la misma función no tengan nombres diferentes.
Una clase, además, puede implementar varias interfaces, por ejemplo, si tenemos la siguiente interfaz:
interface PesoFisico {
public function pesoFisico();
}
Podríamos hacer que nuestra clase Hardware implementara ambas interfaces indicando la lista de las mismas separadas por coma:
class Hardware extends Producto implements VolumenFisico, PesoFisico {
/**... resto de código ...*/
}
Aunque es algo que debemos evitar, a partir de PHP 5.3.9 está permitido que una clase implemente interfaces que tienen métodos con el mismo nombre siempre que tengan los mismos argumentos y retornen el mismo tipo de dato. Antes de la versión PHP 5.3.9 no estaba permitido.
Y por último, es importante que sepas que partir de PHP 5.0 las interfaces pueden ampliar otras interfaces, por ejemplo:
interface A {
public function methodA();
}
interface B extends A {
public function methodB();
}
Una clase que implementara la interfaz B tendría que implementar los métodos methodA y methodB.
Los métodos que se declaren en un interfaz son públicos. Además de métodos, los interfaces podrán contener constantes pero no atributos.
El uso de interfaces son un recurso muy útil en el polimorfismo de inclusión. Por ejemplo, podemos pedir que un método o función reciba una clase que implemente una interfaz concreta, por ejemplo:
function comprobarDimensiones (VolumenFisico $p)
{
extract($p->getDimensiones());
if ($ancho>VolumenFisico::LADO_MAX
|| $alto>VolumenFisico::LADO_MAX
|| $largo>VolumenFisico::LADO_MAX) throw new Exception ('Lado demasiado extenso.',3881);
elseif ($ancho<=0 || $alto<=0 || $largo<=0)
throw new Exception ('Lado con longitud incorrecta.',3880);
}
También podemos comprobar si una clase implementa una interfaz concreta usando el operador instanceof:
function volumen($h)
{
$volumen=false;
if ($h instanceof VolumenFisico)
{
$volumen=array_reduce($h->getDimensiones(),fn($n,$m)=>$n*$m,1);
}
return $volumen;
}
Y que luego, por otro lado, tenemos un blog donde se van realizando diferentes publicaciones y cada publicación tiene también sus propias etiquetas:
Publicación en blog: “Cómo aumentar la velocidad de tu red de área local”; Etiquetas: Redes, GigabitEthernet, Switch.
Fíjate que de alguna forma, la publicación en el blog y el producto están relacionados, y comparten el aspecto o rasgo de “estar etiquetado” para facilitar su clasificación. En PHP aspectos como este se pueden modelar usando traits (que significa rasgo o características).
Un trait es un fragmento de código que puede ser incorporado a una clase pero que mantiene su independencia funcional. Un trait encapsula una serie de atributos y operaciones sobre los mismos que no tienen sentido por si solos, pero que dentro de una serie de clases si tienen sentido.
Para crear un trait o rasgo usamos la palabra reservada trait, y añadimos atributos y métodos como si de una clase se tratase:
trait Tags {
private $tags=[];
public function nuevoTag($tag)
{
if (!in_array($tag,$this->tags)) {
$this->tags[]=$tag;
return true;
}
return false;
}
public function eliminarTag($tag)
{
$p=array_search($tag,$this->tags);
if ($p!==false) {unset($this->tags[$p]);}
}
public function getTags()
{
return $this->tags;
}
}
El trait anterior encapsula las operaciones para añadir, eliminar y obtener las posibles etiquetas que un elemento podría tener, y podría estar asociado a un producto, a una publicación en un blog, etc.
Una vez creado, cuando necesitamos que una clase tenga ese rasgo o característica, indicamos en la clase que usará dicho trait de la siguiente forma:
require_once __DIR__.'/Tags.php';
abstract class Producto {
use Tags;
/*... resto del código ...*/
}
En el ejemplo anterior se añadiría el rasgo Tags a la clase Producto simplemente escribiendo use Tags; dentro de la clase. El resultado es que la clase en cuestión ahora tendría los métodos de implementados en el rasgo Tags de forma transparente. Veamos un ejemplo:
El ejemplo anterior usa la clase Hardware, recuerda que la clase Hardware hereda la clase Producto. Como la clase Producto usa el trait Tags, las subclases de Producto (como es el caso de Hardware), también heredarían dicho trait.
Stockbyte (CD-DVD Num. IE008)(Uso educativo no comercial para plataformas públicas de Formación Profesional a distancia.)
Una de las dudas más comunes en la programación orientada a objetos, es qué solución adoptar en algunas situaciones: interfaces o clases abstractas. Ambas permiten definir reglas para las clases que los implementen o hereden respectivamente. Y ninguna permite instanciar objetos. Las diferencias principales entre ambas opciones son:
En las clases abstractas, los métodos pueden contener código. Si van a existir varias subclases con un comportamiento común, se podría programar en los métodos de la clase abstracta. Si se opta por una interfaz, habría que repetir el código en todas las clases que lo implemente.
Las clases abstractas pueden contener atributos, y los interfaces no.
No se puede crear una clase que herede de dos clases abstractas, pero sí se puede crear una clase que implemente varios interfaces.
Adicionalmente, en lenguajes como PHP tenemos los traits o rasgos. Normalmente un trait lo usaremos cuando deseamos tener una funcionalidad compartida entre varias clases como un elemento de programación separado, para reducir así la complejidad de las clases y de nuestro mismo código. Los traits estarían a medio camino entre una clase abstracta y una interfaz debido a los siguientes motivos:
En un trait se pueden implementar métodos abstractos, dejando su implementación a la clase que use el rasgo.
Una clase puede usar múltiples traits.
Los traits y el polimorfismo de inclusión no son compatibles. Por ejemplo, no puedes crear una función o método que necesite una clase que use un trait concreto. Por ejemplo, el siguiente código no sería válido:
$h=new Hardware();
$h->nuevoTag('Redes');
$h->nuevoTag('Gigabit Ethernet');
$h->nuevoTag('8 puertos');
function mostrarEtiquetas(Tags $t) //ERROR: no habrá instancias del trait Tags
{
var_dump($t->getTags());
}
mostrarEtiquetas($h); //ERROR: Hardware no es una instancia de Tags, sino que usa Tags
Para finalizar con los interfaces y los ragos, a la lista de funciones de PHP relacionadas con la programación orientada a objetos podemos añadir las siguientes:
Funciones de utilidad para interfaces y rasgos
Función
Ejemplo
Significado
get_declared_interfaces
print_r (get_declared_interfaces());
Devuelve un array con los nombres de los interfaces declarados.
interface_exists
if (interface_exists('VolumenFisico')) {
…
}
Devuelve true si existe el interfaz que se indica, o false en caso contrario.
trait_exists
if (trait_exists('Tags'))
{
echo 'El trait Tags existe';
}
Devuelve true si existe el trait que se indica, o false en caso contrario.
Stockbyte (CD-DVD Num. IE008.)(Uso educativo no comercial para plataformas públicas de Formación Profesional a distancia.)
Vamos a revisar algunos aspectos adicionales que tienes que tener en cuenta cuando estés usando clases y objetos en PHP.
Ya sabes cómo instanciar un objeto utilizando new, y cómo acceder a sus métodos y atributos públicos con el operador flecha:
$p = new Hardware();
$p->setNombre('Switch M1R33');
Recuerda también que una vez creado un objeto, puedes utilizar el operador instanceof para comprobar si es o no una instancia de una clase determinada o si implementa una interfaz concreta:
if ($h instanceof Producto && $h instanceof VolumenFisico)
{
echo "Es una instancia de producto e implementa la interfaz VolumenFisico";
}
Además, de los mecanismos anteriores, en PHP se incluyen una serie de funciones útiles para el desarrollo de aplicaciones utilizando programación orientada a objetos que es buena idea que conozcas.
Funciones de utilidad para objetos y clases en PHP
Función
Ejemplo
Significado
get_class
echo "La clase es: " . get_class($p);
Devuelve el nombre de la clase del objeto.
class_exists
if (class_exists('Suscripcion') {
$p = new Suscripcion();
…
}
Devuelve true si la clase está definida o false en caso contrario.
get_declared_classes
print_r(get_declared_classes());
Devuelve un array con los nombres de las clases definidas.
class_alias
class_alias('Hardware', 'ProductoHardware');
$p = new ProductoHardware();
Crea un alias para una clase.
get_class_methods
print_r(get_class_methods('Producto'));
Devuelve un array con los nombres de los métodos de una clase que son accesibles desde dónde se hace la llamada.
method_exists
if (method_exists('Producto', 'vende')) {
…
}
Devuelve true si existe el método en el objeto o la clase que se indica, o false en caso contrario, independientemente de si es accesible o no.
get_class_vars
print_r(get_class_vars('Producto'));
Devuelve un array con los nombres de los atributos de una clase que son accesibles desde dónde se hace la llamada.
get_object_vars
print_r(get_object_vars($p));
Devuelve un array con los nombres de los atributos de un objeto que son accesibles desde dónde se hace la llamada.
property_exists
if (property_exists('Producto', 'codigo') {
…
}
Devuelve true si existe el atributo en el objeto o la clase que se indica, o false en caso contrario, independientemente de si es accesible o no.
En la siguiente página de la documentación oficial tienes el listado completo de funciones asociadas al uso de clases, interfaces y rasgos, no dudes en echarle un vistazo si la necesitas:
Stockbyte (CD-DVD Num. IE008.)(Uso educativo no comercial para plataformas públicas de Formación Profesional a distancia.)
¿Sabrías como copiar objetos en PHP? Suponemos que lo primero que se te viene a la cabeza sería hacer algo como lo siguiente:
$test=new class() {
public $dato;
};
$test->dato='ejemplo';
$copia=$test;
Aparentemente, en el código anterior se ha realizado una copia. Pero, ¿$copia y $test contendrían un objeto diferente? La realidad es que si ejecutamos el ejemplo anterior con PHP 5 o superior tanto $copia como $test realmente tendrían una referencia al mismo objeto, es decir, ambas variables apuntarían al mismo objeto en memoria, con los mismos datos y atributos. Si por ejemplo, realizas una modificación de los datos en $copia, también se modificaría en $test:
Esto no es así en PHP 4, donde los objetos se copian enteros en espacios de memoria diferentes. En PHP 5 se incluyo esta mejora para hacer un uso más eficiente de la memoria, dado que en PHP 4 cualquier asignación de objetos suponía usar más y más memoria muchas veces de forma innecesaria. En el ejemplo anterior $copia y $test serían identificadores de un mismo objeto que está almacenado en un único espacio de memoria y se podría utilizar cualquiera de los identificadores de forma indiferente.
La primera pregunta a responder ahora sería, ¿cómo puedo crea una copia de un objeto? Para crear una copia de un objeto, la cual esté en espacios de memoria diferente, puedes usar la función clone:
Al usar clone tanto $copia como $test contendrían copias idénticas del objeto ubicadas en espacios de memoria diferentes, por lo que una modificación en $copia no afecta a los datos almacenados en $test. Si bien, hay ocasiones en las que al realizar la copia es necesario que algún atributo no sea exactamente igual entre el original y la copia. Son circunstancias donde hay que intervenir tras realizar la clonación.
Para intervenir en el proceso de clonación puedes implementar el método mágico __clone en la clase, el cual se ejecutará tras hacer la clonación (clone). Fíjate en el siguiente ejemplo:
$test=new class() {
public $dato;
public function __clone()
{
$this->dato.='[COPIA]';
}
};
$test->dato='ejemplo';
$copia=clone($test);
echo $copia->dato; //Mostraría "ejemplo[COPIA]"
Es importante que tengas en cuenta que al usar el método mágico __clone en el ejemplo anterior, la ejecución de la función clone no generará copias idénticas. También es importante que tengas en cuenta que dos copias idénticas de un objeto dejan de ser idénticas desde el momento en el que algún atributo es modificado.
La pregunta ahora sería, ¿cómo podemos saber si dos identificadores ($copia y $test por ejemplo) contienen copias idénticas o si contienen referencias al mismo objeto? Esto podemos averiguarlo usando lo operadores == y ===.
$test === $copia será true solo si contienen referencias al mismo objeto.
$test == $copia será true si contienen referencias al mismo objeto o si son copias idénticas.
Fíjate en el siguiente ejemplo:
$test=new class() {
public $dato;
};
$test->dato='ejemplo';
$copia=clone($test);
if ($copia===$test) echo "Son referencias al mismo objeto";
elseif ($copia==$test) echo "Son copias identicas";
else echo "Son objetos diferentes";
Como acabas de comprobar, para crear nuevas referencias a un objeto ya existente se utiliza el operador =. Sin embargo, este operador aplicado a variables de otros tipos crea una copia idéntica del dato. En PHP puedes crear referencias a variables (como números enteros o cadenas de texto o arrays), utilizando el operador &:
$a = 'Ejemplo';
$b = & $a;
En el ejemplo anterior, $a y $b son referencias al mismo dato 'Ejemplo' y cuando se cambia el valor de una de ellas el cambio se refleja en la otra. Esto es especialmente útil en funciones, donde se pueden crear funciones que modifiquen directamente los datos originales al modificar un parámetro:
Sin embargo eso no es necesario cuando el argumento es un objeto, dado que directamente se pasará por referencia:
$test=new class() {
public $numero;
};
$test->numero=3;
function incrementar($objeto) //Se pasa por referencia al ser un objeto
{
$objeto->numero++;
}
incrementar($test);
echo $test->numero; //Mostrará 4
Si quieres ahondar más en el concepto de referencia en PHP puedes consultar el siguiente enlace:
Una práctica habitual en la programación orientada a objetos y que suele causar algunos quebraderos de cabeza es combinar métodos y atributos estáticos o de clase (static) con métodos y atributos de instancia en una misma clase.
Para aproximarnos mejor a la situación, vamos a analizar el siguiente ejemplo:
class Secuencia
{
private $dato;
private function __construct ($dato)
{
$this->dato=$dato;
}
public static function comenzar($dato)
{
if (is_numeric($dato)) return new Secuencia(intval($dato));
else return false;
}
public static function incrementar (Secuencia $e)
{
$e->dato++;
}
public function obtener ()
{
return $this->dato;
}
}
$Secuencia=Secuencia::comenzar(44);
Secuencia::incrementar($Secuencia);
echo $Secuencia->obtener();
La clase anterior simboliza una secuencia numérica que se incrementa de uno en uno. Existen muchas formas de implementar lo mismo, este ejemplo ha sido diseñado ex profeso, intenta analizarlo con calma.
Una vez analizado, ¿qué aspectos te parecen curiosos del ejemplo anterior? Seguro que más de uno. Veamos:
Para empezar, hay un constructor privado, lo cual no es habitual. Los constructores privados se usan cuando deseamos controlar mejor el proceso de construcción de una instancia o cuando el proceso de construcción puede generar instancias de diferentes subclases (denominado patrón Factory).
Desde la función estática comenzar se crea una instancia de la clase Secuencia. El hecho relevante aquí es se hace uso de un constructor privado desde el método estático comenzar, esto es posible porque el método estático comenzar y el constructor privado están en la misma clase, sino no sería posible.
Desde la función estática incrementar se accede al atributo privado $e->dato de la instancia que se pasa por parámetro. Esto es nuevamente posible debido a que forman parte de la misma clase, si ambos estuvieran en clases diferentes no sería posible.
Lo principal aquí es que visualices que desde un método estático puede accederse a los métodos y atributos privados y protegidos de instancias de la misma clase, como es el caso de las funciones comenzar e incrementar anteriores.
En ocasiones es necesario tener acceso al nombre de una clase, y aunque pueda parecer algo poco útil, hay situaciones donde si puede ser necesario (especialmente si se usan namespaces). A partir de la versión 5.5 de PHP puedes acceder al nombre de la clase de la siguiente forma:
<?php
class A {
}
echo A::class;
Y a partir de PHP 8.0 también puedes saber el nombre de la clase a partir de la instancia:
Por fin Carlos tiene un poco más de soltura en la programación orientada a objetos en PHP. No le ha sido fácil, pero como ya tenía ciertos conocimientos de Java realmente solo tenía que aprender las diferencias entre un lenguaje y otro.
Está enfrascado en su aplicación web para cómics, y se le ha dado una circunstancia que no sabe resolver. Se pregunta: "¿Cómo puedo almacenar los datos de un cómic de forma temporal? Necesito poder almacenar en un archivo los datos del cómic, pero no se cómo.".
Al día siguiente ve a Juan otra vez y le pregunta acerca de esto:
- Pues la verdad es que tengo un pequeño problema que no se como resolver de forma sencilla y elegante. ¿Cómo podrías almacenar los datos de un objeto en disco? - pregunta Carlos.
- Eso es fácil, yo lo serializaría o lo convertiría en JSON, o si tienes ganas, puedes usar XML. - responde Juan.
Como ya sabrás, las clases nos van a permitir modelar el comportamiento de nuestra aplicación a través de unidades de programación llamadas objetos que encapsulan conceptos que maneja nuestra aplicación. Por ejemplo, una clase Producto será un concepto que manejará nuestra aplicación que encapsula tanto los datos como la lógica para manejar dichos datos.
Cuando manejamos un objeto y necesitamos guardar la información del mismo (a lo que llamamos estado), ya sea en un archivo o en una base de datos, o cuando necesitamos transferirla (por ejemplo, a través de un mensaje HTTP a otra aplicación web), es costoso extraer el valor de los atributos del objeto y luego gestionar su almacenamiento o transferencia. Para facilitar esta tarea vamos a ver una serie de técnicas que nos van a facilitar mantener el estado del objeto: guardarlo para luego en un momento posterior, por la misma aplicación web o por una aplicación web diferente, restaurarlo.
Un mecanismo para persistir o mantener datos de objetos es a través de XML, aunque eso sí, tendremos que realizar la conversión de forma explicita. En PHP existen también diferentes clases que te permitirán trabajar con documentos XML, entre ellas podemos destacar SimpleXML por su sencillez de uso. Imagina que tienes los datos de un objeto en un XML como el siguiente:
También podríamos usar la función simplexml_load_file para cargar el XML desde un archivo desde disco.
Para realizar el proceso inverso, es decir, convertir los datos de un objeto a XML podríamos crear una instancia de SimpleXMLElement vacía, añadir con addChild los diferentes nodos con datos y posteriormente obtener el XML con el método asXML:
<?php
class Producto
{
private const XML_BASE = '<?xml version="1.0"?><producto></producto>';
private $nombre;
private $precio;
public function __construct($nombre,$precio)
{
$this->nombre=$nombre;
$this->precio=$precio;
}
public function toXML(): string
{
$sx = new SimpleXMLElement(static::XML_BASE);
$sx->addChild('nombre', $this->nombre);
$sx->addChild('precio', $this->precio);
return $sx->asXML();
}
}
$p = new Producto('Reloj',12.3);
echo $p->toXML();
El código anterior generaría un XML como el siguiente:
Stockbyte (CD-DVD Num. IE008.)(Uso educativo no comercial para plataformas públicas de Formación Profesional a distancia.)
Una de las técnicas que nos permitirá almacenar objetos de forma persistente es la serialización. La serialización consiste en crear una representación del dato en forma de cadena de texto, esto es especialmente útil en datos compuestos (como arrays u objetos). Por ejemplo, el array siguiente:
$array=[1,2,3];
En PHP se serializa como la siguiente cadena de texto:
a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}
El resultado obtenido es un string que contiene un flujo de bytes, en el que se encuentran definidos todos los valores del objeto o array. Para serializar un objeto o array lo que debemos hacer es usar la función serialize y la función unserialize. A continuación tienes un ejemplo de como se puede serializar un objeto para guardarlo en disco para luego volver a rescatarlo en otro script diferente:
Ejemplo de serialización y deserializacón
Ejemplo de serialización (archivo a.php)
Ejemplo de deserialización (archivo b.php)
<?php
include_once 'Producto.php';
//Creamos una instancia de la clase Producto
$producto = new Producto();
$producto->setCod('A12');
$producto->setNombre('Sony Walkman\'80s');
$producto->setDescripcion('Fantástico Walkman años 80 en implecable estado');
$producto->setPrecio(19.3);
//Serialización del objeto
$productoSerializado = serialize($producto);
//A modo de ejemplo, guardarmos el producto serializado en un archivo
file_put_contents('ejemplodearchivo.txt',$productoSerializado);
<?php
//Cargamos la clase Producto (sino no es posible deserializar)
include_once 'Producto.php';
$productoSerializado=file_get_contents('ejemplodearchivo.txt');
//Deserialización del objeto
$producto = unserialize($productoSerializado);
//Mostramos el contenido del objeto
var_dump($producto);
Por supuesto, no es necesario guardar el objeto serializado, el ejemplo anterior es solo eso, un ejemplo de como puede usarse la serialización en un escenario concreto, aunque puede haber más casos diferentes.
Las funciones serialize y unserialize se utilizan mucho con objetos, pero sirven para convertir en una cadena cualquier tipo de dato, excepto el tipo resource. Cuando se aplican a un objeto, convierten y recuperan toda la información del mismo, incluyendo sus atributos privados. La única información que no se puede mantener utilizando estas funciones es la que contienen los atributos estáticos de las clases.
A priori, cualquier clase que tu creas es serializable, de hecho, si una clase usa en su interior instancias de otras clases, también sería serializable, sin embargo, no todos los objetos de PHP son serializables. Un ejemplo de clase PHP que no es serializable es PDO, ¿porqué? Porque usa un tipo de dato llamado resource el cual no es serializable.
Si una clase que has creado tiene un atributo que es un objeto no serializable (o simplemente que no deseas serializar), es posible que necesites personalizar el proceso de serialización y deserialización de un objeto. Para ello puedes usar los métodos mágicos __sleep y __wakeup.
Si en la clase está definido un método con nombre __sleep, este se ejecuta antes de serializar un objeto. El método __sleep tiene que retornar un array con los nombrs d elos atribugos de la clase que se deben serializar.
Y si en una clase está definido el método __wakeup, este se ejecuta con cualquier justo después de que la función unserialize reconstruya el objeto. Por ejemplo:
class Suma {
private $c;
public function __construct(private int $a, private int $b)
{
$this->c=$a+$b;
}
public function __sleep()
{
return ['a','b'];
}
public function __wakeup()
{
$this->c=$this->a+$this->b;
}
public function mostrarSuma()
{
echo $this->c;
}
}
En el ejemplo anterior en la clase Suma solo se serializan los atributos a y b, dado que el atributo c se puede reconstruir con los valores de los atributos a y b.
Otra forma alternativa de personalizar la serialización es haciendo que la clase implemente la interfaz Serializable. Si se usa esta interfaz, no pueden usarse los métodos mágicos anteriores. En la siguiente página de la documentación oficial puedes ver como se utilizaría:
En versiones antiguas de PHP era problemático almacenar un objeto en una sesión. En las versiones más recientes de PHP (de la versión 5 en adelante), los objetos se serializan y deserializan de forma automática al guardarlos en la sesión y cuando la sesión se restaura la sesión al ejecutarse session_start, sin embargo es necesaria que la clase esté definida antes de restaurar la sesión, sino podemos tener problemas.
Por ejemplo, el siguiente código funcionaría la primera vez, almacenando en la sesión el producto serializado:
Sin embargo, al ejecutarlo por segunda vez, cuando ya está almacenado el producto en la sesión, generaría un error como el siguiente (versión PHP 5.6 en adelante):
Esto significa que no es posible resolver la clase a la que pertenece el objeto, asignándole la clase __PHP_Incomplete_Class. En versiones de PHP anteriores generaría un error fatal o no llegaría a funcionar.
Una forma muy utilizada hoy día para transferir información estructurada entre sistemas y para almacenarla es usando el formato JSON. El formato JSON es básicamente texto donde los datos se estructuran de una forma concreta que es compatible con el formato de objeto en Javascript de ahí su nombre.
JSON se utiliza hoy día muchísimo para implementar servicios web, donde aplicaciones intercambian información entre sí, como alternativa a XML. XML es más potente, pero también más pesado y, en ocasiones, añade una complejidad innecesaria para muchas situaciones.
En PHP se puede convertir a JSON cualquier tipo de datos (excepto resources), pero es especialmente interesante para serializar arrays y objetos. Veamos como se convertiría un array y un objeto a formato JSON:
Ejemplos de codificación a JSON de arrays y objetos
$objeto=new Class {
var $cod='A12';
var $unidades=2;
};
echo json_encode($objeto);
{"cod":"A12","unidades":2}
Como puedes observar en el código anterior, la codificación para el array asociativo y el objeto (segundo y tercer caso) genera un JSON idéntico en ambos casos. Tanto en el primer como en el segundo caso se codifica como si fuera un objeto en JSON, sin diferenciar que el segundo es un array asociativo y el tercero un objeto. Esto es un problema, dado que luego hay que hacer un procesamiento extra para volver a convertir el JSON al tipo esperado (array asociativo o objeto).
Para poder utilizar posteriormente el contenido JSON debemos decodificarlo, proceso que convertirá el JSON al tipo origen (objeto o array). Para realizar este procesamiento utilizaremos la función json_decode.
Ejemplos de codificación a JSON de arrays y objetos
Ejemplo de decodificación de un JSON a array asociativo
Ejemplo de decodificación de un JSON a un objeto
$json='{"cod":"A12","unidades":2}';
//Decodificamos el JSON como un array asociativo
$arrayAsociativo=json_decode($json,true);
var_dump($arrayAsociativo);
$json='{"cod":"A12","unidades":2}';
//Decodificamos el JSON como un objeto
$objeto=json_decode($json, false);
var_dump($objeto);
Como puedes observar en el código anterior, codificar o decodificar un JSON como array asociativo o objeto depende del segundo argumento de la función json_decode. Sin embargo, has de tener en cuenta que cuando decodificas un JSON a objeto, no se crea una instancia del objeto original, sino que se crea una instancia de la clase stdClass. Esto implica que tendrás que crear un mecanismo propio para poblar el objeto PHP partiendo del objeto JSON.
Cuando se serializaba un objeto PHP, tanto los atributos privados como los atributos públicos se incluían en la versión serializada. Sin embargo, cuando un objeto se convierte a JSON los atributos privados no se incluyen en el JSON generado. Por ejemplo:
class LineaPedido {
private $cod;
private $unidades;
public function __construct ($cod,$unidades)
{
$this->cod=$cod;
$this->unidades=$unidades;
}
}
$objeto=new LineaPedido('A10',3);
echo json_encode($objeto);
El ejemplo anterior generaría el siguiente el siguiente JSON: {}. Como puedes ver, no aparecen los atributos privados. No obstante, esto tiene solución, y la solución es hacer que la clase implemente la interfaz JsonSerializable. Esta interfaz implica la implementación del método jsonSerialize que debe devolver un array (preferiblemente asociativo) con los datos a incluir en el JSON generado:
class LineaPedido implements JsonSerializable {
private $cod;
private $unidades;
public function __construct ($cod,$unidades)
{
$this->cod=$cod;
$this->unidades=$unidades;
}
public function jsonSerialize(): mixed
{
return ['cod'=>$this->cod,'unidades'=>$this->unidades];
}
}
$objeto=new LineaPedido('A10',3);
echo json_encode($objeto);
//Mostrará: {"cod":"A10","unidades":3}
Aparte de poder serializar objetos o convertirlos a JSON, existen otras técnicas para mantener el estado del objeto para luego rescatarlo o restaurarlo posteriormente. Algunas más sencillas, otras no tanto.
En este apartado vamos a ver como podemos usar PDO, una tecnología que ya conoces, para recuperar un objeto de la base de datos. Imagina que tienes una tabla en la base de datos donde almacenas productos (código, nombre, descripción y precio) y que los obtienes con una consulta como la siguiente:
$sql="SELECT cod, descripcion, precio, stock from productos";
El resultado anterior obtendría todos los productos de la base de datos, pero podemos hacer que dichos atributos se almacenan directamente en instancias de la clase siguiente:
class Producto {
private $cod;
private $nombre;
private $descripcion;
private $precio;
//Resto del código
}
Para conseguirlo la clase Producto no debe tener un constructor con argumentos, y simplemente tenemos que usar el método fetch o fetchAll de la clase PDOStatement de la forma adecuada:
$stmt=$pdo->query($sql);
if ($stmt) {
$stmt->setFetchMode(PDO::FETCH_CLASS,Producto::class);
$resultado=$stmt->fetchAll();
}
En el ejemplo anterior, en el método setFetchMode se indica PDO::FETCH_CLASS, lo que permite que el resultado de la consulta se pueda encapsular en objetos de tipo indicado (clase Producto en este caso), generando en el caso anterior un array de instancias de la clase Producto.
Almacenar o actualizar un producto almacenado en una tabla asociada en la base de datos no es tan sencillo. Funciones como get_object_vars nos pueden ayudar, pero no es tan sencillo.
Para facilitar el almacenamiento de objetos en bases de datos relacionales se suelen utilizar los ORM (mapeador objeto-relacional). Ejemplos de ORMs muy usados en PHP son:
Aunque Carlos llevaba bastante avanzada la aplicación de cómics, se ha encontrado con un pequeño escollo. Cada vez que tiene que realizar algo tiene que empezar a poner múltiples sentencias include o require, a veces no sabe si hay algo que ya ha incluido antes o no, y empieza a ser un poco tedioso. Ese mismo día, Carlos ve a Juan, y Juan le pregunta:
- ¿Cómo llevas la aplicación de cómics? ¿Ya la tendrás casi terminada no? -pregunta Juan.
- La verdad es que no Juan, me he puesto a añadir clases, interfaces y demás, tengo el código super organizado pero ahora me da la impresión de que me cuesta más trabajo añadir una funcionalidad que antes -contesta Carlos.
- Y eso, ¿por qué? Debería ser más fácil la verdad.
- Cada vez que tengo que hacer algo tengo que pensar que archivos debo incluir, porque uno depende de otro y al final es un poco caótico.
- Bueno, eso tiene una solución fácil, usa espacios de nombres y carga automática, así no tendrás que preocuparte de que archivos son necesarios. Además, hace tu aplicación más eficiente.
Carlos se queda pensando. Ha visto algo de espacios de nombres y carga automática en la documentación oficial, pero está un poco perdido.
Cuando se usan muchas clases, y sobre todo, cuando se incorporan paquetes de terceros, es habitual encontrar clases y funciones con el mismo nombre.
Hasta la versión 5.3.0 de PHP no existía un mecanismo para que pudieran coexistir dos clases con el mismo nombre en un momento dado, algo que si era posible en otros lenguajes. Por ejemplo, en Java es posible tener clases con distinto nombre siempre que estén en paquetes diferentes.
El mecanismo implementado en PHP se denominan namespaces, que significaría espacios de nombres. Un espacio de nombres es una forma de encapsular elementos tales como funciones o clases, de tal forma que para poder acceder a dichas funciones o clase sea necesario indicar el espacio de nombres en el que se encuentra:
Salvador Romero Villegas (elaboración propia). Uso de espacios de nombres en PHP.(CC BY-NC-SA)
A modo de equivalencia puedes pensar en que un espacio de nombres es como un directorio. Dos archivos con el mismo nombre pueden coexistir en el mismo ordenador, siempre que estén en directorios diferentes. En subsiguientes apartados vamos a analizar el uso de espacios de nombres a nivel introductorio.
Paralelamente a los espacios de nombres surgió la necesidad de implementar mecanismos que redujeran el número de include y require a realizar en un proyecto. Como sabes, el mecanismo generalmente usado para separar código en diferentes archivos en PHP es haciendo uso de include, require, include_once y require_once , pero su uso, aunque necesario, tiene algunos inconvenientes destacados:
El número de inclusiones puede llegar a ser muy elevado en algunos casos, cuanto más dividido está el proyecto PHP en diferentes archivos .php puede llegar a ser tedioso y engorroso.
Al realizar inclusiones de archivos .php a través de include o require, nuestro código incluirá todos los archivos .php y procesará todo el código que contiene aunque no se use. Por ejemplo, si incluimos un archivo .php que contiene una clase concreta que solo se usa en ciertas circunstancias muy específicas, ese archivo se procesará aunque la clase solo se use muy pocas veces haciendo nuestro código sumamente ineficiente.
La pregunta es, ¿cómo podríamos solucionar ese aspecto? Podemos solucionarlo haciendo uso del autoloading, término que podemos traducir al español como carga automática. La carga automática es un aspecto muy potente, y en apartados subsiguientes verás una pequeña introducción a este aspecto.
¿Para qué sirve un espacio de nombres o namespace y para que tipo de elementos del lenguaje puedo usarlos? Esta es una buena pregunta desde luego, veamos un respuesta breve a la misma:
Recuerda que se usan para evitar "colisiones" de nombres, es decir, elementos que se llaman igual y que no pueden coexistir en el mismo código, pero que paralelamente, necesitamos utilizar a la vez.
Podemos usarlos con constantes (creadas con const), clases (class) y funciones (function); aunque realmente es importante cuando se usan con clases.
Permite, en un momento dado, renombrar funciones, clases y constantes para que puedan simultanearse el uso.
¿Cómo se crea un espacio de nombres? En realidad los espacios de nombres no se crean, lo que hacemos es al crear un archivo .php que contenga clases constantes y/o funciones, indicar el espacio de nombres en el que está a través de la palabra reservada namespace justo al principio del código PHP. Fíjate en el siguiente archivo codigo/espacio1.php:
<?php
<br />namespace ESP1;
class Ejemplo
{
public static function nombreDeClase()
{
echo self::class;
}
}
function max($a,$b)
{
if (!is_numeric($a) || !is_numeric($b) ) return false;
if ($a>$b) return $a;
elseif ($b>$a) return $b;
return false;
}
La clase Ejemplo y la función max anteriores estarían declarados en el espacio de nombre ESP1. Fíjate que en el código anterior solo hay PHP, no hay código HTML entremezclado. Los espacios de nombres están pensados para ser usados cuando solo hay código PHP declarando clases, funciones y constantes. Una vez que el espacio de nombres está declarado, como en el ejemplo anterior, si queremos usar las clases o funciones debemos indicar el espacio de nombres en el que se encuentra:
<?php
include __DIR__.'/codigo/espacio1.php';
echo "Calculando el máximo de 5 y 8:";
$maximo=ESP1\max(5,8);
echo $maximo;
Fíjate que en la invocación a la función max del espacio de nombres creado (ESP1) indicamos el espacio de nombres a modo de ruta ESP1\max. Esto le permitirá a PHP diferenciar entre la función max ya predefinida en el lenguaje y nuestra propia función max:
//Uso de la función max propia
$num=\ESP1\max(5,8);
//Uso de la función max ya incluida en PHP:
$num=max(5,8);
La función max predefinida en PHP estaría en lo que se denomina espacio de nombres global. También ocurre con clases como PDO o DateTime de PHP. Para indicar que una clase, función o constante está en un espacio de nombres global podemos podemos usar la "\" al principio de la clase o método:
//Uso de la función max propia
$num=\ESP1\max(5,8);
//Uso de la función max ya incluida en PHP indicando que está en el espacio de nombres GLOBAL:
$num=\max(5,8);
Otro aspecto importante de los espacios de nombres es que puedes renombrar una clase, función o constante para así no tener que volver a escribir la ruta entera. Para ello usamos use function para abreviar el uso de funciones, use constant para abreviar el uso de constantes y use a secas para abreviar el uso de clases. Adicionalmente puedes utilizar as para renombrar una clase, función o constante. Veamos un ejemplo:
include __DIR__.'/codigo/espacio1.php';
use ESP1\Ejemplo;
use function ESP1\max as mimax;
Ejemplo::nombreDeClase();
echo "El máximo de 5 y 6 es: ";
echo mimax(5,6);
En el ejemplo anterior se ha renombrado la función ESP1\max como mimax, principalmente para que no de conflicto con la función \max ya definida en el espacio de nombres global.
Los espacios de nombres pueden estar anidados. Es decir, podemos tener espacios de nombres dentro de nombres, por ejemplo:
namespace ESP1\SUBESP1;
class Ejemplo
{
public static function nombreDeClase()
{
echo self::class;
}
}
La clase anterior estaría en el subespacio de nombres SUBESP1, que estaría en el espacio de nombres ESP1. Esto se puede realizar tantas veces como se necesite, permitiendo así una estructuración del código de forma jerárquica. A la hora de hacer uso de esa clase podríamos poner un nombre completamente cualificado:
<?php
//El nombre completamente cualificado comienza por \
\ESP1\SUBESP1\Ejemplo::nombreDeClase();
El nombre completamente cualificado podemos usarlo desde cualquier contexto. Sin embargo, si estamos dentro de un espacio de nombres (como ESP1) podemos usar un nombre cualificado, que sería una ruta relativa desde el espacio de nombres actual:
<?php
//Archivo PHP en el espacio de nombres ESP1
namespace ESP1;
//Se buscará Ejemplo en el espacio de nombres ESP1\SUBESP1
SUBESP1\Ejemplo::nombreDeClase();
Por otro lado, es importante que sepas que el uso de ::class en una clase definida en un espacio de nombres incluirá también el espacio de nombres en el que está. Si usas el método estático anterior nombreDeClase, encontrarías que el nombre de la clase mostrado sería ESP1\SUBESP1\Ejemplo y no solo Ejemplo.
El autoloading o carga automática está disponible desde PHP 5.1, aunque ha sufrido algunas modificaciones a lo largo del tiempo. Vamos a ver como funciona a nivel general, sin entrar en las profundidades del autoloading.
En primer lugar, tienes que saber que el autoloading está enmarcado dentro de las librería estándar de PHP, donde se definen algunas funciones y clases base del funcionamiento interno de PHP.
En segundo lugar tienes que saber que existen una serie de recomendaciones denominadas PHP Standard Recommendations (PSR), que puedes encontrar en la página php-fig.org, que describen algunas recomendaciones que se siguen a nivel global en el desarrollo de aplicaciones web con PHP. Entre las recomendaciones más relevantes encontramos la recomendación PSR-4 , que regula básicamente como cargar de forma automática las clase partiendo del espacio de nombres donde está la clase y la ruta en el sistema de archivos.
En este apartado vamos a revisar algunas restricciones de la recomendación PSR-4. Aunque esta recomendación admite ciertas variantes, vamos a centrarnos en la configuración más sencilla:
La carga automática está limitada a componentes orientados a objetos.
La carga automática o autoloading que vamos a analizar se usa básicamente con clases, interfaces, rasgos (traits) y estructuras similares.
No obstante, la carga automática es sumamente configurable, por lo que cualquier programador o programadora puede implementar mecanismos más complejos para hacer carga automática de funciones por ejemplo, pero no vamos a ahondar en ellos porque tampoco se usan a nivel práctico y porque llevaría mucho tiempo.
Cada clase debe estar en un archivo .php del mismo nombre.
Cada archivo PHP con clases que se deban cargar automáticamente solo puede contener una única clase. Esta limitación se debe a que para poder hacer la carga automática es necesario asocia unívocamente cada archivo a cada clase. Además, el nombre de la clase y el nombre del archivo deben ser iguales (salvo la extensión .php que habrá que añadir al final).
Por ejemplo, la clase Producto debería estar en el archivo Producto.php. Adicionalmente, en el archivo Producto.php solo debe haber una clase.
La ruta de directorios y de espacio de nombres debe coincidir.
En el tipo de carga automática que vamos a ver la ruta del sistema de archivos a la clase debe coincidir con la ruta del espcio de nombres a la clase. Por ejemplo, la clase:
use \ESP1\SUBESP1\Ejemplo;
Que estaría en el espacio de nombres namespace ESP1\SUBESP1, debería estar en el archivo de disco ESP1/SUBESP1/Ejemplo.php.
Entendidas las limitaciones anteriores, has de tener en cuenta que el sistema de autoloading es sumamente configurable y que en algunos entornos, como cuando usamos composer, sufre ligeras variaciones (muy pocas en realidad).
Conocido lo anterior, vamos ahora a poner un ejemplo sencillo para entender como funciona. Imagina ahora que tienes una estructura de proyecto como la siguiente:
Clase clases\ESP1\Ejemplo en el espacio de nombres namespace clases/ESP1. Fíjate que la ruta del sistema de archivos (clases/ESP1/Ejemplo.php) coincide con la ruta del espacio de nombres (clases\ESP1\Ejemplo).
Clase clases\ESP2\Ejemplo en el espacio de nombres namespace clases/ESP2. Nuevamente, fíjate que la ruta del sistema de archivos (clases/ESP2/Ejemplo.php) coincide con la ruta del espacio de nombres (clases\ESP2\Ejemplo).
Un archivo llamado uso.php que usaría las clases Ejemplo de los espacios de nombres clases/ESP1 y clase/ESP2.
El ejemplo anterior estaría conforme a PSR-4 . Ahora, vamos a usar la carga automática. El autoloading realmente se configura en el archivo PHP que hace de punto de entrada de la aplicación PHP, entendiendo como punto de entrada aquellos script php que se ejecutan para ofrecer un servicio al usuario final y donde se coordina el uso del resto de clases y funciones. Las clases del directorio clases anterior no corresponderían a ningún punto de entrada de mi aplicación web, sino a clases y funciones accesorias que utilizo para estructurar mi código.
Dado el escenario actual, para hacer uso de la carga automática debemos hacer uso de la función spl_autoload_register al principio del código de uso.php. Esto ya evitará la necesidad de usar include o require para cargar las clases, optimizando la carga de código PHP solo cuando es necesario:
<?php
spl_autoload_register();
use \clases\ESP1\Ejemplo as Ejemplo1;
use \clases\ESP2\Ejemplo as Ejemplo2;
if (rand(1,2)===1)
{
Ejemplo1::test();
}
else
{
Ejemplo2::test();
}
Si deseas que las clases a incluir de forma automática estén en una carpeta distinta, puede hacer uso de la función set_include_path para indicar una ubicación adicional donde buscar archivos. Por ejemplo:
En el enlace siguiente puedes encontrar más información sobre la autocarga de clases, es de la documentación oficial de PHP, no dudes en echarle un vistazo:
Carlos ha empezado a aplicar la programación orientada a objetos en su proyecto, ha configurado los diferentes espacios de nombres y ha empezado a usar la carga automática. El mismo se ha dado cuenta del salto cualitativo que ha dado su aplicación. Cada mejora que hace en su aplicación la hace más rápido y de forma más eficiente. Le gusta sobre todo lo elegante que queda ahora el código, mucho más organizado y manejable.
Hoy ha quedado con Juan y Rosa para ir al cine. Hace tiempo que no veía a Rosa, y sabe que surgirá el tema de su proyecto, dado que Rosa es una consagrada programadora en PHP. Está tan contento de su proyecto que sabe que la conversación sobre el tema dará para mucho.
Un rato después, se ven en la puerta del cine, y Carlos decide iniciar la conversación preguntándole que está haciendo ahora:
- Y en qué estás trabajando ahora Rosa - pregunta Carlos.
- Pues sigo en la misma empresa, estoy enfrascada en una librería que estamos haciendo para los clientes que quieren usar nuestros servicios de paquetería, para que puedan instalarla usando composer.
Carlos hace un gesto de aprobación pero no sabe muy bien a que se refiere Rosa.
¿Qué es composer? Composer es un gestor de dependencias para PHP, esto quiere decir que es un software que nos va a permitir gestionar e instalar paquetes de software ya existentes que podremos usar en el desarrollo de nuestra propia aplicación web. A estos componentes de los que dependerá nuestra aplicación web los llamamos dependencias.
Seguro que alguna vez has escuchado el dicho de "no trates de reinventar la rueda" y sabes perfectamente a lo que se refiere. En el campo del software este dicho se aplica con mucha frecuencia, puesto que hay desarrolladores y desarrolladoras que ya han escrito código que resuelve situaciones específicas e implementa aspectos concretos y que podemos utilizar en el desarrollo de nuestra aplicación web. Cuando hablamos de composer a este código ya desarrollado y reutilizable se le denomina paquete.
Composer tiene diferentes tipos de paquetes de software reutilizables, los más comunes son las librerías y los proyectos. Composer nos da acceso a un repertorio inmenso de librerías para PHP. Pero, ¿qué es una librería? Una librería es un repertorio de funciones y clases que resuelve un problema concreto, por ejemplo: crear y gestionar documentos PDF o crear y gestionar conjuntos de pruebas para nuestra aplicación web. En este apartado revisaremos como instalar y usar librerías en nuestro proyecto a través de composer, lo cual será muy sencillo.
Composer además permite crear proyectos partiendo de un arquetipo base, como por ejemplo crear un proyecto Laravel o crear un proyecto Symfony, ambos son frameworks muy extendidos.
Cuando utilices composer verás que asociado a tu proyecto se creará una carpeta llamada vendor, en esa carpeta estarán todas las librerías de las que depende tu aplicación web. Esto lo revisaremos más adelante, lo importante ahora es que sepas de dónde salen dichas librerías. Estas librerías no salen de la nada, en realidad salen de repositorios de paquetes. El repositorio principal de paquetes para composer es Packagist, y al instalar una dependencia los paquetes se descargarán de dicho repositorio:
En el mundo del software un arquetipo representa un proyecto base ideal a partir del cual se construye el software deseado. Normalmente tiene una estructura concreta y sigue una reglas a la hora de añadir elementos que amplían o modifican el comportamiento de partida. El comportamiento de partida de un arquetipo suele ser muy básico.
Un framework o marco de trabajo es un arquetipo software en su máximo exponente. Permite el desarrollo rápido de aplicaciones web gracias a que ofrece una serie de componentes software para realizar gran parte de la funcionalidad. Por otro lado, requiere seguir una estructura de proyecto concreta y una serie de reglas a la hora de añadir o modificar una funcionalidad a la aplicación web. El coste de aprendizaje de un framework concreto es elevado, pero una vez que se ha aprendido el tiempo de desarrollo se reduce drásticamente.
En primer lugar, procedamos a instalar composer. El proceso de instalación requiere lo siguiente:
Instalar PHP (si no lo tienes instalado). Si ya tienes instalado XAMPP es suficiente, dado que XAMPP incluye el ejecutable php.exe.
Establecer en el PATH al ejecutable de PHP (php.exe). Esto permitirá instalar composer más cómodamente dado que composer es un programa escrito en PHP.
Instalar composer. Realmente en este paso no hay que hacer un proceso de instalación como tal, sino más bien descargarlo y verificar que es correcto.
Establecer en el PATH la ruta a composer (que dependerá del mecanismo de instalación). Esto permitirá invocarlo desde cualquier directorio de proyecto.
Veamos como realizar el proceso anterior en Windows si ya tienes instalado XAMPP.
Paso 1. Instalar PHP en Windows
No es necesario que instales PHP si ya tienes XAMPP, pero aún así, puedes instalar otra versión de PHP si lo deseas. Lo primero que tendrías que hacer es descargar e instalar PHP para Windows de la siguiente página:
En esa página encontrarás múltiples versiones de PHP. Al descargar PHP para Windows verás que es un archivo ZIP, tal y como muestra la siguiente captura de pantalla de la página oficial:
Una vez descargado, lo más aconsejable es descomprimir el archivo zip en una carpeta local de tu usuario en Windows, aunque puedes hacerlo en una carpeta accesible por varios usuarios. Por ejemplo, si tu usuario en Windows es "salva", puedes descomprimir PHP en la carpeta "c:\users\salva\bin" (por ejemplo, en C:\users\salva\bin\php) :
Salvador Romero Villegas (elaboración propia)(CC BY-SA)
Paso 2. Establecer el PATH al ejecutable de PHP
Una vez que ya tengas PHP en tu equipo (bien porque ya lo tengas instalado con XAMPP o bien porque ya lo hayas descargado) es el momento de establecer el PATH a dicho ejecutable para poder lanzarlo desde la línea de comandos. Si tienes instalado XAMPP busca la ruta a php.exe dentro de XAMPP (generalmente C:\xampp\php), si lo instalaste en el punto anterior, recuerda la ruta que usaste para descomprimirlo. Para añadirlo al PATH vamos a ejecutar el siguiente comando desde la línea de comandos:
Para modificar la ruta solo para el usuario actual (recomendado): setx PATH "%PATH%;C:\xampp\php"
Para modificar la ruta para todos los usuarios (solo si PHP está instalado en un lugar accesible por todos los usuarios): setx /m PATH "%PATH%;C:\xampp\php"
Una vez realizada esta acción, deberás cerrar la ventana de línea de comandos y volver a abrirla para utilice las variables de entorno modificadas. Comprueba que aparecen la variable de entorno PATH modificada:
Salvador Romero Villegas (Elaboración propia)(CC BY-SA)
Paso 3. Descargar Composer
El proceso de instalación de composeres sencillo, y puedes verlo aquí:
Por otro lado, para instalarlo en Windows también tienes otros mecanismos alternativos, como por ejemplo usar el instalador Composer-Setup.exe, pero requiere que tengas instalado PHP previamente (o al menos la ruta a PHP incluida en el PATH)
Instalar composer en Linux es muy sencillo que instalarlo en Windows, dado que suele estar disponible entre los paquetes habituales. Por ejemplo, en Ubuntu se instalaría simplemente ejecutando el siguiente comando en el terminal:
sudo apt install composer
Para instalarlo en otros sistemas operativos consulta la página oficial: Instalación de composer.
¿Tienes interés en profundizar en la instalación y configuración de Composer? ¿Quieres saber más sobre la gestión de dependencias en PHP? El siguiente enlace te llevará a la documentación oficial de Composer:
Una vez que tenemos instalado Composer el siguiente paso es crear un proyecto Composer. A este respecto vamos a distinguir dos formas: crear un proyecto composer desde la línea de comandos o crear un proyecto composer desde NetBeans. Lo más habitual es crearlo desde la línea de comandos directamente.
Crear un proyecto composer desde la línea de comandos
Para crear un proyecto con composer desde la línea de comandos simplemente abre un terminal y ve a la carpeta donde tienes el proyecto dentro de tu servidor web de desarrollo (por ejemplo: C:\xampp\htdocs\proyectoc), y ejecuta el comando composer init. Al ejecutar este comando se nos preguntará, de forma interactiva, sobre los detalles del proyecto a crear:
En primer lugar, se te preguntará por el nombre del paquete software a crear. Esa información se suele estructurar por vendedor/nombre paquete. Los valores usados identifican a tu proyecto.
Después se pregunta por la descripción y el autor, datos no obligatorios.
A continuación se preguntarán algunos datos opcionales:
Estabilidad del proyecto (minimum-stability), donde deberíamos indicar que estamos en desarrollo (dev).
El tipo de paquete, donde deberíamos indicar 'project'.
La licencia, donde deberíamos indicar la licencia de nuestro proyecto (si es un proyecto privado deberíamos indicar propietary)
Después, se nos pregunta por las dependencias, que podremos incluirlas ahora si las tenemos claras o más adelante. En este caso, lo haremos más adelante, por lo que aquí teclearemos no.
También se nos preguntará por los mapeos PSR-4 a nuestro código, algo que revisaremos más adelante. De momento, en esta parte nos limitamos a aceptar la sugerencia.
Por último, se muestra una configuración del proyecto (en formato JSON), si aceptamos dicha configuración se guardará en el archivo composer.json
Veamos un ejemplo de interacción al crear el proyecto:
Salvador Romero Villegas (Elaboración propia). Captura de pantalla de la ejecución del comando "composer init"(CC BY-SA)
Crear un proyecto composer en NetBeans
Antes de continuar, lo primero que tienes que hacer para crear un proyecto composer en NetBeans es indicar en las opciones de PHP la ruta a composer.bat:
Una vez creado lo anterior, podemos proceder de varias formas, la primera es añadir dependencias al crear un nuevo proyecto, lo que hará que se inicialice un proyecto composer.
NetBeans no va a solicitar los datos del proyecto de forma interactiva (algo que si se pide si realizamos el proceso a través de la línea de comandos). Se generará un archivo composer.json con algunos datos por defecto que debemos modificar manualmente.
Una vez que ya hemos generado el archivo composer.json, ya tendremos configurado nuestro proyecto con composer (a falta de personalizar los aspectos que estimes oportunos). A continuación tienes un ejemplo de archivo composer.json:
Añadir una dependencia es sencillo, tenemos varias opciones. Aquí vamos a revisar las siguientes:
A través de la línea de comandos usando composer require.
Modificando directamente el archivo composer.json y ejecutar composer install.
Añadir una dependencia a través de NetBeans.
No obstante, antes de instalar una dependencia lo primero que tienes que hacer es saber que librería quieres instalar y usar en tu proyecto. En este caso, vamos a usar la librería monolog que es una librería pensada para crear un log en nuestra aplicación donde se van recopilando los diferentes eventos que ocurren en nuestra aplicación. El paquete a instalar será monolog/monolog, aquí tienes más información del mismo:
Verás que el paquete tiene diferentes versiones y que cada versión a su vez puede requerir una versión de PHP diferente. Por ejemplo, la versión 2.8.0 de este paquete requiere usar PHP igual o superior a 7.2:
Instalar una dependencia a través de composer require
Para instalar esta dependencia accede a la carpeta del proyecto desde un terminal y ejecuta el comando composer de la siguiente forma:
composer require monolog/monolog:2.8.0
El comando anterior instalará la versión de monolog 2.8.0 en nuestro proyecto y a partir de ese momento podremos usar la librería. Si no indicamos la versión, se instalará la versión más reciente:
composer require monolog/monolog
También puedes indicar limitaciones a la hora de la versión a utilizar. Por ejemplo:
composer require monolog/monolog:2.*
Lo anterior significará que instalará la versión más reciente del paquete monolog/monolog que haya entre la versión 2.0 (>=2.0) y la versión 3.0 (<3.0), sin incluir las diferentes versiones etiquetadas como 3.0 en adelante.
Si quieres saber más sobre como indicar limitaciones en las versiones a utilizar de los diferentes paquetes visita el siguiente enlace:
Instalar una dependencia modificando el archivo composer.json
Otra opción a la hora de instalar dependencias es modificar directamente el archivo composer.json, para ello añade las dependencias en la sección require separándolas por comas:
Después de realizar esta modificación tendremos que ejecutar el comando siguiente para realizar la instalación:
composer install
Instalar una dependencia a través de NetBeans
NetBeans tiene mecanismos directos para la instalación de dependencias en un proyecto concreto. Una vez iniciado Netbeans, en un proyecto PHP encontrarás las siguientes opciones:
Una vez realizado cualquiera de los procesos anteriores, el paquete instalado se descargará dentro de una carpeta llamada vendor, además se modificará el archivo composer.lock, el cual contiene las versiones exactas de todos los paquetes instalados en la carpeta vendor. Ten en cuenta que cuando instalamos un paquete, este puede depender a su vez de otros paquetes, por lo que es muy probable que se instalen un conjunto de paquetes en cadena.
Algunos comandos interesantes de composer que tienes que tener en mente son:
composer install: cuando borras la carpeta "vendor" o actualizas a mano el archivo composer.json puedes instalar todas las dependencias simplemente ejecutando composer install. También deberás ejecutar composer install cuando eliminas un paquete manualmente de composer.json.
composer update: cuando utilizas modificadores en las versiones de los paquetes (como por ejemplo composer require monolog/monolog:2.*), puedes instalar la última versión disponible dentro del rango de versiones permitido haciendo composer update.
Cuando enviamos un proyecto es normal borrar la carpeta vendor para que este ocupe menos espacio, luego puedes reconstruir las dependencias haciendo composer install.
Es importante que sepas la diferencia entre dependencia de producción y dependencia de desarrollo.
Cuando usas composer require estás instalando una dependencia que será usada tanto en desarrollo como en producción, es decir, será una dependencia que se usará desde el momento en el que se está creando la aplicación web y se está escribiendo su código (desarrollo), hasta el momento en el que la aplicación se está usando por usuarios reales (producción).
Sin embargo, existen paquetes que solo se usan en el momento de desarrollo y que no es necesario en producción. En ese caso usaríamos composer require-dev. Es el caso de paquetes como phpunit usado para generar baterías de pruebas que permiten verificar que nuestra aplicación cumple con determinados criterios.
Una vez que hayas creado el proyecto con composer e instalado las posibles dependencias es el momento de usar las librerías que has instalado. Para ello, vamos a crear lo que sería un endpoint o punto de conexión o punto de entrada para la aplicación web, es decir, vamos a crear un archivo .php que va a ser el que se ejecute cuando el usuario accede a nuestra aplicación web. En este ejemplo vamos a crear el archivo ejemplo.php:
Para que en nuestro archivo ejemplo.php puedan usarse los paquetes instalados es necesario que en él se incluya el script vendor/autoload.php. El script vendor/autoload.php es un script generado automáticamente por composer. Para incluirlo editamos el archivo ejemplo.php y ponemos en la primera línea lo siguiente:
A partir de ahí, ya podremos utilizar en nuestro archivo de ejemplo las dependencias instaladas. Veamos un ejemplo de uso:
<?php
require_once __DIR__.'/vendor/autoload.php';<br />
// Creamos un Logger con el nombre de canal "Eventos de usuario"
<br />$log = new Monolog\Logger('Eventos de usuario');
<br />/* Indicamos que los logs se van a guardar en un archivo llamado eventos.log en el
mismo directorio de la aplicación (lo normal es que esté en otro directorio, pero
como estamos aprendiendo lo vamos a dejar aquí) */
$log->pushHandler(new Monolog\Handler\StreamHandler(__DIR__.'/eventos.log', Monolog\Logger::DEBUG));
<br />// Añadimos un mensaje de warning
$log->info('Iniciado el script');
$log->warning('Registramos un warning');
$log->error('Registramos un error');
<br />?>
Los eventos del log se guardarán en : <?=__DIR__.'/eventos.log'?>
Al invocar el script anterior desde el navegador solo se verá un mensaje indicando donde se están guardando lo mensajes de log (algo no necesario realmente). Lo que si es importante es que en el archivo eventos.log se están guardando los mensajes que hemos registrado gracias a la librería que hemos incorporado a través de composer:
[2022-12-07T10:00:03.631511+00:00] Eventos de usuario.INFO: Iniciado el script [] []
[2022-12-07T10:00:03.651607+00:00] Eventos de usuario.WARNING: Registramos un warning [] []
[2022-12-07T10:00:03.651648+00:00] Eventos de usuario.ERROR: Registramos un error [] []
En el ejemplo anterior observarás que para acceder a las clases, funciones y métodos de las librerías incluidas debes poner el espacio de nombres (namespace) donde se encuentran, por ejemplo:
Monolog\Logger: usamos la clase Logger del espacio de nombre Monolog
Monolog\Handler\StreamHandler: usamos la clase StreamHandler del espacio de nombres Monolog\Handler.
Para simplificar el uso de estas clases puedes usar la palabra reservada use de PHP para indicar que vas a usar dichas clases sin tener que indicar el espacio de nombres una y otra vez:
<?php
require_once __DIR__.'/vendor/autoload.php';
use \Monolog\Logger;
use \Monolog\Handler\StreamHandler;
$log = new Logger('Eventos de usuario');
$log->pushHandler(new StreamHandler(__DIR__.'/eventos.log', Logger::DEBUG));
$log->info('Iniciado el script');
?>
IMPORTANTE: a la hora de indicar una ruta a un archivo en PHP (por ejemplo, al usar include o require_once) es conveniente usar el separador de directorios "/" en vez de "\" (aunque estés en Windows), por ejemplo:
Es mejor poner conf/settings.php en vez de poner conf\settings.php.
La primera forma funciona en todos los sistemas operativos, la segunda solo en Windows. Además, la segunda forma se puede confundir con un espacio de nombres.
La pregunta llegado este momento es, ¿cómo podemos utilizar autoloading para nuestras propias clases en un proyecto composer? Composer registra sus propias funciones de carga automática de clases y nosotros podemos hacer uso del mismo para cargar nuestras propias clases, evitando así tener que hacer include o require múltiples veces.
Para realizar este proceso vamos a revisar dos formas distintas en las que composer puede hacer la carga de clases. En ambos casos es importante que el nombre de archivo coincida con el nombre de la clase, es decir, si la clase se llama Producto el nombre del archivo deberá ser Producto.php, y además, dentro de cada archivo .php solo puede haber definida una única clase.
Veamos los mecanismos esenciales:
Mapeado de clases
Este tipo de mecanismo se suele utilizar cuando las clases de nuestro proyecto no utilizan espacios de nombres, o cuando no siguen una estructura de carpetas para organizar el código que coincida con la estructura de los espacios de nombres.
Imagina el siguiente ejemplo:
Salvador Romero Villegas (Elaboración propia)(CC BY-SA)
En el ejemplo anterior ClaseA.php y ClaseB.php contienen las clases ClaseA y ClaseB, las cuales no están en ningún espacio de nombres concreto (no se usa namespace en ellas). Lo que podemos realizar en este caso es indicar en composer.json que deseamos que se mapeen las clases contenidos en ambos archivos para que las cargue automáticamente cuando se usen. Esto lo realizaríamos así:
Tras realizar ese procedimiento debemos forzar a composer a que regenere el archivo autoload.php, para que así configure el mapeado automático:
composer dumpautoload
Una vez realizado lo anterior, podremos usar las clases anteriores simplemente indicando el nombre de la clase:
<?php
include __DIR__.'/vendor/autoload.php';
$a=new ClaseA(); //Se carga codigo/ClaseA.php y se crea la instancia de ClaseA
$b=new ClaseB(); //Se carga codigo/ClaseB.php y se crea la instancia de ClaseB
Cuando queremos indicar a composer que mapee todas las clases de un directorio podemos indicar el nombre del directorio:
{
"autoload": {
"classmap":
["codigo/"]
}
}
Además, puedes indicar varios directorios o archivos .php (que contengan una clase del mismo nombre) separados por comas.
Carga de clases en el cargador PSR-4
Cuando las clases están dentro de un espacio de nombres concreto y además los espacios de nombres coinciden con la estructura de carpetas y subcarpetas, podemos usar el cargador PSR-4 de composer.
Imagina que tienes la siguientes estructura de proyecto:
Salvador Romero Villegas (elaboración propia)(CC BY-SA)
Imagina que los archivos ClaseA.php y ClaseB.php contienen las clases ClaseA y ClaseB, las cuales están en el espacio de nombres llamado MisClases, por ejemplo:
namespace MisClases;
class ClaseA
{
public function __construct()
{
echo "Se ha construido A".PHP_EOL;
}
}
En este caso, lo que podemos hacer es establecer un ancla que mapea un espacio de nombres MisClases al directorio concreto que contiene dichas clases: src/. Esto se indicaría así en el archivo composer.json:
Una vez realizada la configuración anterior, tendremos que ejecutar composer dumpautoload, para que configure el ancla anterior. A partir de ahí, el uso de las clases del espacio de nombres anclado será tan fácil como indicar la ruta al espacio de nombres en el que se encuentran:
<?php
include __DIR__.'/vendor/autoload.php';
$a=new MisClases\ClaseA();
$b=new MisClases\ClaseB();
En este mecanismo, si hay espacios de nombres dentro de otros espacios de nombres (como podría ser MisClases\Controladores), se espera que exista una estructura de directorios acorde. Por ejemplo:
La clase MisClases\ClaseA se espera que esté en src/ClaseA.php, dado que src/ está anclado al espacio de nombres MisClases en composer.json.
La clase MisClases\Controladores\Principal debería estar en el archivo src\Controladores\Principal.php.
En los dos mecanismos anteriores (mapeado de clases y PSR-4) la carga de clases se produce bajo demanda, es decir, cuando se detecta que se necesita una clase es cuando se realiza el include o require del archivo correspondiente de forma automática, lo que hace nuestro código mucho más eficiente.
No obstante, hay ocasiones en las que es necesario incluir scripts .php de forma permanente. Por ejemplo, archivos .php de configuración o archivos .php que contienen funciones de carácter general o clases que necesitamos que no se carguen bajo demanda. Esta es también una situación prevista por composer, donde podemos indicar que uno o varios archivos se carguen siempre al incluir el archivo /vendor/autoload.php.
Imagina la siguiente situación:
Salvador Romero Villegas (elaboración propia)(CC BY-SA)
En el caso anterior los archivos conf/db.conf.php y lib/misfunciones.php contienen información de configuración y funciones PHP respectivamente. Si queremos que ambos archivos estén disponibles con simplemente cargar /vendor/autoload.php, lo podemos indicar así:
Si revisas la documentación de composer verás que se menciona el cargador PSR-0. La recomendación PSR-0 está obsoleta y se recomienda usar la recomendación PSR-4 en su lugar.
En el siguiente archivo comprimido ZIP puedes encontrar 4 proyectos que usan composer para hacer autoloading de diferentes formas, incluido también el cargador PSR-0:
Stockbyte (CD-DVD Num. IE008)(Uso educativo no comercial para plataformas públicas de Formación Profesional a distancia.)
Después de unas semanas, Carlos ha conseguido reprogramar su aplicación de catalogación utilizando orientación a objetos. Le ha costado bastante esfuerzo, pero reconoce que el resultado merece la pena. El código resultante es mucho más limpio, y cada clase de las que ha creado tiene un cometido concreto y bien definido.
Ahora es mucho más fácil hacer cambios en el código. Definitivamente, tendrán que utilizar orientación a objetos cuando empiecen el nuevo proyecto. Mientras tanto, se propone volver sobre una asignatura pendiente: mejorar el aspecto de sus páginas. Y aunque conoce el lenguaje HTML, y sabe utilizar las hojas de estilo, decide pedirle consejo a un amigo suyo que se dedica al diseño de webs.
Su amigo trabaja en otra empresa y su función es diseñar el aspecto de los sitios web que crean. Cuando ve la aplicación de Carlos, queda muy impresionado por lo que ha avanzado en poco tiempo. Tras examinarla, le indica que tal y como está es muy difícil cambiar su aspecto. Tiene el código HTML distribuido en diversos ficheros, y entremezclado con el código PHP.
Le comenta que en su empresa utilizan mecanismos de separación del código y le anima a que los pruebe. Si lo hace, él se ofrece a darle un diseño más profesional a su aplicación. —¡Manos a la obra!
Hasta ahora hemos revisado como utilizar la programación orientada a objetos para desarrollar una aplicación web en PHP, pero como le ocurre Carlos en el caso práctico, es complicado modificar la interfaz en HTML y trabajar en equipo cuando el código HTML está entremezclado con el código PHP.
Cuando organizamos el código de nuestra aplicación web, nuestro código generalmente no sigue una estructura donde claramente se separan responsabilidades. Aunque tengamos el código estructurado en funciones y clases, muchas veces nos tomamos la libertad de hacer un echo aquí o allí, o de hacer una consulta a la base de datos en cualquier parte, lo cual dificulta el trabajo en equipo y posterior mantenimiento del código.
Cuando creamos un proyecto tenemos que ser capaces de responder a preguntas como la siguiente: ¿Dónde está el código destinado a generar el HTML de la interfaz? Si la respuesta es "en múltiples archivos .php", entonces necesitamos repensar nuestro proyecto.
De la experiencia de muchos y muchas han surgido diferentes formas de resolver problemas como este, lo que han dado a diferentes patrones de arquitectura de aplicaciones web. Estos patrones nos indican como organizar la aplicación web internamente, de manera que la responsabilidad esté claramente diferenciada, permitiendo un desarrollo más claro, escalable y fácil de mantener. Los patrones más usado hoy día en el diseño web son el patrón MVC, el patrón MVP y el patrón MVVM.
Un patrón de diseño define una forma de resolver determinados problemas en la práctica da buenos resultados. Los patrones de arquitectura se consideran un tipo de patrón de diseño.
En general, las aplicaciones que utilizan algún patrón de arquitectura se implementan siguiendo una estructura multicapa. Cada capa aglutina componentes (clases, interfaces, rasgos, etc.) dedicados a realizar funciones y a asumir responsabilidades específicas de dicha capa. Si necesitamos que nuestra aplicación haga algo concreto, debemos revisar a que capa o capas corresponde y crear los componentes necesarios en cada capa, lo cual implica crear componentes de código en diferentes lugares y siguiendo ciertas especificaciones. Los componentes de cada capa se comunican entre si a través de interfaces claramente definidas, por lo que habitualmente un componente de una capa determinada tiene que implementar una interfaz concreta o extender un clase existente.
Habitualmente diferenciamos cuatro capas con responsabilidades diferentes:
Capa de presentación o vista. Esta capa es responsable de generar la interfaz de usuario (HTML, CSS, etc.), si necesitamos mostrar algo al usuario final debemos hacerlo en esta capa y no otra.
Capa de aplicación o capa de servicio. Esta capa es responsable de organizar las operaciones que ofrece nuestra aplicación web, uniendo la capa de presentación y la de negocio, conectando los datos que llegan de la lógica de negocio con la capa de presentación y viceversa. Por ejemplo, si se implementa la operación de mostrar un listado de productos, esta capa recogerá los datos de la lógica de negocio, los adaptará si es necesario y los pasará a la capa de presentación para que sean mostrados al usuario.
Capa de lógica de negocio. Es responsable de controlar que los datos sean creados, accedidos, modificados y borrados conforme a las reglas establecidas, y que no puedan hacerse operaciones con los datos no permitidas.
Capa de acceso a datos. Es responsable de persistir los datos, y es, generalmente, la base de datos y los componentes asociados.
No obstante hay que entender que el enfoque anterior puede variar en función de las necesidades, dando lugar a estructuras con más o menos capas. Por ejemplo, es habitual que la capa de aplicación no exista y esa responsabilidad se aglutine en la capa de lógica de negocio.
Crear aplicaciones multicapa y que implementen algún patrón de arquitectura concreto es práctico y da flexibilidad a nuestras aplicaciones web, pero requiere crear una infraestructura base sobre la que construir nuestra aplicación. En este apartado abordaremos la creación de una aplicación que implementa el patrón MVC a un nivel muy básico (no seguiremos una estructura multicapa, pero si nos aproximaremos al patrón en sí).
Existen innumerables frameworks de desarrollo web que permiten crear aplicaciones multicapa que siguen algún patrón arquitectural como los antes mencionados. Esos frameworks nos ofrecen una infraestructura base muy completa ya desarrollada, a partir de la cual simplemente tenemos que ir añadiendo clases, interfaces, métodos, etc. Lo complicado de estos frameworks es aprender su funcionamiento interno. Algunos de estos frameworks son:
A la hora de elegir un framework deberás estudiar cuál se adapata mejor a tus necesidades. Algunas características que deberás tener en cuenta es la versión de PHP que soporta, si soporta el MVC, funciones AJAX, autenticación, etc. Para ello te puedes guiar con la información que nos facilita la página de bestwebframeworks.com mencionada anteriormente.
Las aplicaciones web son un ejemplo perfecto de lo que denominamos arquitectura de 3 niveles (3-tier architecture):
Nivel de presentación: corresponde a lo que ocurre en el navegador web del cliente, donde se renderiza el HTML generado de forma estática o dinámica por nuestro PHP.
Nivel de aplicación: corresponde a nuestro código PHP, donde se procesan peticiones HTTP y se genera contenido que será renderizado por el navegador web.
Nivel de datos: corresponde con la base de datos donde los datos de la aplicación son persistidos.
Las aplicaciones Web por tanto son de partida una arquitectura de dos o tres niveles; esta diferenciación se hace a un nivel muy elemental, básicamente porque el protocolo HTTP obliga a ello: cada nivel se ejecuta en una máquina diferente.
A menudo en internet se confunden los términos multicapa (multilayer) con multinivel (multitier), pero ambos son términos muy diferentes. Una arquitectura de aplicación multicapa tiene en su interior una división lógica del software en componentes organizados en capas. El código se organiza de manera que las responsabilidades no se entremezclen, cada componente de cada capa va en un lugar diferente. Cuando deseamos usar un componente de una capa determinada usamos directamente en PHP las clases y funciones de dichos componentes dado que todo se ejecuta en la misma aplicación.
Sin embargo, la programación multinivel implica que los componentes de un nivel concreto se ejecutan de forma separada, incluso a veces en máquinas diferentes. Existe una división física en componentes que se ejecutan en diferentes niveles. Esto implica que debe haber una comunicación entre niveles generalmente a través de algún protocolo de comunicaciones como SOAP o RPC, que permita comunicar los diferentes componentes que están distribuidos en diferentes sistemas. Cuando deseamos usar un componente de un nivel diferente se establece una comunicación remota entre componentes.
En este apartado hablamos de aplicaciones multicapa, y no de aplicaciones multinivel.
El patrón Modelo - Vista - Controlador, también conocido como patrón MVC, divide la aplicación en componentes con diferentes responsabilidades. En función de la responsabilidad del componentes tendremos: componentes del modelo, de la vista y del controlador.
Vamos a aproximarnos de forma general a la responsabilidad de cada componente en este modelo, poniendo el foco en desarrollo de aplicaciones web. A priori, la responsabilidad de cada componente es la siguiente:
Vista. Los componentes de la vista se encargan de presentar los datos al usuario de forma que sean fácilmente interpretables y manipulables, por lo que normalmente se dice que la vista es la interfaz de usuario. La vista genera una representación de los datos obtenidos del modelo. Es por tanto la parte del patrón que se encarga de la interacción con el usuario, mostrando datos, formularios y otros elementos.
Modelo. Los componentes del modelo son los encargados de:
Manejar los datos propios de la aplicación y la lógica de los mismos, por lo que encaja dentro de la capa de lógica de negocio, siendo esta su función principal. Los componentes del modelo implementan los mecanismos para obtener, modificar, eliminar o crear datos que maneja la aplicación siguiendo la lógica de la aplicación. Si la aplicación utiliza algún tipo de almacenamiento para almacenar de forma persistente la información que maneja la aplicación (como unSGBD), tendrá que encargarse de almacenarla y recuperarla.
Dependiendo de la implementación, también puede encargarse de comunicar a la vista posibles actualizaciones de los datos. Es decir, cuando se produce un cambio en el modelo, el modelo puede notificar a la vista para que esta se actualice.
Controlador. El controlador recibe los eventos de usuario y los datos (por ejemplo, recibe el evento y los datos cuando el usuario recibe un formulario). En función del evento el controlador puede:
Comunicarse con el modelo para modificar o actualizar los datos gestionados por la aplicación.
Comunicarse con la vista para actualizarla en función de la acción de usuario.
Este patrón tiene su origen en los años 80, en aplicaciones de escritorio, cuyo funcionamiento es inherentemente diferente a las aplicaciones web. Sin embargo, puede aplicarse y adaptarse a las aplicaciones web. En general, en las aplicaciones web el rol de controlador es más activo, y realmente hace de intermediario entre la vista y el modelo.
Para contextualizar el patrón MVC vamos a imaginar el siguiente caso de uso, se trata de un caso muy simple y que nos permitirá aproximarnos a una implementación de este patrón:
Lista de productos favoritos. Un usuario puede tener su lista de productos favoritos, permitiendo realizar tres acciones diferenciadas:
Mostrar la lista de productos favoritos.
Añadir producto a la lista de favoritos.
Eliminar producto de la lista de favoritos.
Veamos como se podrían estructurar los componentes siguiendo el patrón MVC:
Tipos de componentes en el patrón MVC
Modelo
Controlador
Vista
El modelo podría estar compuesto por una serie de clases destinadas a gestionar los datos de los productos favoritos del usuario. Por ejemplo, podríamos tener las siguientes clases:
Producto: clase que contiene datos de un producto.
Usuario: clase que permite gestionar los datos de un usuario.
Favorito: clase que gestiona la relación de producto favorito de un usuario. Esta clase permitiría obtener la lista de productos favoritos, agregar un producto nuevo o eliminar un producto de la lista de favoritos de un usuario concreto.
Las funciones de un controlador en este caso serían coordinar diversas operaciones que se podrían hacer con el modelo, proporcionando a la vista los datos necesarios, estas operaciones podría ser:
Gestionar la acción de mostrar la lista de productos favoritos. En esta acción simplemente se cargaría la vista con los datos de los productos favoritos de un usuario previamente autenticado.
Añadir un producto a la lista de favoritos. Esta acción requeriría:
Recoger los datos recibidos por el usuario que contendrían los datos del producto a añadir a la lista de favoritos, comprobando que los datos recibidos son válidos.
Modificar el modelo, añadiendo un producto a la lista de favoritos.
Actualizar la vista, indicando al usuario si el producto ha sido añadido a la lista de favoritos o no.
Eliminar un producto a la lista de favoritos, que requerirá:
Recoger los datos recibidos por el usuario, que contendrá el id de producto a eliminar de la lista de favoritos.
Modificar el modelo, borrando un producto de la lista de favoritos.
Actualizar la vista, indicando al usuario si el producto ha sido borrado de la lista de favoritos o no.
En este caso tendríamos varias vistas. Lo mejor para comenzar es crear vistas muy segmentadas, donde cada vista tendrá una función específica que no se mezcle con el resto, esto te dará una visión más clara del modelo. Las vistas que podríamos manejar son:
Vista con la lista de productos, donde el usuario puede seleccionar un producto a añadir a su lista de favoritos.
Vista para mostrar el resultado de la acción de añadir un producto a la lista de favoritos.
Vista de la lista de productos favoritos. Esta vista el usuario estaría destinada a mostrar al usuario su lista de productos favoritos y podría haber un botón para eliminar de la lista de favoritos.
Vista para mostrar el resultado de la acción de eliminar de la lista de favoritos.
Aunque hoy día el patrón más mencionado en la literatura de desarrollo web es el patrón MVC, existen otros patrones interesantes. Es el caso del patrón MVP (Modelo - Vista - Presentador).
El patrón MVP deriva del patrón MVC y tiene como principal característica que distingue separa las responsabilidades en tres partes: modelo, vista y presentador. Uno de los aspectos más característicos de este patrón es que la vista y el modelo no se comunican a priori directamente entre sí, sino que lo hacen a través del presentador, algo que en teoría permite depurar y probar mejor las aplicaciones. Veamos a groso modo que responsabilidad tiene cada componente en este patrón.
El modelo en MVP tiene responsabilidades similares al modelo en MVC, con la salvedad de que no va a comunicarse con la vista. La vista, por su lado, será la encargada de mostrar la interfaz de usuario y de recibir los eventos de dicha interfaz (por ejemplo, cuando se envía un formulario).
Cuando la vista recibe un evento de usuario, incluidos sus datos, los redirecciona al presentador. Será en los componentes tipo presentador donde tendremos que programar que se hará con dichos datos, realizando tareas tales como validación de datos o formateado de los mismos. Si es necesario, el presentador puede comunicarse con el modelo para que este realice las operaciones de persistencia oportunas.
El presentador también proporcionará los datos necesarios para generar la vista. Si se necesita un listado de productos por ejemplo, será el presentador el encargado de utilizar el modelo para obtener dichos datos, los transformará y se los facilitará a la vista para que genere la interfaz de usuario, evitando así el contacto entre el modelo y la vista.
Obviamente existen muchas variaciones de este patrón y aquí solo se ha realizado una aproximación general al mismo, esperamos que te sirva de ayuda.
A la hora de estructurar una aplicación siguiendo el patrón MVC la parte más sencilla es comenzar diseñando el modelo. Básicamente vamos a diseñar clases que reciban datos por parámetros y que actualicen la información de la base de datos en función de la lógica de la aplicación. Estos métodos no van a mostrar nada por pantalla, simplemente actualizarán el modelo. Para poder hacer esto, obviamente vamos a necesitar una estructura de la base de datos, para dar sentido a estas operaciones. Esta estructura podría ser similar a la siguiente:
CREATE TABLE `usuarios` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(45) NOT NULL,
`password` varchar(300) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username_UNIQUE` (`username`)
);
CREATE TABLE `productos` (
`id` int NOT NULL AUTO_INCREMENT,
`cod` varchar(45) NOT NULL,
`descripcion` tinytext,
`precio` float NOT NULL,
`stock` int NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `cod_UNIQUE` (`cod`)
);
CREATE TABLE `favoritos` (
`producto_id` int NOT NULL,
`usuario_id` int NOT NULL,
PRIMARY KEY (`producto_id`,`usuario_id`),
KEY `fk_favoritos_productos_idx` (`producto_id`),
KEY `fk_favoritos_usuarios1_idx` (`usuario_id`),
CONSTRAINT `fk_favoritos_productos` FOREIGN KEY (`producto_id`) REFERENCES `productos` (`id`),
CONSTRAINT `fk_favoritos_usuarios1` FOREIGN KEY (`usuario_id`) REFERENCES `usuarios` (`id`)
);
Básicamente vamos a tener tres tablas y vamos a crear una clase para gestionar cada una de esas tablas. Esto no tiene porqué ser siempre así, pero viene bien para aproximarnos al patrón MVC. Por ejemplo, podríamos tener una clase llamada Producto, destinada a manejar las operaciones con el producto:
class Producto {
private $id=null;
private $cod;
private $descripcion;
private $precio;
private $stock;
/* métodos getters y setters */
public function guardar(): bool | int { ... }
public static function rescatar(int|string $idOrCod): object | null | false { ... }
public static function listar(): ?array { ... }<br>
}
En el código anterior se ocultan el contenido de los métodos, lo podrás consultar más adelante en el proyecto de ejemplo. Fíjate que se trata de métodos para realizar operaciones básicas con productos controlando que sus datos son válidos: guardar producto, rescatar o listar.
También podríamos tener una clase llamada Usuario destinada a gestionar los datos de un usuario concreto y a validarlo. Por ejemplo:
class Usuario {
private $id;
private $username;
/* métodos getters y setters, y constructor */
public static function verificarUsuario($username, $password): ?Usuario
: bool | int { ... }
public function modificarPassword($currentpassword, $newpassword) { ... }
public static function crearUsuario ($username, $password): ?Usuario { ... }
}
Y por último, podríamos tener una clase que estableciera la relación de "favorito" entre Usuario y Producto, a la que vamos a llamar Favorito, y cuya estructura podría ser algo como lo que sigue:
class Favorito {
public static function nuevoFavorito(Usuario $u, Producto $p) { ... }
public static function obtenerFavoritos (Usuario $u) { ... }
public static function borrarFavorito (Usuario $u, int|Producto $product_id) { ... }
}
Nuevamente, esta clase está destinada a manipular el modelo. Dado un usuario permitiría: obtener sus favoritos, añadir un nuevo favorito o borrar un producto que ya esté como favorito.
En el siguiente recurso del Marco de Desarrollo de la Junta de Andalucía (MADEJA) se describe la aplicación del patrón MVC en PHP. En esta página encontrarás un ejemplo sencillo pero completo de aplicación del patrón MVC en PHP. Échale un vistazo y analízalo con cuidado:
El ejemplo que desarrollamos en este y siguientes apartados sigue las mismas premisas pero está adaptado para ser más didáctico y parecido a como se realiza en frameworks actuales..
La vista será básicamente una parte del código que recibirá una serie de datos y los mostrará por pantalla. Desde un punto de vista práctico podríamos dividir la vista de la siguiente forma:
Ejemplo de separación de la lógica de negocio y aplicación de la lógica de presentación (vista)
Invocación de la vista
Ejemplo de archivo de vista llamado vista1.php
<?php
//Obtenemos los datos que pasaremos a la vista
$datos['dato1']='A';
$datos['dato2']='B';
//incluimos el archivo de la vista pasándole los datos aislándolo del código principal
function mostrarVista($nombreArchivo, $params)
{
extract($params,EXTR_OVERWRITE);
include($nombreArchivo);
}
mostrarVista('vista1.php', $datos);
En la parte de la izquierda tienes un pequeño código que haría uso de una vista muy sencilla (vista1.php). A esa vista se le pasa unos datos concretos (almacenados en el array $datos) y su única responsabilidad es la de mostrar los datos por pantalla, es decir, la de generar la interfaz de usuario. En un ejemplo como el anterior mantenemos separada la lógica de la aplicación y de negocio (izquierda), de la lógica de presentación (derecha).
La idea de separar la vista facilita la creación de aplicaciones web con menos errores y mejor gestionables. Normalmente, aunque utilizar vistas como las anteriores (desarrolladas directamente en PHP) es posible, normalmente se utilizan motores de plantillas.
Un motor de plantillas es una librería capaz de generar una interfaz de usuario (en HTML generalmente) a partir de un fichero denominado plantilla o template en la que se indica programáticamente como generar la interfaz de usuario. Actualmente, todos o casi todos los frameworks MVC hacen uso de motores de plantillas (por ejemplo, Laravel usa el motor de plantillas Blade).
Usando motores de plantillas es sencillo dividir el trabajo de programación de una aplicación web en dos perfiles: programación, que debe conocer el lenguaje de programación en el que se implementará la lógica de la aplicación (en nuestro caso PHP), y diseño, que se encargará de elaborar las plantillas en un lenguaje específico para plantillas.
Un ejemplo de motor de plantillas es Smarty, el cual es de código abierto y está disponible bajo licencia LGPL:
Entre las características de Smarty cabe destacar:
Permite la inclusión en las plantillas de una lógica de presentación compleja.
Acelera la generación de la página web resultante. Uno de los problemas de los motores de plantillas es que su utilización influye negativamente en el rendimiento. Smarty convierte internamente las plantillas a guiones PHP equivalentes y posibilita el almacenamiento del resultado obtenido en memoria temporal.
Al ser usado por una amplia comunidad de desarrolladores y desarrolladoras existen multitud de ejemplos y foros para la resolución de los problemas que te vayas encontrando al utilizarlo.
Actualmente, la mejor forma de incorporar Smarty en nuestro proyecto es haciendo uso de Composer:
composer require smarty/smarty
Después, hacer uso de Smarty es relativamente sencillo, veamos un ejemplo rápido:
Ejemplo de uso de vista con Smarty
Invocación de la vista (index.php)
Ejemplo de archivo de vista llamado 'plantillas/vista1.tpl'
Para que Smarty funcione adecuadamente necesita al menos tres directorios:
$smarty->template_dir: aquí indicaremos el directorio donde vamos a poner nuestras vistas. Al configurar este directorio solo tendremos que poner el nombre de la plantilla ($smarty->display('vista1.tpl')) y no la ruta completa hasta la misma. En el ejemplo es el directorio plantillas, por eso la plantilla se crea en el directorio plantillas/vista1.tpl.
$smarty->compile_dir: aquí indicaremos el directorio donde Smarty puede depositar las plantillas compiladas. No tenemos que hacer nada en ese directorio, solo crearlo. Será un directorio que Smarty usará internamente. Por cierto, no tiene porqué ser un directorio dentro del proyecto, puede estar en otra ubicación.
$smarty->cache: aquí indicaremos un directorio donde Smarty puede almacenar archivos temporales. Nuevamente, no tenemos que hacer nada en ese directorio, solo crearlo. Smarty lo gestionará de forma interna.
La estructura de proyecto por tanto debería ser algo así:
Para finalizar, también es interesante conocer al menos como:
Poner comentarios. Van encerrados entre asteriscos.
{* Este es un comentario de plantilla en Smarty *}
Incluir otras plantillas.Smartypermite descomponer una plantilla compleja en trozos más pequeños y almacenarlos en otras plantillas, que se incluirán en la actual utilizando la sentenciainclude.
Una vez diseñado el modelo y previstas como van a ser las vistas, es el momento de pensar en los controladores. La función de los controladores será en nuestro caso procesar los datos recibidos, determinar como modificar el modelo y enviar los datos necesario recibidos del modelo a la vista.
Toda esa funcionalidad se agrupa generalmente en una función o método de una clase. Por ejemplo, la acción de añadir un producto a favorito implicaría realizar lo siguientes pasos:
Comprobar si el usuario se ha autenticado, si no es así, el usuario no podría continuar.
Recoger el identificador del producto a añadir y comprobar que corresponde a un producto existente.
Actualizar el modelo, en caso de que el producto exista, usando la clase que agrega un nuevo favorito al usuario autenticado.
Recoger el resultado de las acciones y comunicarlas a la vista.
De forma simplificada, podría ser, más o menos, el siguiente trozo de código:
<?php
class Controladores
{
public static function addtofav(Smarty $smarty)
{
//Control de acceso: área restringida a usuarios autenticados
if (!isset($_SESSION['usuario'])) {
return;
}
//Validación y saneado de datos
$idproducto=filter_input(INPUT_POST,'idprod',FILTER_VALIDATE_INT);
$resultado=false;
if ($idproducto!==null && $idproducto!==false)
{
//Si los datos son correctos, manipulamos el modelo
$p=Producto::rescatar($idproducto);
if ($p instanceof Producto) {
$resultado=Favorito::nuevoFavorito($_SESSION['usuario'],$p);
}
}
//Una vez actualizado el modelo, pasamos los datos a la vista
$smarty->assign('resultado',$resultado);
$smarty->display('add_to_fav_result.tpl');
}
}
Lo importante aquí es que observes como en el controlador no se accede a la base de datos (eso se hace en el modelo), ni se muestran datos por pantalla (eso se hace en la vista), sino que simplemente se organiza la relación entre modelo y vista. El controlador hace las veces de director de orquesta.
Un solo controlador de forma aislada puede decirte poco, la clave es cuando tenemos múltiples controladores cada uno encargado de una operación:
class Controladores
{
/** Acción por defecto: mostrar lista de productos. En caso de que el usuario esté
autenticado se añadira un botón para agregar un producto a favoritos, llevando
la ejecución al controlador addtofav */
public static function default(Smarty $s) { ... }
/** Autenticar un usuario. Se muestra el formulario de login y luego se recogen
los datos, autenticado al usuario en caso de que sean correctos. */
public static function autenticar($smarty) { ... }
/** Añadir producto a favorito. Se recogen los datos del producto seleccionado por el usuario. Si son correctos y el usuario está autenticado, se añade a favoritos. */
public static function addtofav(Smarty $smarty) { ... }
/** Lista los productos del usuario autenticado añadiendo un botón para eliminar el producto de la lista de favoritos (conduce a removefromfav). */
public static function listfavs(Smarty $smarty) { ... }
/** Elimina el producto de favoritos. */
public static function removefromfav (Smarty $smarty) {...}
/** Realiza el cierre de sesión. */
public static function logout() { ... }
}
Una vez conocemos como implementar modelos, vistas y controladores, llega el momento de unirlo todo de forma que tenga sentido. Para ello dotaremos a nuestra aplicación de un punto de entrada o punto de conexión (también llamado endpoint en inglés), es decir, un script que se encargará de determinar que controlador debe ejecutarse en cada momento. A este punto de entrada lo llamamos normalmente enrutador.
El enrutador será el script que recibirá todas las peticiones HTTP. En vez de tener múltiples puntos de entrada, tendremos uno solo. Por ejemplo, podemos determinar que el punto de entrada de nuestra aplicación será el archivo index.php. Todas las peticiones HTTP irán dirigidas a dicho archivo, y en él se determinará que controlador ejecutar.
A la hora de determinar que controlador ejecutar podemos distinguir dos técnicas principales:
Encapsular la operación en un parámetro GET. Por ejemplo: http://localhost/dwes04/index.php?accion=addtofav. Esta técnica es la más simple, requiere analizar un parámetro GET y a partir de él determinar que operación a realizar.
Encapsular la operación en la ruta al recurso. Por ejemplo: http://localhost/dwes04/addtofav. Esta técnica requiere:
Configurar Apache, o el servidor web que estemos usando, para que redireccione todas las peticiones al punto de entrada (por ejemplo: index.php).
En el script que actúa de punto de entrada se debe analizar la ruta, contenida en $_SERVER['REQUEST_URI'] y que contendrá la parte de la ruta que nos interesa: /dwes04/addtofav. De ahí, hay que extraer la operación (addtofav) y otros datos que pudiera haber, e invocar al controlador asociado a dicha ruta.
Sea cual sea el estilo elegido, la idea es extraer de la información de la petición el controlador a invocar. La primera forma es más sencilla para nosotros, dado que no requiere analizar la ruta al recurso para determinar el controlador a invocar, por lo que nos centraremos en ese escenario. No obstante, el segundo tipo es el más usado, dado que es el que da más versatilidad, por lo que la mayor parte de los frameworks actuales (por no decir todos), trabajan de esa segunda forma.
Una vez determinada la operación a realizar, en el script de punto de entrada (index.php en nuestro caso), tendríamos un código como el siguiente:
$accion = $_GET['accion'] ?? '';
switch ($accion) {
case "logout":
Controladores::logout();
break;
case "autenticar":
Controladores::autenticar($smarty);
break;
case "addtofav":
Controladores::addtofav($smarty);
break;
case "listfavs":
Controladores::listfavs($smarty);
break;
case "removefromfav":
Controladores::removefromfav($smarty);
break;
default:
Controladores::default($smarty);
break;
}
Podemos implementarlo con un simple switch o con técnicas mucho más avanzadas. Esta sería la forma más simple. Es importante que prestes atención al hecho de que en caso de que la operación no se especifique o no exista, se invocará un controlador por defecto (Controladores::default).
Después, cuando desde una vista necesitamos que los datos se envíen a un controlador u otro, simplemente encapsularemos la operación a la que deben enviarse los datos en la ruta, a través del parámetro en la ruta (por ejemplo, ?accion=addtofav):
¿Serías capaz de implementar un proyecto que implementara el patrón MVC a nivel básico? Siguiendo la línea de los ejemplos anteriores, se trataría de hacer una aplicación que:
Permitiera la autenticación de usuarios, almacenando al usuario autenticado en una sesión.
Por defecto, mostrara la lista de productos disponibles. Si el usuario está autenticado, se muestra un botón en cada producto para agregarlo a la lista de favoritos.
Permita a un usuario autenticado ver su lista de favoritos y eliminar productos de su lista de favoritos.
Realizar esto con un motor de plantillas (como Smarty) y ordenando el código es el propósito de este apartado.
Materiales desarrollados inicialmente por el Ministerio de Educación, Cultura y Deporte y actualizados por el profesorado de la Junta de Andalucía bajo licencia Creative Commons BY-NC-SA.
Antes de cualquier uso leer detenidamente el siguenteAviso legal
Actualización de materiales y correcciones menores.
Versión: 02.00.00
Fecha de actualización: 31/05/23
Autoría: Salvador Romero Villegas
Ubicación: Dividir esta unidad en dos. Mejora (tipo 3): Actualmente esta unidad no trata adecuadamente (por no decir que no trata) los criterios de evaluación CE5.c, CE5.d, CE5.e y CE5.f, relacionados con los patrones MVC y su puesta en uso. Es por ello que sugiero realizar lo siguiente: dividir esta unidad en dos unidades.
Nueva unidad 4: Programación orietada a objetos y por capas en PHP (centrada en los CEs RA5.a, RA5.b, RA5.g y RA5.h)
Nueva unidad 5: Desarrollo de aplicaciones web en MVC en aravel (centrada en los CEs RA5.c RA5.d, RA5.e y RA5.f)
Mover la unidad 5 actual (Servicios web) a la unidad 6 (sería la nueva unidad 6)
Esto permitiría dar la necesaria a la programación orientada a objetos, sumamente necesaria en la para usar el patrón MVC tal y como indica la normativa. En la nueva unidad 4 se podría aprovechar parte de lo ya creado, pero se tendría que modificar aproximadamente el 80% del contenido actual (puede que más). Para la nueva unidad 5 no se podría aprovechar nada de lo existente (dado que el contenido está desactualizado y no se adhiere a normativa), con lo que habría que modificar el 100% del contenido.
Esta dos nuevas unidades deberían tener una carga horaria mayor (dado que el patrón MVC es fundamental hoy día en el diseño de aplicaciones web en entorno servidor). Se propone la siguiente distribución horaria: nueva unidad 4 = 30 horas, nueva unidad 5 = 23 horas.
Para la nueva unidad 4 se intentaría aprovechar lo que hay, pero sería necesario:
- Mejorar la parte de orientación a objetos existentes (orientando la creación del proyecto a autoloading).
- Insertar un ejemplo de uso de clases en un aplicación real (con sus formularios y demás)
- Explicar que es un objeto (sin clase)
- Explicar JSON y conversión de objetos a JSON, viceversa (necesario para webservices más adelante)
- Añadir el uso de traits.
- Añadir el uso de namespaces.
- Añadir el uso de autoloading y PSR-4
- Añadir apartado “gestor de dependencias” que explique el uso de composer: crear un proyecto con composer, añadir dependencias, instalar dependencias, usar dependencias, etc.
- Añadir un ejemplo de proyecto real usando composer, namespaces, etc.
- Añadir una sección de preparación para la siguiente unidad (MVC), donde se exponga: programación por capas (capas como lógica de negocio - BLL, DAL, three-tier, etc.), patrones de diseño, patrones de diseño usados en la web, patrón mvc y sus carácteristicas, estructura de aplicaiones mvc (partes y carácteristicas), ejemplo de diseño básico con mvc, y frameworks que implementan MVC.
Para la nueva unidad 5 se propone un contenido centrado fundamentalmente en el uso de Laravel (que debería ser al menos lo siguiente):
- Instalación de Laravel y descripción de la estructura de aplicación.
- Configuración.
- Enrutado.
- Creación de controladores y validadores.
- Inyección de dependencias.
- Implementación de clases en el modelo.
- Vistas con Blade templates
Ubicación: Mapa conceptual Mejora (Mapa conceptual): Reestructuración del mapa conceptual debido a una reestructuración completa del mapa conceptual.
Ubicación: Orientaciones Mejora (Orientaciones del alumnado): Modificación en las orientaciones debido a una restructuración de los contenidos.