Módulo 12: POO avanzada
Objetivos del módulo
- Reutilizar código con herencia:
extends,parent::, sobrescritura yfinal - Definir clases y métodos abstractos
- Diseñar contratos con interfaces, incluidas las propiedades en interfaces (PHP 8.4)
- Aplicar el polimorfismo para escribir código flexible
- Compartir código horizontalmente con traits y resolver sus conflictos
- Modelar conjuntos cerrados de valores con enums puros y respaldados
- Entender
staticfrente aselfy el late static binding - Usar los métodos mágicos y las interfaces nativas (
Stringable,Countable,IteratorAggregate,ArrayAccess,JsonSerializable) - Crear clases anónimas y trabajar con atributos (
#[\Override],#[\Deprecated]y atributos propios) - Conocer los principios SOLID y preferir la composición frente a la herencia
1. Herencia
📘 Concepto: La herencia permite crear una clase (hija o subclase) a partir de otra (padre o superclase). La hija hereda todas las propiedades y métodos públicos y protegidos del padre, puede añadir los suyos y puede sobrescribir los heredados. Expresa una relación “es un”: un
Gatoes unAnimal.
classDiagram
Empleado <|-- Desarrollador
Empleado <|-- Gerente
class Empleado {
#string nombre
#float salarioBase
+salario() float
+presentarse() string
}
class Desarrollador {
-array lenguajes
+presentarse() string
}
class Gerente {
-float bonus
+salario() float
}
<?php
declare(strict_types=1);
class Empleado
{
public function __construct(
protected string $nombre,
protected float $salarioBase,
) {}
public function salario(): float
{
return $this->salarioBase;
}
public function presentarse(): string
{
return "Soy {$this->nombre} y cobro {$this->salario()} €";
}
}
class Desarrollador extends Empleado
{
public function __construct(
string $nombre,
float $salarioBase,
private array $lenguajes = [],
) {
parent::__construct($nombre, $salarioBase); // Ejecuta el constructor del padre
}
// Sobrescritura: reutiliza el método del padre y lo amplía
public function presentarse(): string
{
return parent::presentarse() . '. Programo en ' . implode(', ', $this->lenguajes);
}
}
class Gerente extends Empleado
{
public function __construct(string $nombre, float $salarioBase, private float $bonus)
{
parent::__construct($nombre, $salarioBase);
}
public function salario(): float
{
return parent::salario() + $this->bonus;
}
}
$equipo = [
new Empleado('Marta', 1800),
new Desarrollador('Luis', 2400, ['PHP', 'SQL']),
new Gerente('Ana', 3000, 750),
];
foreach ($equipo as $persona) {
echo $persona->presentarse() . PHP_EOL;
}
// Soy Marta y cobro 1800 €
// Soy Luis y cobro 2400 €. Programo en PHP, SQL
// Soy Ana y cobro 3750 €
| Elemento | Significado |
|---|---|
class B extends A | B hereda de A (PHP solo permite un padre) |
parent::metodo() | Llama a la versión del padre de un método sobrescrito |
parent::__construct(...) | Ejecuta el constructor del padre. No es automático si la hija define su propio constructor |
protected | Visible en la clase y en sus hijas, no desde fuera |
$obj instanceof Empleado | true también para Desarrollador y Gerente |
⚠️ Importante: Si la clase hija define su propio constructor, el del padre no se ejecuta solo: tienes que llamar a
parent::__construct(). Si lo olvidas, las propiedades del padre quedarán sin inicializar.
Reglas de la sobrescritura
Al sobrescribir un método, la firma debe ser compatible con la del padre (principio de sustitución de Liskov, sección 13):
- La visibilidad no puede ser más restrictiva (
publicno puede pasar aprotected). - Los parámetros pueden aceptar más tipos, nunca menos (contravarianza).
- El tipo de retorno puede ser más concreto, nunca más general (covarianza).
- Puedes añadir parámetros nuevos solo si son opcionales.
final: impedir la herencia o la sobrescritura
<?php
declare(strict_types=1);
class Pago
{
// Ninguna hija puede cambiar cómo se calcula la comisión
final public function comision(float $importe): float
{
return round($importe * 0.015, 2);
}
}
final class PagoBizum extends Pago {} // Nadie puede heredar de PagoBizum
// class PagoBizumPremium extends PagoBizum {} // Error: cannot extend final class
💡 Consejo: Muchos equipos marcan todas las clases como
finalpor defecto y solo lo quitan cuando hay una razón para heredar. Así evitan jerarquías de herencia accidentales y frágiles.
2. Clases y métodos abstractos
📘 Concepto: Una clase abstracta es una clase incompleta: sirve de base para otras, pero no se puede instanciar. Puede contener métodos abstractos (solo la firma, sin cuerpo) que las hijas están obligadas a implementar, junto con métodos normales ya implementados.
Analogía: “Vehículo” es un concepto abstracto; no puedes comprar “un vehículo” a secas, sino un coche, una moto o una bicicleta. Pero todos comparten cosas (tienen ruedas, se mueven).
<?php
declare(strict_types=1);
abstract class Figura
{
public function __construct(protected string $color = 'negro') {}
// Las hijas DEBEN implementarlos
abstract public function area(): float;
abstract public function nombre(): string;
// Método concreto que usa los abstractos (patrón "método plantilla")
public function describir(): string
{
return sprintf('%s %s con área %.2f', $this->nombre(), $this->color, $this->area());
}
}
final class Circulo extends Figura
{
public function __construct(private float $radio, string $color = 'negro')
{
parent::__construct($color);
}
public function area(): float
{
return M_PI * $this->radio ** 2;
}
public function nombre(): string
{
return 'Círculo';
}
}
final class Rectangulo extends Figura
{
public function __construct(private float $ancho, private float $alto, string $color = 'negro')
{
parent::__construct($color);
}
public function area(): float
{
return $this->ancho * $this->alto;
}
public function nombre(): string
{
return 'Rectángulo';
}
}
$figuras = [new Circulo(2, 'rojo'), new Rectangulo(3, 4, 'azul')];
foreach ($figuras as $f) {
echo $f->describir() . PHP_EOL;
}
// Círculo rojo con área 12.57
// Rectángulo azul con área 12.00
// new Figura(); // Error: Cannot instantiate abstract class Figura
3. Interfaces
📘 Concepto: Una interfaz es un contrato: una lista de métodos públicos que una clase se compromete a implementar, sin decir cómo. Expresa una relación “puede hacer” o “se comporta como”: una clase
Facturapuede exportarse, unUsuariopuede notificarse.
Analogía: el enchufe de la pared es una interfaz. No le importa si conectas una lámpara, un portátil o una tostadora: cualquier aparato que respete la forma del enchufe funciona.
<?php
declare(strict_types=1);
interface Notificador
{
public function enviar(string $destinatario, string $mensaje): bool;
}
final class NotificadorEmail implements Notificador
{
public function enviar(string $destinatario, string $mensaje): bool
{
echo "📧 Email a $destinatario: $mensaje" . PHP_EOL;
return true;
}
}
final class NotificadorSms implements Notificador
{
public function enviar(string $destinatario, string $mensaje): bool
{
echo '📱 SMS a ' . $destinatario . ': ' . mb_substr($mensaje, 0, 160) . PHP_EOL;
return true;
}
}
// Esta función acepta CUALQUIER notificador: no le importa cuál
function avisarPedidoEnviado(Notificador $notificador, string $cliente): void
{
$notificador->enviar($cliente, 'Tu pedido ha salido del almacén');
}
avisarPedidoEnviado(new NotificadorEmail(), 'ana@correo.es');
avisarPedidoEnviado(new NotificadorSms(), '+34600111222');
Implementar varias interfaces
Una clase solo puede heredar de una clase, pero puede implementar todas las interfaces que quiera:
<?php
declare(strict_types=1);
interface Exportable
{
public function exportar(): array;
}
interface Imprimible
{
public const string FORMATO_PAPEL = 'A4'; // Constante de interfaz (tipada, PHP 8.3)
public function imprimir(): string;
}
final class Factura implements Exportable, Imprimible
{
public function __construct(private string $numero, private float $total) {}
public function exportar(): array
{
return ['numero' => $this->numero, 'total' => $this->total];
}
public function imprimir(): string
{
return "FACTURA {$this->numero} ({$this->total} €) - papel " . self::FORMATO_PAPEL;
}
}
$f = new Factura('2026-001', 99.9);
echo $f->imprimir() . PHP_EOL; // FACTURA 2026-001 (99.9 €) - papel A4
echo Imprimible::FORMATO_PAPEL . PHP_EOL; // A4
var_dump($f instanceof Exportable); // bool(true)
Las interfaces también pueden heredar de otras: interface Documento extends Exportable, Imprimible {}.
Propiedades en interfaces (PHP 8.4)
Gracias a los property hooks, desde PHP 8.4 una interfaz puede exigir propiedades, indicando si deben poder leerse (get), escribirse (set) o ambas:
<?php
declare(strict_types=1);
interface TieneNombre
{
public string $nombre { get; } // Debe poder leerse públicamente
}
interface Identificable
{
public int $id { get; }
public string $slug { get; set; } // Debe poder leerse y escribirse
}
final class Categoria implements TieneNombre, Identificable
{
public string $slug;
public function __construct(
public readonly int $id, // Cumple { get } con una propiedad readonly
public string $nombre, // Una propiedad pública normal también lo cumple
) {
$this->slug = mb_strtolower(str_replace(' ', '-', $nombre));
}
}
final class Autor implements TieneNombre
{
public function __construct(private string $nombrePila, private string $apellido) {}
// Cumple { get } con una propiedad virtual
public string $nombre {
get => "{$this->nombrePila} {$this->apellido}";
}
}
function etiqueta(TieneNombre $algo): string
{
return "[{$algo->nombre}]";
}
echo etiqueta(new Categoria(1, 'Libros técnicos')) . PHP_EOL; // [Libros técnicos]
echo etiqueta(new Autor('Ada', 'Lovelace')) . PHP_EOL; // [Ada Lovelace]
💡 Consejo: Antes de PHP 8.4, una interfaz solo podía exigir un método
getNombre(). Ahora puede exigir la propiedad, y cada clase decide si la implementa como propiedad normal,readonlyo virtual.
Clase abstracta frente a interfaz
| Característica | Clase abstracta | Interfaz |
|---|---|---|
| Relación | “es un” | “puede hacer” / “se comporta como” |
| ¿Cuántas por clase? | Una (extends) | Varias (implements A, B, C) |
| Métodos con código | Sí | No |
| Propiedades con estado | Sí | Solo puede exigir propiedades (8.4), no guardarlas |
| Constructor | Sí | No (puede declararlo, pero no es habitual) |
| Visibilidad de métodos | Cualquiera | Solo public |
| Uso típico | Compartir código base entre clases muy relacionadas | Definir contratos entre partes del sistema |
4. Polimorfismo
📘 Concepto: Polimorfismo (“muchas formas”) significa que objetos de clases distintas pueden usarse a través de la misma interfaz, y cada uno responde a su manera. El código que los usa no necesita saber de qué clase concreta es cada uno.
Sin polimorfismo, el código se llena de condicionales que hay que tocar cada vez que aparece un caso nuevo:
<?php
// ❌ Sin polimorfismo: cada método de pago nuevo obliga a modificar esta función
function cobrar(string $metodo, float $importe): string
{
if ($metodo === 'tarjeta') {
return "Cobrados $importe € con tarjeta (comisión 1,5 %)";
} elseif ($metodo === 'bizum') {
return "Cobrados $importe € por Bizum";
} elseif ($metodo === 'paypal') {
// ...
}
throw new InvalidArgumentException('Método desconocido');
}
<?php
declare(strict_types=1);
// ✅ Con polimorfismo: cada método de pago sabe cobrar
interface MetodoPago
{
public function cobrar(float $importe): string;
public function comision(float $importe): float;
}
final class Tarjeta implements MetodoPago
{
public function __construct(private string $ultimos4) {}
public function cobrar(float $importe): string
{
return "Cobrados $importe € con la tarjeta ****{$this->ultimos4}";
}
public function comision(float $importe): float
{
return round($importe * 0.015, 2);
}
}
final class Bizum implements MetodoPago
{
public function __construct(private string $telefono) {}
public function cobrar(float $importe): string
{
return "Cobrados $importe € por Bizum al {$this->telefono}";
}
public function comision(float $importe): float
{
return 0.0;
}
}
final class Caja
{
// No sabe (ni le importa) qué método concreto recibe
public function procesar(MetodoPago $metodo, float $importe): void
{
echo $metodo->cobrar($importe) . ' | comisión: ' . $metodo->comision($importe) . ' €' . PHP_EOL;
}
}
$caja = new Caja();
$caja->procesar(new Tarjeta('4242'), 80);
$caja->procesar(new Bizum('600111222'), 25);
Para añadir PayPal basta con crear una clase nueva que implemente MetodoPago. No se modifica ni Caja ni las demás clases. Este es el principio abierto/cerrado (sección 13).
5. Traits
📘 Concepto: Un trait es un bloque de métodos (y propiedades) que se copia dentro de una clase con
use. Resuelve un problema de la herencia simple: compartir código entre clases que no están relacionadas por herencia. Es reutilización “horizontal”.
Analogía: un trait es como un accesorio que puedes acoplar a distintos aparatos: el mismo cargador USB sirve para un móvil, un altavoz o un ratón, aunque no sean “parientes”.
<?php
declare(strict_types=1);
trait TieneMarcasDeTiempo
{
public private(set) ?DateTimeImmutable $creadoEn = null;
public private(set) ?DateTimeImmutable $modificadoEn = null;
public function marcarCreado(): void
{
$this->creadoEn = new DateTimeImmutable();
}
public function marcarModificado(): void
{
$this->modificadoEn = new DateTimeImmutable();
}
}
trait Sluggable
{
public function generarSlug(string $texto): string
{
return $texto
|> mb_strtolower(...)
|> (fn(string $s) => preg_replace('/[^a-z0-9]+/u', '-', $s))
|> (fn(string $s) => trim($s, '-'));
}
}
final class Articulo
{
use TieneMarcasDeTiempo, Sluggable;
public string $slug;
public function __construct(public string $titulo)
{
$this->slug = $this->generarSlug($titulo);
$this->marcarCreado();
}
}
final class Producto
{
use TieneMarcasDeTiempo; // Producto y Articulo no tienen nada más en común
}
$a = new Articulo('Novedades de PHP 8.5');
echo $a->slug . PHP_EOL; // novedades-de-php-8-5
echo $a->creadoEn->format('Y-m-d H:i') . PHP_EOL;
Métodos abstractos y estáticos en traits
Un trait puede exigir que la clase que lo usa tenga ciertos métodos, y también incluir miembros estáticos y constantes (PHP 8.2):
<?php
declare(strict_types=1);
trait Contable
{
public const int LIMITE = 1000; // Constantes en traits (PHP 8.2)
private static int $instancias = 0; // Cada clase que usa el trait tiene SU contador
abstract public function nombreEntidad(): string; // La clase debe implementarlo
public static function instancias(): int
{
return self::$instancias;
}
protected function registrarInstancia(): void
{
self::$instancias++;
echo 'Nueva instancia de ' . $this->nombreEntidad() . PHP_EOL;
}
}
final class Cliente
{
use Contable;
public function __construct() { $this->registrarInstancia(); }
public function nombreEntidad(): string { return 'Cliente'; }
}
new Cliente();
new Cliente();
echo Cliente::instancias() . PHP_EOL; // 2
Conflictos: insteadof y as
Si dos traits definen un método con el mismo nombre, hay que decidir cuál se usa:
<?php
declare(strict_types=1);
trait Saludo
{
public function hola(): string { return 'Hola'; }
}
trait Greeting
{
public function hola(): string { return 'Hello'; }
}
final class Recepcion
{
use Saludo, Greeting {
Saludo::hola insteadof Greeting; // Usa el de Saludo
Greeting::hola as holaIngles; // Y el otro, con otro nombre
Saludo::hola as protected holaInterno; // Alias con otra visibilidad
}
}
$r = new Recepcion();
echo $r->hola() . PHP_EOL; // Hola
echo $r->holaIngles() . PHP_EOL; // Hello
| Sintaxis | Significado |
|---|---|
A::m insteadof B; | En caso de conflicto, usa m de A en lugar de la de B |
B::m as otroNombre; | Crea un alias para el método |
m as protected; | Cambia la visibilidad |
m as private aliasPrivado; | Alias con otra visibilidad |
⚠️ Importante: Los traits son útiles, pero no los uses para esconder dependencias o para compartir lógica de negocio compleja: el código queda repartido en ficheros y es difícil ver qué hace una clase. Para lo que no sea “infraestructura” sencilla (marcas de tiempo, utilidades), suele ser mejor la composición (sección 14).
6. Enums
📘 Concepto: Un enum (enumeración, PHP 8.1) define un tipo con un conjunto cerrado de valores posibles: los estados de un pedido, los palos de la baraja, los roles de usuario. Cada valor es un caso (
case). PHP garantiza que una variable de ese tipo solo puede contener uno de esos casos.
Antes de los enums se usaban constantes o cadenas ('pendiente', 'enviado'), con el riesgo de errores tipográficos ('enviadoo') que nadie detectaba.
Enums puros
<?php
declare(strict_types=1);
enum Palo
{
case Oros;
case Copas;
case Espadas;
case Bastos;
}
function describirPalo(Palo $palo): string // Solo admite un caso de Palo
{
return match ($palo) {
Palo::Oros => '🪙 Oros',
Palo::Copas => '🍷 Copas',
Palo::Espadas => '⚔️ Espadas',
Palo::Bastos => '🪵 Bastos',
};
}
$palo = Palo::Copas;
echo describirPalo($palo) . PHP_EOL; // 🍷 Copas
echo $palo->name . PHP_EOL; // Copas
var_dump($palo === Palo::Copas); // bool(true)
var_dump($palo instanceof Palo); // bool(true)
// describirPalo('Copas'); // TypeError
💡 Consejo:
matchcon enums es exhaustivo en la práctica: si añades un caso nuevo y olvidas tratarlo,matchlanzará unUnhandledMatchErroren lugar de fallar en silencio. Y herramientas como PHPStan te avisan antes de ejecutar.
Enums respaldados (backed enums)
Cuando necesitas guardar el valor en una base de datos, un JSON o un formulario, cada caso puede tener un valor escalar asociado (string o int):
<?php
declare(strict_types=1);
enum EstadoPedido: string
{
case Pendiente = 'pendiente';
case Pagado = 'pagado';
case Enviado = 'enviado';
case Entregado = 'entregado';
case Cancelado = 'cancelado';
// Constantes en enums: pueden apuntar a un caso
public const INICIAL = self::Pendiente;
// Métodos
public function etiqueta(): string
{
return match ($this) {
self::Pendiente => 'Pendiente de pago',
self::Pagado => 'Pagado',
self::Enviado => 'En camino',
self::Entregado => 'Entregado',
self::Cancelado => 'Cancelado',
};
}
public function color(): string
{
return match ($this) {
self::Pendiente => 'warning',
self::Pagado, self::Enviado => 'info',
self::Entregado => 'success',
self::Cancelado => 'danger',
};
}
/** Transiciones permitidas desde este estado */
public function puedePasarA(self $nuevo): bool
{
return in_array($nuevo, match ($this) {
self::Pendiente => [self::Pagado, self::Cancelado],
self::Pagado => [self::Enviado, self::Cancelado],
self::Enviado => [self::Entregado],
self::Entregado, self::Cancelado => [],
}, true);
}
// Métodos estáticos
/** @return list<self> */
public static function activos(): array
{
return array_values(array_filter(self::cases(), fn(self $e) => $e !== self::Cancelado));
}
}
$estado = EstadoPedido::from('pagado'); // Desde un valor (por ejemplo, de la base de datos)
echo $estado->name . PHP_EOL; // Pagado
echo $estado->value . PHP_EOL; // pagado
echo $estado->etiqueta() . PHP_EOL; // Pagado
var_dump($estado->puedePasarA(EstadoPedido::Entregado)); // bool(false)
var_dump(EstadoPedido::tryFrom('perdido')); // NULL (no lanza excepción)
// EstadoPedido::from('perdido'); // ValueError: "perdido" is not a valid backing value
echo EstadoPedido::INICIAL->etiqueta() . PHP_EOL; // Pendiente de pago
echo count(EstadoPedido::cases()) . PHP_EOL; // 5
| Elemento | Devuelve |
|---|---|
Enum::Caso | El caso (un objeto único) |
$caso->name | Nombre del caso: 'Pagado' |
$caso->value | Valor respaldado: 'pagado' (solo en backed enums) |
Enum::cases() | Array con todos los casos, en orden de declaración |
Enum::from($valor) | El caso correspondiente o ValueError |
Enum::tryFrom($valor) | El caso correspondiente o null |
Enums que implementan interfaces
<?php
declare(strict_types=1);
interface TieneEtiqueta
{
public function etiqueta(): string;
}
enum Rol: string implements TieneEtiqueta
{
case Admin = 'admin';
case Editor = 'editor';
case Lector = 'lector';
public function etiqueta(): string
{
return ucfirst($this->value);
}
public function puede(string $accion): bool
{
return match ($this) {
self::Admin => true,
self::Editor => in_array($accion, ['leer', 'crear', 'editar'], true),
self::Lector => $accion === 'leer',
};
}
}
// Generar un <select> a partir del enum
foreach (Rol::cases() as $rol) {
echo "<option value=\"{$rol->value}\">{$rol->etiqueta()}</option>" . PHP_EOL;
}
// Validar lo que llega de un formulario
$rol = Rol::tryFrom($_POST['rol'] ?? '') ?? Rol::Lector;
var_dump($rol->puede('editar'));
⚠️ Importante: Los enums no pueden tener propiedades (estado), ni se pueden instanciar con
new. Sí pueden tener métodos, métodos estáticos, constantes, implementar interfaces y usar traits (siempre que el trait no tenga propiedades).
💡 Consejo: Con
tryFrom()validar la entrada de un formulario es trivial y seguro: si el valor no es uno de los casos, obtienesnull. Sustituye a las listas blancas de arrays del módulo 08 cuando el conjunto de valores forma parte de tu dominio.
7. static frente a self: late static binding
self:: se refiere a la clase donde está escrito el código. static:: se refiere a la clase sobre la que se ha llamado realmente el método, que puede ser una hija. Esta resolución “tardía” se llama late static binding.
<?php
declare(strict_types=1);
class Modelo
{
protected static string $tabla = 'modelos';
public static function crearConSelf(): self
{
return new self(); // Siempre crea un Modelo
}
public static function crear(): static
{
return new static(); // Crea un objeto de la clase sobre la que se llamó
}
public static function tablaConSelf(): string
{
return self::$tabla; // Siempre la de Modelo
}
public static function tabla(): string
{
return static::$tabla; // La de la clase hija si la redefine
}
}
final class Usuario extends Modelo
{
protected static string $tabla = 'usuarios';
}
echo Usuario::crearConSelf()::class . PHP_EOL; // Modelo
echo Usuario::crear()::class . PHP_EOL; // Usuario
echo Usuario::tablaConSelf() . PHP_EOL; // modelos
echo Usuario::tabla() . PHP_EOL; // usuarios
| Expresión | Se resuelve a |
|---|---|
self:: | La clase donde está escrito el código |
static:: | La clase sobre la que se hizo la llamada (en tiempo de ejecución) |
parent:: | La clase padre de donde está escrito el código |
Retorno : self | Un objeto de la clase donde se declara |
Retorno : static | Un objeto de la clase sobre la que se llama (ideal para fábricas y métodos with* heredables) |
8. Métodos mágicos
Los métodos mágicos empiezan por __ y PHP los llama automáticamente en situaciones concretas. Ya conoces __construct, __destruct y __clone.
| Método | Se ejecuta cuando… |
|---|---|
__toString(): string | El objeto se usa como cadena (echo $obj, concatenación) |
__get(string $n): mixed | Se lee una propiedad inaccesible o inexistente |
__set(string $n, mixed $v): void | Se escribe una propiedad inaccesible o inexistente |
__isset(string $n): bool | isset() o empty() sobre una propiedad inaccesible |
__unset(string $n): void | unset() sobre una propiedad inaccesible |
__call(string $m, array $args): mixed | Se llama a un método inaccesible o inexistente |
__callStatic(string $m, array $args): mixed | Igual, en contexto estático |
__invoke(...): mixed | El objeto se llama como una función: $obj() |
__serialize(): array / __unserialize(array $d): void | serialize() / unserialize() |
__toString y Stringable
<?php
declare(strict_types=1);
final class Dinero
{
public function __construct(private int $centimos, private string $moneda = 'EUR') {}
public function __toString(): string
{
return number_format($this->centimos / 100, 2, ',', '.') . ' ' . $this->moneda;
}
}
$precio = new Dinero(1999);
echo "Precio: $precio" . PHP_EOL; // Precio: 19,99 EUR
var_dump($precio instanceof Stringable); // bool(true): se implementa automáticamente
function mostrar(string|Stringable $texto): void
{
echo htmlspecialchars((string) $texto) . PHP_EOL;
}
mostrar($precio);
📘 Concepto: Toda clase con
__toString()implementa automáticamente la interfaz nativaStringable(PHP 8.0). Úsala como tipo cuando una función acepte “cualquier cosa que se pueda convertir en texto”.
__get, __set, __isset y __unset
<?php
declare(strict_types=1);
/** Configuración con acceso como propiedades, respaldada por un array */
final class Configuracion
{
private array $datos = [];
public function __get(string $nombre): mixed
{
if (!array_key_exists($nombre, $this->datos)) {
throw new OutOfBoundsException("La opción '$nombre' no existe");
}
return $this->datos[$nombre];
}
public function __set(string $nombre, mixed $valor): void
{
$this->datos[$nombre] = $valor;
}
public function __isset(string $nombre): bool
{
return isset($this->datos[$nombre]);
}
public function __unset(string $nombre): void
{
unset($this->datos[$nombre]);
}
}
$config = new Configuracion();
$config->idioma = 'es'; // __set
echo $config->idioma . PHP_EOL; // __get → es
var_dump(isset($config->tema)); // __isset → false
unset($config->idioma); // __unset
⚠️ Importante:
__gety__sethacen el código “mágico” y opaco: el IDE no puede autocompletar y los errores aparecen en tiempo de ejecución. Desde PHP 8.4, para validar o calcular propiedades usa property hooks (módulo 11). Reserva__get/__setpara casos realmente dinámicos.
__call y __callStatic
<?php
declare(strict_types=1);
final class Consulta
{
private array $filtros = [];
// $consulta->dondeCiudad('Sevilla') → filtro ciudad = Sevilla
public function __call(string $metodo, array $args): static
{
if (str_starts_with($metodo, 'donde')) {
$campo = lcfirst(substr($metodo, 5));
$this->filtros[$campo] = $args[0] ?? null;
return $this;
}
throw new BadMethodCallException("Método $metodo no existe");
}
// Consulta::tabla('clientes') → new Consulta con la tabla
public static function __callStatic(string $metodo, array $args): static
{
if ($metodo === 'tabla') {
$c = new static();
$c->filtros['_tabla'] = $args[0];
return $c;
}
throw new BadMethodCallException("Método estático $metodo no existe");
}
public function sql(): string
{
$tabla = $this->filtros['_tabla'] ?? '?';
$condiciones = array_filter($this->filtros, fn($k) => $k !== '_tabla', ARRAY_FILTER_USE_KEY);
$where = implode(' AND ', array_map(fn($k) => "$k = ?", array_keys($condiciones)));
return "SELECT * FROM $tabla" . ($where ? " WHERE $where" : '');
}
}
echo Consulta::tabla('clientes')->dondeCiudad('Sevilla')->dondeActivo(true)->sql() . PHP_EOL;
// SELECT * FROM clientes WHERE ciudad = ? AND activo = ?
__invoke: objetos que se usan como funciones
<?php
declare(strict_types=1);
final class AplicarDescuento
{
public function __construct(private float $porcentaje) {}
public function __invoke(float $precio): float
{
return round($precio * (1 - $this->porcentaje / 100), 2);
}
}
$rebajas = new AplicarDescuento(20);
echo $rebajas(50) . PHP_EOL; // 40
print_r(array_map($rebajas, [10, 25, 99.99])); // [8, 20, 79.99]
var_dump(is_callable($rebajas)); // bool(true)
echo 100 |> $rebajas |> (fn($p) => "$p €"), PHP_EOL; // 80 € (¡también con pipe!)
__serialize y __unserialize
Controlan qué se guarda al serializar un objeto (por ejemplo, al guardarlo en la sesión) y cómo se reconstruye:
<?php
declare(strict_types=1);
final class ConexionApi
{
private ?CurlHandle $curl = null; // Un recurso no se puede serializar
public function __construct(private string $url, private string $token) {}
public function __serialize(): array
{
return ['url' => $this->url]; // No guardamos el token ni la conexión
}
public function __unserialize(array $datos): void
{
$this->url = $datos['url'];
$this->token = getenv('API_TOKEN') ?: '';
}
}
$texto = serialize(new ConexionApi('https://api.example', 'secreto'));
echo $texto . PHP_EOL; // O:11:"ConexionApi":1:{s:3:"url";s:19:"https://api.example";}
$copia = unserialize($texto);
⚠️ Importante:
__sleep()y__wakeup()son la forma antigua de hacer esto; desde PHP 8.5 están desaconsejados (soft-deprecated) en favor de__serialize()/__unserialize(). Y nunca llames aunserialize()con datos que vengan del usuario: puede permitir ejecutar código. Para intercambiar datos usa JSON.
9. Interfaces nativas útiles
PHP incluye interfaces que, al implementarlas, hacen que tus objetos funcionen con construcciones del lenguaje.
| Interfaz | Permite | Métodos a implementar |
|---|---|---|
Stringable | Usar el objeto como texto | __toString() (automática) |
Countable | count($obj) | count(): int |
IteratorAggregate | foreach ($obj as ...) | getIterator(): Iterator |
ArrayAccess | $obj['clave'], isset($obj['x']), unset(...) | offsetExists, offsetGet, offsetSet, offsetUnset |
JsonSerializable | Controlar json_encode($obj) | jsonSerialize(): mixed |
Una colección que las implementa todas:
<?php
declare(strict_types=1);
final class Producto implements JsonSerializable
{
public function __construct(
public readonly string $sku,
public readonly string $nombre,
public readonly float $precio,
) {}
public function jsonSerialize(): array
{
return ['sku' => $this->sku, 'nombre' => $this->nombre, 'precio' => $this->precio];
}
}
/**
* @implements IteratorAggregate<string, Producto>
* @implements ArrayAccess<string, Producto>
*/
final class Catalogo implements Countable, IteratorAggregate, ArrayAccess, JsonSerializable
{
/** @var array<string, Producto> */
private array $productos = [];
public function anadir(Producto $p): void
{
$this->productos[$p->sku] = $p;
}
// Countable
public function count(): int
{
return count($this->productos);
}
// IteratorAggregate: devolver un iterador (un generador con yield es lo más cómodo)
public function getIterator(): Iterator
{
yield from $this->productos;
}
// ArrayAccess
public function offsetExists(mixed $sku): bool
{
return isset($this->productos[$sku]);
}
public function offsetGet(mixed $sku): Producto
{
return $this->productos[$sku] ?? throw new OutOfBoundsException("No existe $sku");
}
public function offsetSet(mixed $sku, mixed $producto): void
{
if (!$producto instanceof Producto) {
throw new InvalidArgumentException('Solo se admiten productos');
}
$this->productos[$producto->sku] = $producto; // $catalogo[] = $p también funciona
}
public function offsetUnset(mixed $sku): void
{
unset($this->productos[$sku]);
}
// JsonSerializable
public function jsonSerialize(): array
{
return array_values($this->productos);
}
}
$catalogo = new Catalogo();
$catalogo->anadir(new Producto('A1', 'Teclado', 45.0));
$catalogo[] = new Producto('B2', 'Ratón', 19.9); // offsetSet
echo count($catalogo) . PHP_EOL; // 2
echo $catalogo['A1']->nombre . PHP_EOL; // Teclado
var_dump(isset($catalogo['Z9'])); // bool(false)
foreach ($catalogo as $sku => $producto) { // getIterator
echo "$sku: {$producto->nombre}" . PHP_EOL;
}
echo json_encode($catalogo, JSON_UNESCAPED_UNICODE) . PHP_EOL;
// [{"sku":"A1","nombre":"Teclado","precio":45},{"sku":"B2","nombre":"Ratón","precio":19.9}]
💡 Consejo:
IteratorAggregateconyield fromes la forma más sencilla de hacer un objeto recorrible. La interfazIterator(concurrent,key,next,rewindyvalid) da más control pero exige mucho más código.
10. Clases anónimas
Una clase anónima se declara y se instancia en el mismo sitio, sin nombre. Es útil para implementaciones de un solo uso, sobre todo en pruebas:
<?php
declare(strict_types=1);
interface Reloj
{
public function ahora(): DateTimeImmutable;
}
final class Saludador
{
public function __construct(private Reloj $reloj) {}
public function saludo(): string
{
$hora = (int) $this->reloj->ahora()->format('H');
return match (true) {
$hora < 14 => 'Buenos días',
$hora < 21 => 'Buenas tardes',
default => 'Buenas noches',
};
}
}
// Un reloj "falso" que siempre devuelve las 22:00, sin crear un fichero de clase
$relojFijo = new class implements Reloj {
public function ahora(): DateTimeImmutable
{
return new DateTimeImmutable('2026-10-03 22:00');
}
};
echo new Saludador($relojFijo)->saludo() . PHP_EOL; // Buenas noches
// También pueden recibir argumentos, heredar y usar traits
$logger = new class('app') {
public function __construct(private string $canal) {}
public function info(string $m): void { echo "[{$this->canal}] $m" . PHP_EOL; }
};
$logger->info('Arrancando');
11. Atributos
📘 Concepto: Los atributos (PHP 8.0) son metadatos que se añaden a clases, métodos, propiedades, parámetros o constantes con la sintaxis
#[NombreAtributo]. No cambian el código por sí mismos: PHP o tu propio código los leen para actuar en consecuencia. Frameworks como Symfony los usan masivamente:#[Route('/blog')],#[ORM\Entity].
Atributos nativos
| Atributo | Versión | Efecto |
|---|---|---|
#[\Override] | 8.3 | Error si el método no sobrescribe nada en el padre o interfaz |
#[\Deprecated] | 8.4 | Emite un aviso E_USER_DEPRECATED al usar la función, método o constante |
#[\NoDiscard] | 8.5 | Aviso si se ignora el valor de retorno (módulo 07) |
#[\SensitiveParameter] | 8.2 | Oculta el valor del parámetro en las trazas de error (contraseñas) |
#[\AllowDynamicProperties] | 8.2 | Permite propiedades dinámicas en esa clase |
#[\ReturnTypeWillChange] | 8.1 | Silencia avisos de compatibilidad de tipos de retorno en código antiguo |
#[\Override] (PHP 8.3)
<?php
declare(strict_types=1);
class Notificacion
{
public function enviar(): string { return 'Enviando...'; }
}
final class NotificacionUrgente extends Notificacion
{
#[\Override]
public function enviar(): string { return '¡URGENTE! ' . parent::enviar(); }
#[\Override]
public function enviarr(): string { return ''; }
// Fatal error: NotificacionUrgente::enviarr() has #[\Override] attribute,
// but no matching parent method exists
}
💡 Consejo:
#[\Override]te protege de errores tipográficos y de cambios en la clase padre: si alguien renombra el método del padre, tu clase dejará de compilar en lugar de quedarse con un método “huérfano” que nunca se llama.
#[\Deprecated] (PHP 8.4)
<?php
declare(strict_types=1);
final class Calculadora
{
#[\Deprecated(message: 'usa precioFinal() en su lugar', since: '2.0')]
public function calcularPrecio(float $base): float
{
return $this->precioFinal($base);
}
public function precioFinal(float $base): float
{
return round($base * 1.21, 2);
}
}
new Calculadora()->calcularPrecio(10);
// Deprecated: Method Calculadora::calcularPrecio() is deprecated since 2.0, usa precioFinal() en su lugar
Antes solo se podía documentar con @deprecated en PHPDoc; ahora PHP avisa en tiempo de ejecución, igual que con las funciones nativas obsoletas.
Crear y leer tus propios atributos
Un atributo es una clase marcada con #[\Attribute]. Para leerlo se usa la API de Reflection, que permite a un programa examinar su propia estructura:
<?php
declare(strict_types=1);
#[\Attribute(\Attribute::TARGET_METHOD)]
final readonly class Ruta
{
public function __construct(
public string $ruta,
public string $metodo = 'GET',
) {}
}
final class ProductoController
{
#[Ruta('/productos')]
public function listar(): string { return 'Listado de productos'; }
#[Ruta('/productos', metodo: 'POST')]
public function crear(): string { return 'Producto creado'; }
public function ayudante(): void {} // Sin atributo: no es una ruta
}
// Un "mini router" que descubre las rutas leyendo los atributos
$rutas = [];
$clase = new ReflectionClass(ProductoController::class);
foreach ($clase->getMethods() as $metodo) {
foreach ($metodo->getAttributes(Ruta::class) as $atributo) {
$ruta = $atributo->newInstance(); // Crea el objeto Ruta con sus argumentos
$rutas["{$ruta->metodo} {$ruta->ruta}"] = [$clase->getName(), $metodo->getName()];
}
}
print_r($rutas);
// [GET /productos] => [ProductoController, listar]
// [POST /productos] => [ProductoController, crear]
[$controlador, $accion] = $rutas['POST /productos'];
echo (new $controlador())->$accion() . PHP_EOL; // Producto creado
Así funcionan, simplificando mucho, los routers de Symfony. Lo implementarás de verdad en el módulo 19.
12. Herencia de excepciones: un adelanto
Las excepciones son un ejemplo perfecto de herencia y polimorfismo: todas heredan de Exception (o implementan Throwable), y puedes crear las tuyas:
<?php
declare(strict_types=1);
class TiendaException extends Exception {}
final class StockInsuficienteException extends TiendaException
{
public function __construct(public readonly string $sku, public readonly int $disponible)
{
parent::__construct("Stock insuficiente de $sku (quedan $disponible)");
}
}
try {
throw new StockInsuficienteException('A1', 2);
} catch (TiendaException $e) { // Captura cualquier hija de TiendaException
echo $e->getMessage() . PHP_EOL;
}
Lo verás en profundidad en el módulo 14.
13. Principios SOLID
SOLID son cinco principios de diseño orientado a objetos que hacen el código más fácil de mantener, ampliar y probar. Son guías, no leyes: aplícalas con sentido común.
| Letra | Principio | En una frase |
|---|---|---|
| S | Single Responsibility (responsabilidad única) | Una clase debe tener un único motivo para cambiar |
| O | Open/Closed (abierto/cerrado) | Abierto a extensión, cerrado a modificación |
| L | Liskov Substitution (sustitución de Liskov) | Una hija debe poder usarse donde se espera el padre sin sorpresas |
| I | Interface Segregation (segregación de interfaces) | Mejor varias interfaces pequeñas que una enorme |
| D | Dependency Inversion (inversión de dependencias) | Depende de abstracciones (interfaces), no de clases concretas |
S: responsabilidad única
<?php
// ❌ Una clase que valida, guarda en BD, envía correos y genera PDF
class GestorPedidos
{
public function procesar(array $datos): void { /* validar + guardar + email + pdf */ }
}
// ✅ Cada clase con una responsabilidad
final class ValidadorPedido { public function validar(array $datos): array { return []; } }
final class RepositorioPedidos { public function guardar(array $pedido): void {} }
final class EmailConfirmacion { public function enviar(array $pedido): void {} }
O: abierto/cerrado
Lo viste en el polimorfismo: para añadir un método de pago se crea una clase nueva que implementa MetodoPago, sin modificar Caja.
L: sustitución de Liskov
<?php
// ❌ Viola Liskov: un Pinguino "es un" Ave pero rompe lo que se espera de volar()
class Ave { public function volar(): string { return 'Volando'; } }
class Pinguino extends Ave
{
public function volar(): string { throw new LogicException('Los pingüinos no vuelan'); }
}
// ✅ Modela solo lo que es cierto para todos
interface Ave2 { public function comer(): string; }
interface Voladora { public function volar(): string; }
final class Gorrion implements Ave2, Voladora
{
public function comer(): string { return 'Comiendo semillas'; }
public function volar(): string { return 'Volando'; }
}
final class Pinguino2 implements Ave2
{
public function comer(): string { return 'Comiendo peces'; }
}
I: segregación de interfaces
<?php
// ❌ Obliga a una impresora sencilla a implementar fax y escáner
interface Multifuncion { public function imprimir(): void; public function escanear(): void; public function fax(): void; }
// ✅ Interfaces pequeñas y combinables
interface Impresora { public function imprimir(): void; }
interface Escaner { public function escanear(): void; }
final class ImpresoraBasica implements Impresora { public function imprimir(): void {} }
final class Multifuncional implements Impresora, Escaner
{
public function imprimir(): void {}
public function escanear(): void {}
}
D: inversión de dependencias
<?php
declare(strict_types=1);
// ❌ Acoplado a una clase concreta: imposible cambiar de proveedor o probar sin enviar correos
final class RegistroAcoplado
{
public function registrar(string $email): void
{
$mailer = new NotificadorEmail(); // Dependencia "escondida" y fija
$mailer->enviar($email, 'Bienvenido');
}
}
// ✅ Depende de la interfaz y la recibe desde fuera (inyección de dependencias)
final class Registro
{
public function __construct(private Notificador $notificador) {}
public function registrar(string $email): void
{
$this->notificador->enviar($email, 'Bienvenido');
}
}
$registro = new Registro(new NotificadorEmail()); // En producción
$registroTest = new Registro(new class implements Notificador { // En pruebas
public function enviar(string $d, string $m): bool { return true; }
});
📘 Concepto: La inyección de dependencias consiste en que un objeto reciba lo que necesita (normalmente por el constructor) en lugar de crearlo él mismo. Es la base del contenedor de servicios de Symfony y de las pruebas con dobles (módulo 18).
14. Composición frente a herencia
📘 Concepto: La composición consiste en construir objetos complejos combinando otros objetos que se guardan como propiedades (“tiene un”), en lugar de heredar de ellos (“es un”). El consejo clásico del diseño orientado a objetos es: “favorece la composición sobre la herencia”.
El problema de la herencia excesiva
Notificador
├── NotificadorEmail
│ ├── NotificadorEmailConLog
│ └── NotificadorEmailConReintentos
│ └── NotificadorEmailConReintentosYLog ← explosión de combinaciones
└── NotificadorSms
├── NotificadorSmsConLog
└── ...
Cada combinación de comportamientos necesita una clase nueva, y un cambio en el padre afecta a toda la jerarquía (clase base frágil).
La solución con composición (patrón decorador)
<?php
declare(strict_types=1);
// Reutilizamos la interfaz Notificador y las clases NotificadorEmail / NotificadorSms de la sección 3
final class ConLog implements Notificador
{
public function __construct(private Notificador $interno) {} // "Tiene un" notificador
public function enviar(string $destinatario, string $mensaje): bool
{
$ok = $this->interno->enviar($destinatario, $mensaje);
echo ' [log] ' . ($ok ? 'OK' : 'FALLO') . " → $destinatario" . PHP_EOL;
return $ok;
}
}
final class ConReintentos implements Notificador
{
public function __construct(private Notificador $interno, private int $intentos = 3) {}
public function enviar(string $destinatario, string $mensaje): bool
{
for ($i = 1; $i <= $this->intentos; $i++) {
if ($this->interno->enviar($destinatario, $mensaje)) {
return true;
}
}
return false;
}
}
// Combinaciones libres, sin crear clases nuevas
$email = new ConLog(new ConReintentos(new NotificadorEmail()));
$sms = new ConLog(new NotificadorSms());
$email->enviar('ana@correo.es', 'Tu factura está disponible');
$sms->enviar('+34600111222', 'Código de verificación: 482913');
| Criterio | Herencia | Composición |
|---|---|---|
| Relación | “es un” | “tiene un” / “usa un” |
| Se decide | Al escribir el código (estática) | Al crear los objetos (dinámica) |
| Acoplamiento | Alto: la hija depende de los detalles del padre | Bajo: solo de la interfaz |
| Combinar comportamientos | Explosión de subclases | Se combinan objetos |
| Pruebas | Difícil aislar | Fácil sustituir piezas |
| Cuándo usarla | Relación “es un” clara y estable (Circulo es una Figura) | Casi siempre que dudes |
💡 Consejo: Pregúntate: “¿la hija es siempre y en todo un caso particular del padre?”. Si la respuesta es “más o menos” o “de momento”, usa composición e interfaces.
15. Errores comunes
| Error | Causa | Solución |
|---|---|---|
| Propiedades del padre sin inicializar | Falta parent::__construct() | Llámalo en el constructor de la hija |
Cannot instantiate abstract class | new de una clase abstracta | Instancia una hija concreta |
Class X contains 1 abstract method and must therefore be declared abstract or implement the remaining methods | Falta implementar un método abstracto o de interfaz | Implementa todos los métodos del contrato |
Declaration of B::m() must be compatible with A::m() | Firma incompatible al sobrescribir | Respeta tipos y visibilidad del padre |
ValueError con Enum::from() | Valor no válido | Usa tryFrom() con datos externos |
Trait method m has not been applied because of collision | Dos traits con el mismo método | Resuelve con insteadof / as |
new self() devuelve el padre en una hija | self es la clase donde se escribió | Usa new static() y retorno : static |
| Método nunca llamado tras renombrar el padre | Sobrescritura “huérfana” | Añade #[\Override] |
16. Ejercicios
Ejercicio 1: Jerarquía de vehículos
Crea una clase abstracta Vehiculo (matrícula, marca, método abstracto costeAlquilerDia(): float y método concreto presupuesto(int $dias): string) y tres clases final: Coche, Moto y Furgoneta, cada una con su forma de calcular el coste (la furgoneta, según su capacidad de carga). Usa #[\Override] en todos los métodos sobrescritos y muestra un presupuesto de 7 días para una flota de 5 vehículos.
Ejercicio 2: Exportadores polimórficos
Define la interfaz Exportador con exportar(array $filas): string y una propiedad de interfaz public string $extension { get; }. Implementa ExportadorCsv, ExportadorJson y ExportadorHtml (tabla). Crea una función guardarInforme(Exportador $e, array $filas, string $nombre) que escriba el fichero con la extensión correcta sin usar ningún if sobre el tipo de exportador.
Ejercicio 3: Enum de estados de una incidencia
Crea un backed enum EstadoIncidencia (abierta, en_progreso, resuelta, cerrada, reabierta) con métodos etiqueta(), color() (clases de Bootstrap), transicionesPermitidas(): array y puedePasarA(self $nuevo): bool. Crea una clase Incidencia con public private(set) EstadoIncidencia $estado y un método cambiarEstado() que lance una excepción ante transiciones no permitidas. Genera un <select> con cases() y valida lo recibido con tryFrom().
Ejercicio 4: Traits con conflicto
Crea los traits Loggable (con log(string $m) que escribe en pantalla) y FileLogger (con un log(string $m) que escribe en fichero) y una clase que use ambos: el log() por defecto debe ser el de fichero y el de pantalla debe estar disponible como logPantalla(). Añade a Loggable un método abstracto prefijo(): string que la clase deba implementar.
Ejercicio 5: Colección tipada
Crea una clase ListaTareas que implemente Countable, IteratorAggregate, ArrayAccess y JsonSerializable y solo admita objetos Tarea (lanzando InvalidArgumentException si no). Añade los métodos filtrar(callable $fn): static (devuelve una lista nueva), pendientes(): static y __toString(). Prueba count(), foreach, $lista[] = ..., isset($lista[0]) y json_encode().
Ejercicio 6: Mini router con atributos
Crea un atributo #[Ruta(string $ruta, string $metodo = 'GET')] aplicable a métodos y otro #[RequiereRol(string $rol)]. Escribe dos controladores con varios métodos anotados y una clase Router que, mediante Reflection, registre todas las rutas y, dado un método y una URL (GET /productos), ejecute la acción correspondiente o devuelva un 404. Si la acción tiene #[RequiereRol], comprueba el rol de un usuario simulado y devuelve 403 si no lo tiene.
Ejercicio 7: Refactorizar herencia a composición
Parte de esta jerarquía: Informe → InformePdf → InformePdfConMarcaDeAgua → InformePdfConMarcaDeAguaYFirma. Rediséñala con una interfaz Informe y decoradores (ConMarcaDeAgua, ConFirma, ConCabecera) que se puedan combinar en cualquier orden. Explica en un comentario qué principios SOLID se aplican.
Resumen
| Concepto | Sintaxis |
|---|---|
| Herencia | class B extends A + parent::metodo() |
| Impedir herencia / sobrescritura | final class, final public function |
| Clase abstracta | abstract class X { abstract public function m(): T; } |
| Interfaz | interface I { public function m(): T; } / implements I, J |
| Propiedad en interfaz (8.4) | public string $nombre { get; } |
| Trait | trait T { } / use T; |
| Conflicto de traits | A::m insteadof B; B::m as otro; |
| Enum puro | enum Palo { case Oros; } |
| Enum respaldado | enum E: string { case A = 'a'; } + from(), tryFrom(), cases() |
| Late static binding | new static(), static::$x, retorno : static |
| Texto | __toString() → Stringable |
| Acceso mágico | __get, __set, __isset, __unset, __call, __callStatic |
| Invocable | __invoke() → $obj() |
| Serialización | __serialize() / __unserialize() |
| Interfaces nativas | Countable, IteratorAggregate, ArrayAccess, JsonSerializable |
| Clase anónima | new class implements I { ... } |
| Atributos nativos | #[\Override] (8.3), #[\Deprecated] (8.4), #[\NoDiscard] (8.5) |
| Atributo propio | #[\Attribute] + ReflectionClass::getMethods()->getAttributes() |
| SOLID | S, O, L, I, D |
| Composición | __construct(private Interfaz $dependencia) |