Introducción a la programación orientada a objetos

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

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

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

class Car
{
	function honk()
	{
		echo '¡Bip bip!';
	}
}

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

Cada clase debería resolver solo una tarea principal. Si una clase hace demasiadas cosas, puede ser adecuado dividirla en clases más pequeñas y especializadas.

Las clases se guardan normalmente en archivos separados para mantener el código organizado y fácil de recorrer. El nombre del archivo debería coincidir con el nombre de la clase, así que para la clase Car el archivo se llamaría Car.php.

Al nombrar las clases conviene seguir la convención “PascalCase”, es decir, cada palabra del nombre empieza con mayúscula y no hay guiones bajos ni otros separadores. Los métodos y las propiedades siguen la convención “camelCase”, es decir, empiezan con minúscula.

Algunos métodos de PHP tienen papeles especiales y llevan el prefijo __ (dos guiones bajos). Uno de los métodos especiales más importantes es el “constructor”, designado como __construct. El constructor es un método que se llama automáticamente al crear una instancia nueva 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, puede usar el constructor para establecer su edad, su nombre u otros atributos.

Veamos cómo usar un constructor en PHP:

class Person
{
	private $age;

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

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person(25);
echo $person->howOldAreYou(); // Imprime: 25

En este ejemplo, la clase Person tiene una propiedad (variable) $age y un constructor que establece esa propiedad. El método howOldAreYou() proporciona después acceso a la edad de la persona.

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

La palabra clave new se usa para crear una instancia nueva de la clase. En el ejemplo de arriba hemos creado una persona nueva de 25 años.

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

class Person
{
	private $age;

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

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person;  // si no se pasa ningún argumento, se pueden omitir los paréntesis
echo $person->howOldAreYou(); // Imprime: 20

En este ejemplo, si no indica la edad al crear un objeto Person, se usará el valor predeterminado 20.

Lo bonito es que la definición de la propiedad junto con su inicialización mediante el constructor se puede acortar y simplificar así:

class Person
{
	function __construct(
		private $age = 20,
	) {
	}
}

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

Espacios de nombres

Los espacios de nombres nos permiten organizar y agrupar clases, funciones y constantes relacionadas, evitando a la vez los conflictos de nombres. Puede imaginarlos como carpetas en un ordenador, donde cada carpeta contiene archivos relacionados con un proyecto o un tema concreto.

Los espacios de nombres resultan especialmente útiles en proyectos más grandes o al usar bibliotecas de terceros, donde podrían surgir conflictos entre los nombres de las clases.

Imagine que tiene en su proyecto una clase llamada Car y quiere colocarla en un espacio de nombres llamado Transport. Lo haría así:

namespace Transport;

class Car
{
	function honk()
	{
		echo '¡Bip bip!';
	}
}

Si quiere usar la clase Car en otro archivo, tiene que indicar de qué espacio de nombres proviene la clase:

$car = new Transport\Car;

Para simplificar, puede indicar al principio del archivo qué clase de qué espacio de nombres quiere usar, lo que le permite crear instancias sin mencionar la ruta completa:

use Transport\Car;

$car = new Car;

Herencia

La herencia es una herramienta de la programación orientada a objetos que permite crear clases nuevas a partir de otras existentes, heredando sus propiedades y métodos y ampliándolos o redefiniéndolos según haga falta. La herencia asegura la reutilización del código y la jerarquía de clases.

Dicho de forma sencilla, si tenemos una clase y queremos crear otra derivada de ella pero con algunas modificaciones, podemos “heredar” la clase nueva de la original.

En PHP la herencia se implementa con la palabra clave extends.

Nuestra clase Person guarda la información de la edad. Podemos tener otra clase, Student, que amplía Person y añade información sobre el campo de estudio.

Veamos un ejemplo:

class Person
{
	private $age;

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

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

class Student extends Person
{
	private $fieldOfStudy;

	function __construct($age, $fieldOfStudy)
	{
		parent::__construct($age);
		$this->fieldOfStudy = $fieldOfStudy;
	}

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

$student = new Student(20, 'Informática');
$student->printInformation();

¿Cómo funciona este código?

  • Hemos usado la palabra clave extends para ampliar la clase Person, es decir, la clase Student hereda todos los métodos y propiedades de Person.
  • La palabra clave parent:: nos permite llamar a los métodos de la clase padre. En este caso hemos llamado al constructor de la clase Person antes de añadir nuestra propia funcionalidad a la clase Student. Y, del mismo modo, al método printInformation() del antecesor antes de listar la información del estudiante.

La herencia está pensada para las situaciones en las que entre las clases hay una relación “es un”. Por ejemplo, un Student es una Person. Un gato es un animal. Nos permite, en los casos en los que el código espera un objeto (p. ej. “Person”), usar en su lugar un objeto derivado (p. ej. “Student”).

Es esencial darse cuenta de que el objetivo principal de la herencia no es evitar la duplicación de código. Al contrario, un uso indebido de la herencia puede llevar a un código complejo y difícil de mantener. Si entre las clases no hay una relación “es un”, deberíamos plantearnos la composición en lugar de la herencia.

Fíjese en que los métodos printInformation() de las clases Person y Student imprimen información ligeramente distinta. Y podemos añadir otras clases (como Employee) que proporcionarán otras implementaciones de este método. La capacidad de los objetos de distintas clases de responder al mismo método de maneras distintas se llama polimorfismo:

$people = [
	new Person(30),
	new Student(20, 'Informática'),
	new Employee(45, 'Director'),
];

foreach ($people as $person) {
	$person->printInformation();
}

Composición

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

Por ejemplo, tenemos una clase Engine y una clase Car. En lugar de decir “un coche es un motor”, decimos “un coche tiene un motor”, que es una relación de composición típica.

class Engine
{
	function start()
	{
		echo 'El motor está en marcha.';
	}
}

class Car
{
	private $engine;

	function __construct()
	{
		$this->engine = new Engine;
	}

	function start()
	{
		$this->engine->start();
		echo '¡El coche está listo para circular!';
	}
}

$car = new Car;
$car->start();

Aquí el Car no tiene todas las propiedades y métodos del Engine, pero tiene acceso a él a través de la propiedad $engine.

La ventaja de la composición es una mayor flexibilidad del diseño y una mejor adaptabilidad a los cambios futuros.

Visibilidad

En PHP puede definir la “visibilidad” de las propiedades, los métodos y las constantes de una clase. La visibilidad determina desde dónde puede acceder a esos elementos.

  1. Public: si un elemento está marcado como public, significa que puede acceder a él desde cualquier sitio, incluso fuera de la clase.
  2. Protected: un elemento marcado como protected es accesible solo dentro de la clase y de todos sus descendientes (las clases que heredan de ella).
  3. Private: si un elemento es private, solo puede acceder a él desde dentro de la clase donde se definió.

Si no indica la visibilidad, PHP la establecerá automáticamente a public.

Veamos un código de ejemplo:

class VisibilityExample
{
	public $publicProperty = 'Public';
	protected $protectedProperty = 'Protected';
	private $privateProperty = 'Private';

	public function printProperties()
	{
		echo $this->publicProperty;     // Funciona
		echo $this->protectedProperty;  // Funciona
		echo $this->privateProperty;    // Funciona
	}
}

$object = new VisibilityExample;
$object->printProperties();
echo $object->publicProperty;        // Funciona
// echo $object->protectedProperty;   // Lanza un error
// echo $object->privateProperty;     // Lanza un error

Continuando con la herencia de clases:

class ChildClass extends VisibilityExample
{
	public function printProperties()
	{
		echo $this->publicProperty;     // Funciona
		echo $this->protectedProperty;  // Funciona
		// echo $this->privateProperty;   // Lanza un error
	}
}

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

Los datos y los métodos deberían estar lo más ocultos posible y ser accesibles solo a través de una interfaz definida. Eso le permite cambiar la implementación interna de la clase sin afectar al resto del código.

La palabra clave final

En PHP podemos usar la palabra clave final si queremos impedir que una clase, un método o una constante se hereden o se sobrescriban. Cuando una clase está marcada como final, no se puede ampliar. Cuando un método está marcado como final, no se puede sobrescribir en una clase derivada.

Saber que una clase o un método concretos ya no se van a modificar nos permite hacer cambios más fácilmente sin preocuparnos por posibles conflictos. Por ejemplo, podemos añadir un método nuevo sin temer que algún descendiente ya tenga un método con el mismo nombre y se produzca una colisión. O podemos cambiar los parámetros de un método, de nuevo sin el riesgo de provocar una inconsistencia con un método sobrescrito en un descendiente.

final class FinalClass
{
}

// El código siguiente lanzará un error, porque no podemos heredar de una clase final.
class ChildOfFinalClass extends FinalClass
{
}

En este ejemplo, intentar heredar de la clase final FinalClass acabará en un error.

Propiedades y métodos estáticos

Cuando hablamos de elementos “estáticos” de una clase en PHP, nos referimos a los métodos y las propiedades que pertenecen a la propia clase, no a una instancia concreta de la clase. Eso significa que no tiene que crear una instancia de la clase para acceder a ellos. En su lugar, los llama o accede a ellos directamente a través del nombre de la clase.

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

Usar propiedades estáticas lleva a un código confuso y lleno de trampas, así que no debería usarlas nunca y aquí no mostraremos ningún ejemplo. Los métodos estáticos, en cambio, son útiles. Aquí tiene un ejemplo:

class Calculator
{
	public static function add($a, $b)
	{
		return $a + $b;
	}

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

// Uso del método estático sin crear una instancia de la clase
echo Calculator::add(5, 3); // Salida: 8
echo Calculator::subtract(5, 3); // Salida: 2

En este ejemplo hemos creado una clase Calculator con dos métodos estáticos. Podemos llamar a estos métodos directamente, sin crear una instancia de la clase, con el operador ::. Los métodos estáticos son especialmente útiles para las operaciones que no dependen del estado de una instancia concreta de la clase.

Constantes de clase

Dentro de las clases tenemos la posibilidad 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 sigue siendo el mismo.

class Car
{
	public const NumberOfWheels = 4;

	public function displayNumberOfWheels(): void
	{
		echo self::NumberOfWheels;
	}
}

echo Car::NumberOfWheels;  // Salida: 4

En este ejemplo tenemos una clase Car con la constante NumberOfWheels. Al 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, tiene que contener todos los métodos que la interfaz define. Es una forma estupenda de asegurarse de que ciertas clases cumplen el mismo “contrato” o la misma estructura.

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

interface Animal
{
	function makeSound();
}

class Cat implements Animal
{
	public function makeSound()
	{
		echo 'Miau';
	}
}

$cat = new Cat;
$cat->makeSound();

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

Una clase puede implementar varias interfaces a la vez, a diferencia de la herencia, donde una clase solo puede heredar de una clase:

interface Guardian
{
	function guardHouse();
}

class Dog implements Animal, Guardian
{
	public function makeSound()
	{
		echo 'Guau';
	}

	public function guardHouse()
	{
		echo 'El perro vigila la casa con diligencia';
	}
}

Clases abstractas

Las clases abstractas sirven de plantillas base para otras clases, pero no puede crear sus instancias directamente. Contienen una mezcla de métodos completos y métodos abstractos que no tienen contenido definido. Las clases que heredan de clases abstractas tienen que proporcionar las definiciones de todos los métodos abstractos del padre.

Para definir una clase abstracta usamos la palabra clave abstract.

abstract class AbstractClass
{
	public function regularMethod()
	{
		echo 'Esto es un método normal';
	}

	abstract public function abstractMethod();
}

class Child extends AbstractClass
{
	public function abstractMethod()
	{
		echo 'Esto es la implementación del método abstracto';
	}
}

$instance = new Child;
$instance->regularMethod();
$instance->abstractMethod();

En este ejemplo tenemos una clase abstracta con un método normal y uno abstracto. Después tenemos una clase Child que hereda de AbstractClass y proporciona una implementación del método abstracto.

¿En qué se diferencian las interfaces y las clases abstractas? Las clases abstractas pueden contener tanto métodos abstractos como concretos, mientras que las interfaces solo definen qué métodos tiene que implementar la clase, pero no proporcionan ninguna implementación. Una clase puede heredar solo de una clase abstracta, pero puede implementar cualquier número de interfaces.

Comprobación de tipos

En programación es clave asegurarse de que los datos con los que trabajamos son del tipo correcto. En PHP tenemos herramientas que proporcionan esa garantía. Verificar que los datos son del tipo correcto se llama “comprobación de tipos”.

Tipos con los que nos 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: cuando queremos que un valor sea una instancia de una clase concreta.
  3. Interfaces: definen un conjunto de métodos que una clase tiene que implementar. Un valor que cumple una interfaz debe tener esos métodos.
  4. Tipos mixtos: podemos indicar que una variable puede tener varios tipos permitidos.
  5. Void: este tipo especial indica que una función o un método no devuelve ningún valor.

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

class Person
{
	private int $age;

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

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

/**
 * Una función que acepta un objeto Person e imprime la edad de la persona.
 */
function printPersonAge(Person $person): void
{
	$person->printAge();
}

De esta forma nos aseguramos de 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 ese caso se indican en el comentario phpDoc, que es el formato estándar para documentar el código PHP y que empieza por /** y termina por */. Permite añadir descripciones de las clases, los métodos, etc. Y también indicar tipos complejos mediante las llamadas anotaciones @var, @param y @return. Esos tipos los usan después las herramientas de análisis estático del código, pero PHP mismo no los comprueba.

class Registry
{
	/** @var array<Person>  indica que es un array de objetos Person */
	private array $persons = [];

	public function addPerson(Person $person): void
	{
		$this->persons[] = $person;
	}
}

Comparación e identidad

En PHP puede comparar objetos de dos maneras:

  1. Comparación por valor ==: comprueba si los objetos son de la misma clase y tienen los mismos valores en sus propiedades.
  2. Identidad ===: comprueba si se trata de la misma instancia del objeto.
class Car
{
	public string $brand;

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

$car1 = new Car('Skoda');
$car2 = new Car('Skoda');
$car3 = $car1;

var_dump($car1 == $car2);   // true, porque tienen el mismo valor
var_dump($car1 === $car2);  // false, porque no son la misma instancia
var_dump($car1 === $car3);  // true, porque $car3 es la misma instancia que $car1

El operador instanceof

El operador instanceof le permite averiguar si un objeto dado es una instancia de una clase concreta, un descendiente de esa clase o si implementa una interfaz determinada.

Imagine que tenemos una clase Person y otra clase Student, que es descendiente de Person:

class Person
{
	private int $age;

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

class Student extends Person
{
	private string $major;

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

$student = new Student(20, 'Informática');

// Comprueba si $student es una instancia de la clase Student
var_dump($student instanceof Student);  // Salida: bool(true)

// Comprueba si $student es una instancia de la clase Person (porque Student es descendiente de Person)
var_dump($student instanceof Person);   // Salida: bool(true)

De las salidas se ve que el objeto $student se considera una instancia tanto de la clase Student como de la clase Person.

Interfaces fluidas

Una “interfaz fluida” (fluent interface) es una técnica de la POO que permite encadenar métodos en una sola llamada. Eso a menudo simplifica y aclara el código.

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

Veamos qué aspecto puede tener una interfaz fluida para enviar correos:

public function sendMessage()
{
	$email = new Email;
	$email->setFrom('sender@example.com')
		  ->setRecipient('admin@example.com')
		  ->setMessage('Hola, esto es un mensaje.')
		  ->send();
}

En este ejemplo, los métodos setFrom(), setRecipient() y setMessage() se usan para establecer los valores correspondientes (remitente, destinatario, contenido del mensaje). Después de establecer cada uno de esos valores, los métodos devuelven el objeto actual ($email), lo que nos permite encadenar otro método detrás. Por último llamamos al método send(), que envía realmente el correo.

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

Copiar con clone

En PHP podemos crear una copia de un objeto con el operador clone. Así obtenemos una instancia nueva con un contenido idéntico.

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

class Sheep
{
	public string $name;

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

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

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

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

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

Traits

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

Echemos un vistazo a un ejemplo sencillo de cómo usar los traits en PHP:

trait Honking
{
	public function honk()
	{
		echo '¡Bip bip!';
	}
}

class Car
{
	use Honking;
}

class Truck
{
	use Honking;
}

$car = new Car;
$car->honk(); // Imprime '¡Bip bip!'

$truck = new Truck;
$truck->honk(); // También imprime '¡Bip bip!'

En este ejemplo tenemos un trait llamado Honking que contiene un método honk(). Después tenemos dos clases, Car y Truck, que usan ambas el trait Honking. Como resultado, las dos clases “tienen” el método honk() y podemos llamarlo sobre los objetos de ambas clases.

Los traits permiten compartir código entre clases de forma fácil y eficiente. No entran en la jerarquía de herencia, es decir, $car instanceof Honking devolverá false.

Excepciones

Las excepciones en la POO nos permiten manejar con elegancia los errores y las situaciones inesperadas en nuestro código. Son objetos que llevan información sobre un error o una situación inusual.

En PHP tenemos una clase integrada Exception, que sirve de base para todas las excepciones. Esta 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 se produjo el error, etc.

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

function division(float $a, float $b): float
{
	if ($b == 0) {
		throw new Exception('¡División por cero!');
	}
	return $a / $b;
}

Cuando la función division() recibe cero como segundo argumento, lanza una excepción con el mensaje de error '¡División por cero!'. Para que el programa no se caiga al lanzarse la excepción, la atrapamos en el bloque try/catch:

try {
	echo division(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 la excepción, la ejecución del código pasa al bloque catch, donde podemos manejarla (p. ej. imprimir un mensaje de error).

Detrás de los bloques try y catch podemos añadir un bloque opcional finally, que se ejecuta siempre, se haya lanzado la excepción o no (incluso si usamos return, break o continue en el bloque try o catch):

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Excepción capturada: '. $e->getMessage();
} finally {
	// Código que se ejecuta siempre, se haya lanzado la excepción o no
}

También podemos crear nuestras propias clases (jerarquías) de excepciones que heredan de la clase Exception. Como ejemplo, imagine una aplicación bancaria sencilla que permite ingresos y retiradas:

class BankingException extends Exception {}
class InsufficientFundsException extends BankingException {}
class ExceededLimitException extends BankingException {}

class BankAccount
{
	private int $balance = 0;
	private int $dailyLimit = 1000;

	public function deposit(int $amount): int
	{
		$this->balance += $amount;
		return $this->balance;
	}

	public function withdraw(int $amount): int
	{
		if ($amount > $this->balance) {
			throw new InsufficientFundsException('No hay fondos suficientes en la cuenta.');
		}

		if ($amount > $this->dailyLimit) {
			throw new ExceededLimitException('Límite diario de retirada superado.');
		}

		$this->balance -= $amount;
		return $this->balance;
	}
}

Para un mismo bloque try se pueden indicar varios bloques catch si espera distintos tipos de excepciones.

$account = new BankAccount;
$account->deposit(500);

try {
	$account->withdraw(1500);
} catch (ExceededLimitException $e) {
	echo $e->getMessage();
} catch (InsufficientFundsException $e) {
	echo $e->getMessage();
} catch (BankingException $e) {
	echo 'Se produjo un error durante la operación.';
}

En este ejemplo es importante fijarse en el orden de los bloques catch. Como todas las excepciones heredan de BankingException, si tuviéramos ese bloque el primero, todas las excepciones se capturarían en él y el código no llegaría a los bloques catch siguientes. Por eso es importante tener las excepciones más específicas (es decir, las que heredan de otras) más arriba en el orden de los bloques catch que sus excepciones padre.

Iteraciones

En PHP puede recorrer los objetos con el bucle foreach, igual que recorre un array. Para que eso funcione, el objeto tiene que 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 pasa al valor siguiente, rewind(), que vuelve al principio, y valid(), que comprueba si ya estamos al final.

La otra opción es implementar la interfaz IteratorAggregate, que tiene un único método getIterator(). Este devuelve o bien un objeto auxiliar que se encargará del recorrido, o bien puede ser un generador, que es una función especial que usa yield para devolver las claves y los valores uno tras otro:

class Person
{
	public function __construct(
		public int $age,
	) {
	}
}

class Registry implements IteratorAggregate
{
	private array $people = [];

	public function addPerson(Person $person): void
	{
		$this->people[] = $person;
	}

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

$list = new Registry;
$list->addPerson(new Person(30));
$list->addPerson(new Person(25));

foreach ($list as $person) {
	echo "Edad: {$person->age} años\n";
}

Buenas prácticas

Cuando ya domine los principios básicos de la programación orientada a objetos, es clave centrarse en las buenas prácticas de la POO. Le ayudarán a escribir un código que no solo funcione, sino que además sea legible, comprensible y fácil de mantener.

  1. Separación de responsabilidades: cada clase debería tener una responsabilidad claramente definida y resolver solo una tarea principal. Si una clase hace demasiadas cosas, puede ser adecuado dividirla en clases más pequeñas y especializadas.
  2. Encapsulación: los datos y los métodos deberían estar lo más ocultos posible y ser accesibles solo a través de una interfaz definida. Eso le permite cambiar la implementación interna de una clase sin afectar al resto del código.
  3. Dependency Injection: en lugar de crear las dependencias directamente dentro de una clase, debería “inyectarlas” desde fuera. Para entender más a fondo este principio, le recomendamos los capítulos sobre Dependency Injection.
versión: 4.x