Módulo 12: POO avanzada

Objetivos del módulo

  • Reutilizar código con herencia: extends, parent::, sobrescritura y final
  • 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 static frente a self y 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 Gato es un Animal.

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 (public no puede pasar a protected).
  • 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 final por 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 Factura puede exportarse, un Usuario puede 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, readonly o 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: match con enums es exhaustivo en la práctica: si añades un caso nuevo y olvidas tratarlo, match lanzará un UnhandledMatchError en 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, obtienes null. 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 nativa Stringable (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: __get y __set hacen 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/__set para 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 a unserialize() 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: IteratorAggregate con yield from es la forma más sencilla de hacer un objeto recorrible. La interfaz Iterator (con current, key, next, rewind y valid) 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)