Skip to content

POO: herencia, interfaces y traits ​

Ya sabes crear clases. Ahora aprenderás a relacionarlas: reutilizar código con herencia y traits, definir contratos con interfaces y organizar todo con namespaces. Son las piezas con las que están construidos Laravel y cualquier proyecto PHP profesional.

Concepto ​

  • Herencia (extends): una clase hija recibe todo lo de su clase padre y puede añadir o cambiar cosas. Relación "es un": un Administrador es un Usuario.
  • Clase abstracta (abstract): un padre incompleto que no se puede instanciar; obliga a las hijas a completar ciertos métodos.
  • Interfaz (interface): un contrato. Dice qué métodos debe tener una clase, pero no cómo. Relación "puede": un Pdf y un Excel pueden exportarse.
  • Trait (trait): un bloque de código que se "copia y pega" en varias clases sin relación entre sí.
  • Namespace: el "apellido" de una clase, para que no choque con otra del mismo nombre.

Herencia ​

php
<?php
declare(strict_types=1);

class Usuario
{
    public function __construct(
        protected string $nombre,   // protected: accesible desde las hijas
        protected string $email,
    ) {}

    public function saludo(): string
    {
        return "Hola, {$this->nombre}";
    }

    public function puedeEntrarAlPanel(): bool
    {
        return false;
    }
}

class Administrador extends Usuario
{
    public function __construct(
        string $nombre,
        string $email,
        private array $permisos = [],
    ) {
        // Llamamos al constructor del padre para que inicialice nombre y email
        parent::__construct($nombre, $email);
    }

    // Sobrescribimos (override) un método del padre
    public function puedeEntrarAlPanel(): bool
    {
        return true;
    }

    // Extendemos un método del padre reutilizando su lógica
    public function saludo(): string
    {
        return parent::saludo() . ' (administrador)';
    }

    public function tienePermiso(string $permiso): bool
    {
        return in_array($permiso, $this->permisos, true);
    }
}

$cliente = new Usuario('Ana', 'ana@correo.com');
$admin   = new Administrador('Luis', 'luis@correo.com', ['borrar_usuarios']);

echo $cliente->saludo() . PHP_EOL; // Hola, Ana
echo $admin->saludo() . PHP_EOL;   // Hola, Luis (administrador)
var_dump($admin->puedeEntrarAlPanel()); // true
var_dump($admin instanceof Usuario);    // true: un admin ES un usuario

El atributo #[\Override] (PHP 8.3) ​

Marca los métodos que sobrescriben a uno del padre. Si te equivocas en el nombre (o el padre lo elimina), PHP lanza un error en vez de crear un método nuevo en silencio:

php
<?php
class Administrador extends Usuario
{
    #[\Override]
    public function puedeEntrarAlPanel(): bool
    {
        return true;
    }
}

Error común

Abusar de la herencia: cadenas como Animal > Mamifero > Perro > PerroGuardian > PerroGuardianEntrenado se vuelven imposibles de mantener. Regla práctica: no más de 2 niveles, y prefiere composición (un objeto que contiene a otro) cuando la relación no sea claramente "es un".

Clases abstractas ​

php
<?php
declare(strict_types=1);

abstract class MetodoPago
{
    public function __construct(protected float $monto) {}

    // Método abstracto: sin cuerpo. Cada hija DEBE implementarlo.
    abstract protected function procesar(): bool;

    abstract public function nombre(): string;

    // Método concreto compartido: el "esqueleto" del proceso (patrón Template Method)
    public function pagar(): string
    {
        if ($this->monto <= 0) {
            return 'Monto inválido';
        }
        return $this->procesar()
            ? "Pago de \${$this->monto} con {$this->nombre()} aprobado"
            : "Pago con {$this->nombre()} rechazado";
    }
}

class PagoTarjeta extends MetodoPago
{
    protected function procesar(): bool
    {
        // Aquí iría la llamada a Stripe, Conekta, Mercado Pago...
        return true;
    }

    public function nombre(): string { return 'tarjeta'; }
}

class PagoOxxo extends MetodoPago
{
    protected function procesar(): bool
    {
        // Generaría una referencia de pago en efectivo
        return $this->monto <= 10000; // límite de ejemplo
    }

    public function nombre(): string { return 'OXXO'; }
}

// $p = new MetodoPago(100); // Error: no se puede instanciar una clase abstracta
echo (new PagoTarjeta(599))->pagar() . PHP_EOL;
echo (new PagoOxxo(15000))->pagar() . PHP_EOL;

Interfaces ​

Una interfaz define un contrato. Cualquier clase que la implemente (implements) promete tener esos métodos. Una clase puede implementar varias interfaces, pero sólo heredar de una clase.

php
<?php
declare(strict_types=1);

interface Notificador
{
    // Sólo firmas, sin cuerpo. Todos los métodos son públicos.
    public function enviar(string $destino, string $mensaje): bool;
}

class NotificadorEmail implements Notificador
{
    public function enviar(string $destino, string $mensaje): bool
    {
        echo "Email a $destino: $mensaje" . PHP_EOL;
        return true; // en la vida real: PHPMailer
    }
}

class NotificadorWhatsApp implements Notificador
{
    public function __construct(private string $token) {}

    public function enviar(string $destino, string $mensaje): bool
    {
        echo "WhatsApp a $destino: $mensaje" . PHP_EOL;
        return true; // en la vida real: API de WhatsApp Business
    }
}

// Esta clase NO sabe ni le importa cómo se envía: sólo exige un Notificador
class ServicioPedidos
{
    public function __construct(private Notificador $notificador) {}

    public function confirmar(int $pedidoId, string $cliente): void
    {
        // ... guardar el pedido
        $this->notificador->enviar($cliente, "Tu pedido #$pedidoId fue confirmado");
    }
}

// Cambiar el canal es tan fácil como pasar otro objeto
$servicio = new ServicioPedidos(new NotificadorEmail());
$servicio->confirmar(1001, 'ana@correo.com');

$servicio = new ServicioPedidos(new NotificadorWhatsApp('token-secreto'));
$servicio->confirmar(1002, '+525512345678');

Esto se llama inyección de dependencias y programar contra interfaces: es exactamente lo que hace Laravel con su contenedor de servicios.

Buena práctica

Tipa los parámetros con la interfaz (Notificador), no con la clase concreta (NotificadorEmail). Así puedes cambiar la implementación o usar una falsa en las pruebas sin tocar el resto del código.

Interfaces útiles de PHP ​

php
<?php
// Countable: permite usar count($objeto)
// IteratorAggregate: permite usar foreach sobre el objeto
// JsonSerializable: controla cómo se convierte a JSON
// Stringable: se implementa sola al definir __toString()

class ListaTareas implements Countable, IteratorAggregate, JsonSerializable
{
    private array $tareas = [];

    public function agregar(string $tarea): void { $this->tareas[] = $tarea; }

    public function count(): int { return count($this->tareas); }

    public function getIterator(): ArrayIterator { return new ArrayIterator($this->tareas); }

    public function jsonSerialize(): array { return ['total' => count($this), 'tareas' => $this->tareas]; }
}

$lista = new ListaTareas();
$lista->agregar('Diseñar logo');
$lista->agregar('Maquetar landing');

echo count($lista);           // 2
foreach ($lista as $t) echo "- $t\n";
echo json_encode($lista);     // {"total":2,"tareas":["Diseñar logo","Maquetar landing"]}

Traits ​

Cuando varias clases sin relación necesitan el mismo comportamiento, un trait evita copiar y pegar:

php
<?php
declare(strict_types=1);

trait TieneFechas
{
    private ?DateTimeImmutable $creadoEn = null;

    public function marcarCreado(): void
    {
        $this->creadoEn = new DateTimeImmutable();
    }

    public function creadoEn(): string
    {
        return $this->creadoEn?->format('d/m/Y H:i') ?? 'sin fecha';
    }
}

trait GeneraSlug
{
    public function slug(string $texto): string
    {
        $texto = strtolower(trim($texto));
        $texto = strtr($texto, ['á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u', 'ñ' => 'n']);
        return trim(preg_replace('/[^a-z0-9]+/', '-', $texto), '-');
    }
}

class Articulo
{
    use TieneFechas, GeneraSlug; // "pega" el código de ambos traits

    public function __construct(public string $titulo) {}

    public function url(): string
    {
        return '/blog/' . $this->slug($this->titulo);
    }
}

class Producto
{
    use TieneFechas;
}

$a = new Articulo('¡Cómo crear tu tienda en línea!');
$a->marcarCreado();
echo $a->url();        // /blog/como-crear-tu-tienda-en-linea
echo $a->creadoEn();   // 18/10/2026 14:35

Laravel usa traits constantemente: use HasFactory, Notifiable; en el modelo User.

Namespaces ​

En un proyecto grande (y con librerías de terceros) es fácil que existan dos clases Usuario o Logger. Los namespaces les dan un "apellido" único.

php
<?php
// src/Modelos/Usuario.php
declare(strict_types=1);

namespace App\Modelos; // debe ser la primera instrucción después de declare

class Usuario
{
    public function __construct(public string $nombre) {}
}
php
<?php
// src/Servicios/Registro.php
declare(strict_types=1);

namespace App\Servicios;

use App\Modelos\Usuario;              // importar una clase
use App\Contratos\Notificador as N;   // importar con alias
use DateTimeImmutable;                // las clases de PHP están en el namespace global

class Registro
{
    public function crear(string $nombre): Usuario
    {
        $u = new Usuario($nombre);
        $fecha = new DateTimeImmutable(); // sin el "use" habría que escribir \DateTimeImmutable
        return $u;
    }
}
php
<?php
// index.php
require 'src/Modelos/Usuario.php';
require 'src/Servicios/Registro.php';

use App\Servicios\Registro;

$registro = new Registro();
$usuario = $registro->crear('Ana');
echo $usuario::class; // App\Modelos\Usuario (nombre completo)

La convención es que el namespace coincida con la carpeta: App\Modelos\Usuario vive en src/Modelos/Usuario.php. Así Composer puede cargar las clases solo, sin escribir ningún require. Lo verás en la siguiente lección: Composer y PSR-4.

Error común

Dentro de un namespace, new Exception() busca App\Servicios\Exception, que no existe. Para clases nativas de PHP escribe \Exception o agrega use Exception; arriba.

¿Cuándo usar cada cosa? ​

Necesito...Usa
Una versión especializada de una clase ("es un")Herencia
Un esqueleto común con pasos que cada hija completaClase abstracta
Que clases distintas sean intercambiablesInterfaz
Reutilizar métodos en clases sin relaciónTrait
Evitar choques de nombres y organizar carpetasNamespace

Ejercicios ​

  1. Crea una clase abstracta Figura con un método abstracto area(): float y un método concreto describir(): string que devuelva "Soy un X con área Y". Crea Circulo y Rectangulo.
  2. Crea una interfaz Exportador con exportar(array $filas): string y dos implementaciones: ExportadorCsv y ExportadorJson. Escribe una función que reciba cualquier Exportador.
  3. Crea un trait Loggable con un método log(string $mensaje) que escriba en un arreglo interno con la fecha, y úsalo en dos clases distintas.
Ver solución
php
<?php
declare(strict_types=1);

// 1
abstract class Figura
{
    abstract public function area(): float;

    public function describir(): string
    {
        // static::class devuelve el nombre de la clase hija real
        return sprintf('Soy un %s con área %.2f', static::class, $this->area());
    }
}

final class Circulo extends Figura
{
    public function __construct(private float $radio) {}

    #[\Override]
    public function area(): float { return M_PI * $this->radio ** 2; }
}

final class Rectangulo extends Figura
{
    public function __construct(private float $ancho, private float $alto) {}

    #[\Override]
    public function area(): float { return $this->ancho * $this->alto; }
}

foreach ([new Circulo(2), new Rectangulo(3, 4)] as $f) {
    echo $f->describir() . PHP_EOL;
}

// 2
interface Exportador
{
    public function exportar(array $filas): string;
}

final class ExportadorCsv implements Exportador
{
    public function exportar(array $filas): string
    {
        $salida = fopen('php://temp', 'r+');
        fputcsv($salida, array_keys($filas[0] ?? []));
        foreach ($filas as $fila) fputcsv($salida, $fila);
        rewind($salida);
        return stream_get_contents($salida);
    }
}

final class ExportadorJson implements Exportador
{
    public function exportar(array $filas): string
    {
        return json_encode($filas, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    }
}

function generarReporte(Exportador $exp, array $datos): void
{
    echo $exp->exportar($datos) . PHP_EOL;
}

$ventas = [['mes' => 'enero', 'total' => 1200], ['mes' => 'febrero', 'total' => 980]];
generarReporte(new ExportadorCsv(), $ventas);
generarReporte(new ExportadorJson(), $ventas);

// 3
trait Loggable
{
    private array $registro = [];

    public function log(string $mensaje): void
    {
        $this->registro[] = '[' . date('H:i:s') . "] $mensaje";
    }

    public function verLog(): array { return $this->registro; }
}

final class Inventario { use Loggable; }
final class Facturador { use Loggable; }

$inv = new Inventario();
$inv->log('Stock actualizado');
print_r($inv->verLog());

Resumen ​

  • extends hereda propiedades y métodos; parent:: llama al padre; #[\Override] protege las sobrescrituras.
  • Las clases abstractas son moldes incompletos que obligan a implementar métodos.
  • Las interfaces son contratos; tipar con interfaces hace el código flexible y testeable.
  • Los traits reutilizan código entre clases sin relación.
  • Los namespaces organizan y evitan choques; deben coincidir con la estructura de carpetas.

Academia Web — aprende, copia y construye.