Introducción a la programación orientada a objetos

El término “POO” se refiere a la programación orientada a objetos, que es una forma de organizar y estructurar el código. La POO nos permite ver un programa como un conjunto de objetos que se comunican entre sí, en lugar de una secuencia de comandos y funciones.

En POO, un “objeto” es una unidad que contiene datos y funciones que trabajan con esos datos. Los objetos se crean a partir de “clases”, que podemos entender como planos o plantillas para los objetos. Cuando tenemos una clase, podemos crear su “instancia”, que es un objeto concreto creado a partir de esa clase.

Veamos cómo podemos crear una clase simple en PHP. Al definir una clase, usamos la palabra clave “class”, seguida del nombre de la clase y luego llaves que encierran las funciones (llamadas “métodos”) y las variables de la clase (llamadas “propiedades”):

class Coche
{
	function tocarBocina()
	{
		echo '¡Bip bip!';
	}
}

En este ejemplo, hemos creado una clase llamada Coche con una función (o “método”) llamada tocarBocina.

Cada clase debe abordar solo una tarea principal. Si una clase hace demasiadas cosas, puede ser apropiado dividirla en clases más pequeñas y especializadas.

Normalmente, guardamos las clases en archivos separados para mantener el código organizado y fácil de navegar. El nombre del archivo debe coincidir con el nombre de la clase, por lo que para la clase Coche, el nombre del archivo sería Coche.php.

Al nombrar las clases, es bueno seguir la convención “PascalCase”, lo que significa que cada palabra en el nombre comienza con una letra mayúscula y no hay guiones bajos ni otros separadores entre ellas. Los métodos y propiedades usan la convención “camelCase”, lo que significa que comienzan con una letra minúscula.

Algunos métodos en PHP tienen tareas especiales y se marcan con el prefijo __ (dos guiones bajos). Uno de los métodos especiales más importantes es el “constructor”, que se marca como __construct. El constructor es un método que se llama automáticamente cuando se crea una nueva instancia de la clase.

A menudo usamos el constructor para establecer el estado inicial del objeto. Por ejemplo, al crear un objeto que representa a una persona, puedes usar el constructor para establecer su edad, nombre u otras propiedades.

Veamos cómo usar un constructor en PHP:

class Persona
{
	private $edad;

	function __construct($edad)
	{
		$this->edad = $edad;
	}

	function cuantosAnosTiene()
	{
		return $this->edad;
	}
}

$persona = new Persona(25);
echo $persona->cuantosAnosTiene(); // Imprime: 25

En este ejemplo, la clase Persona tiene una propiedad (variable) $edad y además un constructor que establece esta propiedad. El método cuantosAnosTiene() permite acceder a la edad de la persona.

La pseudovariable $this se usa dentro de la clase para acceder a las propiedades y métodos del objeto.

La palabra clave new se usa para crear una nueva instancia de la clase. En el ejemplo anterior, creamos una nueva persona con 25 años.

También puedes establecer valores predeterminados para los parámetros del constructor si no se especifican al crear el objeto. Por ejemplo:

class Persona
{
	private $edad;

	function __construct($edad = 20)
	{
		$this->edad = $edad;
	}

	function cuantosAnosTiene()
	{
		return $this->edad;
	}
}

$persona = new Persona;  // si no pasamos ningún argumento, se pueden omitir los paréntesis
echo $persona->cuantosAnosTiene(); // Imprime: 20

En este ejemplo, si no especificas la edad al crear el objeto Persona, se usará el valor predeterminado de 20.

Es conveniente que la definición de la propiedad con su inicialización a través del constructor se pueda acortar y simplificar de esta manera:

class Persona
{
	function __construct(
		private $edad = 20,
	) {
	}
}

Para completar, además de los constructores, los objetos también pueden tener destructores (método __destruct), que se llaman antes de que el objeto sea liberado de la memoria.

Espacios de nombres

Los espacios de nombres (o “namespaces” en inglés) nos permiten organizar y agrupar clases, funciones y constantes relacionadas, evitando al mismo tiempo conflictos de nombres. Puedes imaginarlos como carpetas en tu computadora, donde cada carpeta contiene archivos que pertenecen a un proyecto o tema específico.

Los espacios de nombres son especialmente útiles en proyectos más grandes o cuando usas librerías de terceros, donde podrían surgir conflictos en los nombres de las clases.

Imagina que tienes una clase llamada Coche en tu proyecto y quieres colocarla en un espacio de nombres llamado Transporte. Lo harías así:

namespace Transporte;

class Coche
{
	function tocarBocina()
	{
		echo '¡Bip bip!';
	}
}

Si quieres usar la clase Coche en otro archivo, debes especificar de qué espacio de nombres proviene la clase:

$coche = new Transporte\Coche;

Para simplificar, puedes indicar al principio del archivo qué clase del espacio de nombres dado quieres usar, lo que permite crear instancias sin necesidad de especificar la ruta completa:

use Transporte\Coche;

$coche = new Coche;

Herencia

La herencia es una herramienta de la programación orientada a objetos que permite crear nuevas clases basadas en clases ya existentes, heredando sus propiedades y métodos, y extendiéndolos o redefiniéndolos según sea necesario. La herencia permite asegurar la reutilización del código y la jerarquía de clases.

En pocas palabras, si tenemos una clase y quisiéramos crear otra derivada de ella, pero con algunos cambios, podemos “heredar” la nueva clase de la clase original.

En PHP, la herencia se realiza usando la palabra clave extends.

Nuestra clase Persona almacena información sobre la edad. Podemos tener otra clase Estudiante que extiende Persona y agrega información sobre el campo de estudio.

Veamos un ejemplo:

class Persona
{
	private $edad;

	function __construct($edad)
	{
		$this->edad = $edad;
	}

	function imprimirInformacion()
	{
		echo "Edad: {$this->edad} años\n";
	}
}

class Estudiante extends Persona
{
	private $especialidad;

	function __construct($edad, $especialidad)
	{
		parent::__construct($edad);
		$this->especialidad = $especialidad;
	}

	function imprimirInformacion()
	{
		parent::imprimirInformacion();
		echo "Campo de estudio: {$this->especialidad} \n";
	}
}

$estudiante = new Estudiante(20, 'Informática');
$estudiante->imprimirInformacion();

¿Cómo funciona este código?

  • Usamos la palabra clave extends para extender la clase Persona, lo que significa que la clase Estudiante hereda todos los métodos y propiedades de Persona.
  • La palabra clave parent:: nos permite llamar a métodos de la clase padre. En este caso, llamamos al constructor de la clase Persona antes de agregar nuestra propia funcionalidad a la clase Estudiante. Y de manera similar, también al método imprimirInformacion() del padre antes de imprimir la información del estudiante.

La herencia está destinada a situaciones en las que existe una relación “es un” entre clases. Por ejemplo, un Estudiante es una Persona. Un gato es un animal. Nos da la posibilidad, en casos donde el código espera un objeto (por ejemplo, “Persona”), de usar en su lugar un objeto heredado (por ejemplo, “Estudiante”).

Es importante darse cuenta de que el propósito principal de la herencia no es evitar la duplicación de código. Por el contrario, el uso incorrecto de la herencia puede llevar a un código complejo y difícil de mantener. Si no existe una relación “es un” entre las clases, deberíamos considerar la composición en lugar de la herencia.

Observa que los métodos imprimirInformacion() en las clases Persona y Estudiante imprimen información ligeramente diferente. Y podemos agregar otras clases (por ejemplo, Empleado) que proporcionarán otras implementaciones de este método. La capacidad de los objetos de diferentes clases para responder al mismo método de diferentes maneras se llama polimorfismo:

$personas = [
	new Persona(30),
	new Estudiante(20, 'Informática'),
	new Empleado(45, 'Director'), // Suponiendo que existe una clase Empleado similar
];

foreach ($personas as $persona) {
	$persona->imprimirInformacion();
}

Composición

La composición es una técnica en la que, en lugar de heredar propiedades y métodos de otra clase, simplemente usamos su instancia en nuestra clase. Esto nos permite combinar funcionalidades y propiedades de múltiples clases sin necesidad de crear estructuras de herencia complejas.

Veamos un ejemplo. Tenemos una clase Motor y una clase Coche. En lugar de decir “Coche es un Motor”, decimos “Coche tiene un Motor”, que es una relación típica de composición.

class Motor
{
	function encender()
	{
		echo 'Motor en marcha.';
	}
}

class Coche
{
	private $motor;

	function __construct()
	{
		$this->motor = new Motor;
	}

	function arrancar()
	{
		$this->motor->encender();
		echo '¡El coche está listo para conducir!';
	}
}

$coche = new Coche;
$coche->arrancar();

Aquí, Coche no tiene todas las propiedades y métodos de Motor, pero tiene acceso a él a través de la propiedad $motor.

La ventaja de la composición es una mayor flexibilidad en el diseño y una mejor capacidad de modificación en el futuro.

Visibilidad

En PHP, puedes definir la “visibilidad” para propiedades, métodos y constantes de una clase. La visibilidad determina desde dónde puedes acceder a estos elementos.

  1. Public: Si un elemento está marcado como public, significa que puedes acceder a él desde cualquier lugar, incluso fuera de la clase.
  2. Protected: Un elemento marcado como protected solo es accesible dentro de la clase dada y todos sus descendientes (clases que heredan de esta clase).
  3. Private: Si un elemento es private, solo puedes acceder a él desde dentro de la clase en la que fue definido.

Si no especificas la visibilidad, PHP la establecerá automáticamente en public.

Veamos un código de ejemplo:

class EjemploVisibilidad
{
	public $propiedadPublica = 'Pública';
	protected $propiedadProtegida = 'Protegida';
	private $propiedadPrivada = 'Privada';

	public function imprimirPropiedades()
	{
		echo $this->propiedadPublica;  // Funciona
		echo $this->propiedadProtegida; // Funciona
		echo $this->propiedadPrivada; // Funciona
	}
}

$objeto = new EjemploVisibilidad;
$objeto->imprimirPropiedades();
echo $objeto->propiedadPublica;      // Funciona
// echo $objeto->propiedadProtegida;  // Lanza un error
// echo $objeto->propiedadPrivada;  // Lanza un error

Continuamos con la herencia de clases:

class ClaseHija extends EjemploVisibilidad
{
	public function imprimirPropiedades()
	{
		echo $this->propiedadPublica;   // Funciona
		echo $this->propiedadProtegida;  // Funciona
		// echo $this->propiedadPrivada;  // Lanza un error
	}
}

En este caso, el método imprimirPropiedades() en la clase ClaseHija puede acceder a las propiedades públicas y protegidas, pero no puede acceder a las propiedades privadas de la clase padre.

Los datos y métodos deben estar lo más ocultos posible y ser accesibles solo a través de una interfaz definida. Esto te permitirá cambiar la implementación interna de la clase sin afectar al resto del código.

Palabra clave final

En PHP, podemos usar la palabra clave final si queremos evitar que una clase, método o constante sea heredada o sobrescrita. Cuando marcamos una clase como final, no puede ser extendida. Cuando marcamos un método como final, no puede ser sobrescrito en una clase descendiente.

Saber que una clase o método en particular no será modificado posteriormente nos permite realizar cambios más fácilmente sin tener que preocuparnos por posibles conflictos. Por ejemplo, podemos agregar un nuevo método sin temor a que alguno de sus descendientes ya tenga un método con el mismo nombre y se produzca una colisión. O podemos modificar los parámetros de un método, ya que nuevamente no hay riesgo de causar una inconsistencia con un método sobrescrito en un descendiente.

final class ClaseFinal
{
}

// El siguiente código provocará un error, porque no podemos heredar de una clase final.
class HijaClaseFinal extends ClaseFinal
{
}

En este ejemplo, intentar heredar de la clase final ClaseFinal provocará un error.

Propiedades y métodos estáticos

Cuando hablamos de elementos “estáticos” de una clase en PHP, nos referimos a métodos y propiedades que pertenecen a la clase misma, y no a una instancia específica de esa clase. Esto significa que no necesitas crear una instancia de la clase para acceder a ellos. En su lugar, los llamas o accedes a ellos directamente a través del nombre de la clase.

Ten en cuenta que, dado que los elementos estáticos pertenecen a la clase y no a sus instancias, no puedes usar la pseudovariable $this dentro de métodos estáticos.

El uso de propiedades estáticas conduce a código confuso lleno de trampas, por lo que nunca deberías usarlas y ni siquiera mostraremos un ejemplo de uso aquí. Por el contrario, los métodos estáticos son útiles. Ejemplo de uso:

class Calculadora
{
	public static function sumar($a, $b)
	{
		return $a + $b;
	}

	public static function restar($a, $b)
	{
		return $a - $b;
	}
}

// Uso del método estático sin crear una instancia de la clase
echo Calculadora::sumar(5, 3); // Resultado: 8
echo Calculadora::restar(5, 3); // Resultado: 2

En este ejemplo, creamos una clase Calculadora con dos métodos estáticos. Podemos llamar a estos métodos directamente sin crear una instancia de la clase usando el operador ::. Los métodos estáticos son especialmente útiles para operaciones que no dependen del estado de una instancia específica de la clase.

Constantes de clase

Dentro de las clases, tenemos la opción de definir constantes. Las constantes son valores que nunca cambian durante la ejecución del programa. A diferencia de las variables, el valor de una constante permanece siempre igual.

class Coche
{
	public const NumeroRuedas = 4;

	public function mostrarNumeroRuedas(): int
	{
		echo self::NumeroRuedas;
	}
}

echo Coche::NumeroRuedas;  // Salida: 4

En este ejemplo, tenemos una clase Coche con la constante NumeroRuedas. Cuando queremos acceder a la constante dentro de la clase, podemos usar la palabra clave self en lugar del nombre de la clase.

Interfaces de objeto

Las interfaces de objeto funcionan como “contratos” para las clases. Si una clase va a implementar una interfaz de objeto, debe contener todos los métodos que define esa interfaz. Es una excelente manera de asegurar que ciertas clases cumplan con el mismo “contrato” o estructura.

En PHP, una interfaz se define con la palabra clave interface. Todos los métodos definidos en la interfaz son públicos (public). Cuando una clase implementa una interfaz, usa la palabra clave implements.

interface Animal
{
	function hacerSonido();
}

class Gato implements Animal
{
	public function hacerSonido()
	{
		echo 'Miau';
	}
}

$gato = new Gato;
$gato->hacerSonido();

Si una clase implementa una interfaz, pero no define todos los métodos esperados, PHP lanzará un error.

Una clase puede implementar múltiples interfaces a la vez, lo cual es una diferencia con respecto a la herencia, donde una clase solo puede heredar de una clase:

interface Guardian
{
	function vigilarCasa();
}

class Perro implements Animal, Guardian
{
	public function hacerSonido()
	{
		echo 'Guau';
	}

	public function vigilarCasa()
	{
		echo 'El perro vigila atentamente la casa';
	}
}

Clases abstractas

Las clases abstractas sirven como plantillas base para otras clases, pero no puedes crear instancias de ellas directamente. Contienen una combinación de métodos completos y métodos abstractos, que no tienen contenido definido. Las clases que heredan de clases abstractas deben proporcionar definiciones para todos los métodos abstractos del ancestro.

Para definir una clase abstracta, usamos la palabra clave abstract.

abstract class ClaseAbstracta
{
	public function metodoComun()
	{
		echo 'Este es un método común';
	}

	abstract public function metodoAbstracto();
}

class Hija extends ClaseAbstracta
{
	public function metodoAbstracto()
	{
		echo 'Esta es la implementación del método abstracto';
	}
}

$instancia = new Hija;
$instancia->metodoComun();
$instancia->metodoAbstracto();

En este ejemplo, tenemos una clase abstracta con un método común y un método abstracto. Luego tenemos una clase Hija que hereda de ClaseAbstracta y proporciona la implementación para el método abstracto.

¿Cuál es la diferencia entre interfaces y clases abstractas? Las clases abstractas pueden contener tanto métodos abstractos como concretos, mientras que las interfaces solo definen qué métodos debe implementar una clase, pero no proporcionan ninguna implementación. Una clase solo puede heredar de una clase abstracta, pero puede implementar cualquier número de interfaces.

Comprobación de tipos

En programación, es muy importante estar seguro de que los datos con los que trabajamos son del tipo correcto. En PHP, tenemos herramientas que nos aseguran esto. La verificación de si los datos tienen el tipo correcto se llama “comprobación de tipos” (type hinting).

Tipos que podemos encontrar en PHP:

  1. Tipos básicos: Incluyen int (números enteros), float (números decimales), bool (valores booleanos), string (cadenas), array (arrays) y null.
  2. Clases: Si queremos que el valor sea una instancia de una clase específica.
  3. Interfaces: Define un conjunto de métodos que una clase debe implementar. Un valor que cumple con la interfaz debe tener estos métodos.
  4. Tipos mixtos (Union Types): Podemos especificar que una variable puede tener varios tipos permitidos.
  5. Void: Este tipo especial indica que una función o método no devuelve ningún valor.

Veamos cómo modificar el código para incluir tipos:

class Persona
{
	private int $edad;

	public function __construct(int $edad)
	{
		$this->edad = $edad;
	}

	public function imprimirEdad(): void
	{
		echo "Esta persona tiene {$this->edad} años.";
	}
}

/**
 * Función que recibe un objeto de la clase Persona e imprime la edad de la persona.
 */
function imprimirEdadPersona(Persona $persona): void
{
	$persona->imprimirEdad();
}

De esta manera, hemos asegurado que nuestro código espera y trabaja con datos del tipo correcto, lo que nos ayuda a prevenir posibles errores.

Algunos tipos no se pueden escribir directamente en PHP. En tal caso, se indican en un comentario phpDoc, que es un formato estándar para documentar código PHP que comienza con /** y termina con */. Permite agregar descripciones de clases, métodos, etc. Y también indicar tipos complejos usando las llamadas anotaciones @var, @param y @return. Estos tipos son luego utilizados por herramientas de análisis estático de código, pero PHP mismo no los verifica.

class Lista
{
	/** @var array<Persona>  la anotación dice que es un array de objetos Persona */
	private array $personas = [];

	public function agregarPersona(Persona $persona): void
	{
		$this->personas[] = $persona;
	}
}

Comparación e identidad

En PHP, puedes comparar objetos de dos maneras:

  1. Comparación de valores ==: Verifica si los objetos son de la misma clase y tienen los mismos valores en sus propiedades.
  2. Identidad ===: Verifica si se trata de la misma instancia de objeto.
class Coche
{
	public string $marca;

	public function __construct(string $marca)
	{
		$this->marca = $marca;
	}
}

$coche1 = new Coche('Skoda');
$coche2 = new Coche('Skoda');
$coche3 = $coche1;

var_dump($coche1 == $coche2);   // true, porque tienen el mismo valor
var_dump($coche1 === $coche2);  // false, porque no son la misma instancia
var_dump($coche1 === $coche3);  // true, porque $coche3 es la misma instancia que $coche1

Operador instanceof

El operador instanceof permite determinar si un objeto dado es una instancia de una clase particular, un descendiente de esa clase, o si implementa una interfaz específica.

Imaginemos que tenemos una clase Persona y otra clase Estudiante, que es descendiente de la clase Persona:

class Persona
{
	private int $edad;

	public function __construct(int $edad)
	{
		$this->edad = $edad;
	}
}

class Estudiante extends Persona
{
	private string $especialidad;

	public function __construct(int $edad, string $especialidad)
	{
		parent::__construct($edad);
		$this->especialidad = $especialidad;
	}
}

$estudiante = new Estudiante(20, 'Informática');

// Verificar si $estudiante es una instancia de la clase Estudiante
var_dump($estudiante instanceof Estudiante);  // Salida: bool(true)

// Verificar si $estudiante es una instancia de la clase Persona (porque Estudiante es descendiente de Persona)
var_dump($estudiante instanceof Persona);     // Salida: bool(true)

De las salidas se desprende que el objeto $estudiante se considera simultáneamente una instancia de ambas clases: Estudiante y Persona.

Interfaces Fluidas

La “interfaz fluida” (en inglés “Fluent Interface”) es una técnica en POO que permite encadenar métodos juntos en una sola llamada. Esto a menudo simplifica y aclara el código.

El elemento clave de una interfaz fluida es que cada método en la cadena devuelve una referencia al objeto actual. Logramos esto usando return $this; al final del método. Este estilo de programación a menudo se asocia con métodos llamados “setters”, que establecen los valores de las propiedades del objeto.

Mostremos cómo puede verse una interfaz fluida en el ejemplo del envío de correos electrónicos:

public function sendMessage()
{
	$email = new Email; // Suponiendo que existe una clase Email
	$email->setFrom('sender@example.com')
		  ->setRecipient('admin@example.com')
		  ->setMessage('Hola, este es un mensaje.')
		  ->send();
}

En este ejemplo, los métodos setFrom(), setRecipient() y setMessage() sirven para establecer los valores correspondientes (remitente, destinatario, contenido del mensaje). Después de establecer cada uno de estos valores, los métodos nos devuelven el objeto actual ($email), lo que nos permite encadenar otro método después. Finalmente, llamamos al método send(), que realmente envía el correo electrónico.

Gracias a las interfaces fluidas, podemos escribir código que es intuitivo y fácil de leer.

Copia usando clone

En PHP, podemos crear una copia de un objeto usando el operador clone. De esta manera, obtenemos una nueva instancia con contenido idéntico.

Si necesitamos modificar algunas propiedades al copiar un objeto, podemos definir un método especial __clone() en la clase. Este método se llama automáticamente cuando se clona el objeto.

class Oveja
{
	public string $nombre;

	public function __construct(string $nombre)
	{
		$this->nombre = $nombre;
	}

	public function __clone()
	{
		$this->nombre = 'Clon ' . $this->nombre;
	}
}

$original = new Oveja('Dolly');
echo $original->nombre . "\n";  // Imprime: Dolly

$clon = clone $original;
echo $clon->nombre . "\n";      // Imprime: Clon Dolly

En este ejemplo, tenemos una clase Oveja con una propiedad $nombre. Cuando clonamos una instancia de esta clase, el método __clone() se encarga de que el nombre de la oveja clonada obtenga el prefijo “Clon”.

Traits

Los traits en PHP son una herramienta que permite compartir métodos, propiedades y constantes entre clases y evitar la duplicación de código. Puedes imaginarlos como un mecanismo de “copiar y pegar” (Ctrl-C y Ctrl-V), donde el contenido del trait se “inserta” en las clases. Esto te permite reutilizar código sin necesidad de crear jerarquías de clases complicadas.

Veamos un ejemplo simple de cómo usar traits en PHP:

trait TocarBocina
{
	public function tocarBocina()
	{
		echo '¡Bip bip!';
	}
}

class Coche
{
	use TocarBocina;
}

class Camion
{
	use TocarBocina;
}

$coche = new Coche;
$coche->tocarBocina(); // Imprime '¡Bip bip!'

$camion = new Camion;
$camion->tocarBocina(); // También imprime '¡Bip bip!'

En este ejemplo, tenemos un trait llamado TocarBocina que contiene un método tocarBocina(). Luego tenemos dos clases: Coche y Camion, que ambas usan el trait TocarBocina. Gracias a esto, ambas clases “tienen” el método tocarBocina(), y podemos llamarlo en objetos de ambas clases.

Los traits te permiten compartir código entre clases de manera fácil y eficiente. Sin embargo, no entran en la jerarquía de herencia, es decir, $coche instanceof TocarBocina devolverá false.

Excepciones

Las excepciones en POO nos permiten manejar errores y situaciones inesperadas en nuestro código de manera elegante. Son objetos que llevan información sobre el error o la situación inusual.

En PHP, tenemos una clase incorporada Exception, que sirve como base para todas las excepciones. Tiene varios métodos que nos permiten obtener más información sobre la excepción, como el mensaje de error, el archivo y la línea donde ocurrió el error, etc.

Cuando ocurre un error en el código, podemos “lanzar” una excepción usando la palabra clave throw.

function dividir(float $a, float $b): float
{
	if ($b === 0.0) { // Comparar floats con 0.0
		throw new Exception('¡División por cero!');
	}
	return $a / $b;
}

Cuando la función dividir() recibe cero como segundo argumento, lanza una excepción con el mensaje de error '¡División por cero!'. Para evitar que el programa se bloquee al lanzar una excepción, la capturamos en un bloque try/catch:

try {
	echo dividir(10, 0);
} catch (Exception $e) {
	echo 'Excepción capturada: '. $e->getMessage();
}

El código que puede lanzar una excepción se envuelve en un bloque try. Si se lanza una excepción, la ejecución del código se traslada al bloque catch, donde podemos procesar la excepción (por ejemplo, imprimir un mensaje de error).

Después de los bloques try y catch, podemos agregar un bloque opcional finally, que se ejecutará siempre, ya sea que se haya lanzado una excepción o no (incluso si usamos una instrucción return, break o continue en el bloque try o catch):

try {
	echo dividir(10, 0);
} catch (Exception $e) {
	echo 'Excepción capturada: '. $e->getMessage();
} finally {
	// Código que se ejecutará siempre, se lance o no una excepción
}

También podemos crear nuestras propias clases (jerarquía) de excepciones que hereden de la clase Exception. Como ejemplo, imaginemos una aplicación bancaria simple que permite realizar depósitos y retiros:

class ExcepcionBancaria extends Exception {}
class FondosInsuficientesExcepcion extends ExcepcionBancaria {}
class LimiteExcedidoExcepcion extends ExcepcionBancaria {}

class CuentaBancaria
{
	private int $saldo = 0;
	private int $limiteDiario = 1000;

	public function depositar(int $cantidad): int
	{
		$this->saldo += $cantidad;
		return $this->saldo;
	}

	public function retirar(int $cantidad): int
	{
		if ($cantidad > $this->saldo) {
			throw new FondosInsuficientesExcepcion('No hay fondos suficientes en la cuenta.');
		}

		if ($cantidad > $this->limiteDiario) {
			throw new LimiteExcedidoExcepcion('Se ha excedido el límite diario para retiros.');
		}

		$this->saldo -= $cantidad;
		return $this->saldo;
	}
}

Para un bloque try, se pueden especificar múltiples bloques catch si esperas diferentes tipos de excepciones.

$cuenta = new CuentaBancaria;
$cuenta->depositar(500);

try {
	$cuenta->retirar(1500);
} catch (LimiteExcedidoExcepcion $e) {
	echo $e->getMessage();
} catch (FondosInsuficientesExcepcion $e) {
	echo $e->getMessage();
} catch (ExcepcionBancaria $e) {
	echo 'Ocurrió un error al realizar la operación.';
}

En este ejemplo, es importante notar el orden de los bloques catch. Dado que todas las excepciones heredan de ExcepcionBancaria, si tuviéramos este bloque primero, capturaría todas las excepciones sin que el código llegara a los bloques catch siguientes. Por lo tanto, es importante tener las excepciones más específicas (es decir, las que heredan de otras) en el bloque catch más arriba en el orden que sus excepciones padre.

Iteración

En PHP, puedes recorrer objetos usando un bucle foreach, de manera similar a como recorres arrays. Para que esto funcione, el objeto debe implementar una interfaz especial.

La primera opción es implementar la interfaz Iterator, que tiene los métodos current() que devuelve el valor actual, key() que devuelve la clave, next() que se mueve al siguiente valor, rewind() que se mueve al principio y valid() que comprueba si aún no hemos llegado al final.

La segunda opción es implementar la interfaz IteratorAggregate, que solo tiene un método getIterator(). Este devuelve un objeto sustituto que se encargará de la iteración, o puede representar un generador, que es una función especial en la que se usa yield para devolver claves y valores secuencialmente:

class Persona
{
	public function __construct(
		public int $edad,
	) {
	}
}

class Lista implements IteratorAggregate
{
	private array $personas = [];

	public function agregarPersona(Persona $persona): void
	{
		$this->personas[] = $persona;
	}

	public function getIterator(): Generator
	{
		foreach ($this->personas as $persona) {
			yield $persona;
		}
	}
}

$lista = new Lista;
$lista->agregarPersona(new Persona(30));
$lista->agregarPersona(new Persona(25));

foreach ($lista as $persona) {
	echo "Edad: {$persona->edad} años \n";
}

Buenas prácticas

Una vez que tienes los principios básicos de la programación orientada a objetos, es importante centrarse en las buenas prácticas en POO. Estas te ayudarán a escribir código que no solo sea funcional, sino también legible, comprensible y fácil de mantener.

  1. Separación de responsabilidades (Separation of Concerns): Cada clase debe tener una responsabilidad claramente definida y debe abordar solo una tarea principal. Si una clase hace demasiadas cosas, puede ser apropiado dividirla en clases más pequeñas y especializadas.
  2. Encapsulación (Encapsulation): Los datos y métodos deben estar lo más ocultos posible y ser accesibles solo a través de una interfaz definida. Esto te permitirá cambiar la implementación interna de la clase sin afectar al resto del código.
  3. Inyección de dependencias (Dependency Injection): En lugar de crear dependencias directamente en la clase, deberías “inyectarlas” desde el exterior. Para una comprensión más profunda de este principio, recomendamos los capítulos sobre Inyección de Dependencias.
versión: 4.0