Tema
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": unAdministradores unUsuario. - 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": unPdfy unExcelpueden 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 usuarioEl 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:35Laravel 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 completa | Clase abstracta |
| Que clases distintas sean intercambiables | Interfaz |
| Reutilizar métodos en clases sin relación | Trait |
| Evitar choques de nombres y organizar carpetas | Namespace |
Ejercicios
- Crea una clase abstracta
Figuracon un método abstractoarea(): floaty un método concretodescribir(): stringque devuelva "Soy un X con área Y". CreaCirculoyRectangulo. - Crea una interfaz
Exportadorconexportar(array $filas): stringy dos implementaciones:ExportadorCsvyExportadorJson. Escribe una función que reciba cualquierExportador. - Crea un trait
Loggablecon un métodolog(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
extendshereda 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.