Herencia, interfaces, traits y namespaces en PHP
En esta lección
En la lección anterior creaste clases independientes. En cuanto tienes varias parecidas (socios, monitores y empleados; o clases de yoga, de escalada y de pilates) empiezas a repetir código. En esta lección aprenderás las herramientas de PHP para reutilizar y organizar clases: la herencia con extends, las clases abstractas, las interfaces, el polimorfismo, los traits y los espacios de nombres (namespaces), con los que se organizan todos los proyectos PHP modernos.
Herencia con extends
La herencia permite crear una clase nueva a partir de otra que ya existe. La clase nueva (hija o subclase) recibe todas las propiedades y métodos de la original (padre o superclase) y puede añadir los suyos o cambiar los que hereda. Se indica con extends:
<?php
class Actividad
{
public function __construct(
protected string $nombre,
protected float $precio,
) {}
public function descripcion(): string
{
return "$this->nombre: $this->precio €";
}
}
class Curso extends Actividad
{
public function __construct(
string $nombre,
float $precio,
private int $sesiones,
) {
parent::__construct($nombre, $precio);
}
public function descripcion(): string
{
return parent::descripcion() . " ($this->sesiones sesiones)";
}
}
$suelta = new Actividad('Clase de yoga', 9);
$curso = new Curso('Iniciación a la escalada', 80, 6);
echo $suelta->descripcion(), "\n";
echo $curso->descripcion(), "\n";
// Clase de yoga: 9 €
// Iniciación a la escalada: 80 € (6 sesiones)
Fíjate en tres cosas:
protected: las propiedades del padre sonprotected, noprivate, para que la clase hija pueda usarlas con$this->nombre. Si fueranprivate, solo existirían paraActividad.parent::__construct(...): si la hija tiene su propio constructor, el del padre no se ejecuta solo. Tienes que llamarlo tú. Si lo olvidas,$nombrey$preciose quedan sin valor.- Sobrescribir un método:
Cursovuelve a escribirdescripcion(). Conparent::descripcion()aprovecha la versión del padre y le añade información.
Una clase solo puede heredar de una clase. Usa la herencia cuando la relación sea “es un”: un curso es una actividad. Si la relación es “tiene un” (un socio tiene un bono), no heredes: guarda un objeto dentro de otro como propiedad. Esto último se llama composición y, en caso de duda, suele ser la mejor opción.
final: prohibir la herencia
Con final delante de una clase, nadie puede heredar de ella. Delante de un método, las hijas no pueden sobrescribirlo. Es útil para métodos cuyo comportamiento no debe cambiar, como un cálculo de seguridad:
<?php
final class Factura {} // class Rectificativa extends Factura -> error
class Usuario
{
final public function comprobarClave(string $clave): bool
{
return strlen($clave) >= 8;
}
}
Clases abstractas
A veces la clase padre representa una idea general que no tiene sentido crear por sí sola. Nadie paga “una tarifa” sin más: paga una tarifa mensual, un bono o una entrada de día. Una clase abstracta no se puede instanciar con new; solo sirve de base para otras. Además, puede tener métodos abstractos: métodos sin cuerpo que obliga a escribir a cada hija.
<?php
abstract class Tarifa
{
public function __construct(protected string $nombre) {}
abstract public function precio(int $dias): float;
public function resumen(int $dias): string
{
return sprintf('%s, %d días: %.2f €', $this->nombre, $dias, $this->precio($dias));
}
}
class EntradaDia extends Tarifa
{
public function precio(int $dias): float
{
return 11 * $dias;
}
}
class CuotaMensual extends Tarifa
{
public function precio(int $dias): float
{
return 45 * (int) ceil($dias / 30);
}
}
$tarifas = [new EntradaDia('Entrada de día'), new CuotaMensual('Cuota mensual')];
foreach ($tarifas as $tarifa) {
echo $tarifa->resumen(12), "\n";
}
// Entrada de día, 12 días: 132.00 €
// Cuota mensual, 12 días: 45.00 €
resumen() está escrito una sola vez en la clase padre, pero llama a precio(), que cada hija calcula a su manera. Si creas una hija y olvidas escribir precio(), PHP te avisa con un error en cuanto cargas la clase. Y new Tarifa('x') da Cannot instantiate abstract class Tarifa.
Polimorfismo
El bucle anterior tiene algo importante: trata todas las tarifas igual, sin preguntar de qué clase es cada una, y cada objeto responde a precio() con su propia versión. Esa capacidad de que objetos distintos respondan al mismo mensaje cada uno a su manera se llama polimorfismo.
Gracias a él puedes añadir una tarifa nueva (por ejemplo, BonoDiezEntradas) sin tocar el bucle ni resumen(). Compara con la alternativa sin POO: un match con un caso por cada tipo de tarifa, repetido en cada sitio donde calcules precios.
Interfaces
Una interfaz es un contrato: una lista de métodos públicos que una clase se compromete a tener, sin decir cómo los hace. Se declara con interface y una clase la cumple con implements:
<?php
interface Notificador
{
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\n";
return true;
}
}
class NotificadorSms implements Notificador
{
public function enviar(string $destino, string $mensaje): bool
{
echo "[SMS a $destino] " . mb_substr($mensaje, 0, 20) . "\n";
return true;
}
}
function avisarCancelacion(Notificador $canal, string $destino): void
{
$canal->enviar($destino, 'Se cancela la clase del jueves por mantenimiento');
}
avisarCancelacion(new NotificadorEmail(), 'irene@ejemplo.es');
avisarCancelacion(new NotificadorSms(), '600111222');
// [email a irene@ejemplo.es] Se cancela la clase del jueves por mantenimiento
// [SMS a 600111222] Se cancela la clase
La función avisarCancelacion() pide un Notificador, no una clase concreta. Le da igual cómo se envíe el mensaje; solo necesita saber que el objeto tiene enviar(). Si mañana añades WhatsApp, creas otra clase que implemente la interfaz y la función sigue igual. Esta idea, programar contra interfaces, es la base de los frameworks como Laravel o Symfony.
Algunas reglas:
- Los métodos de una interfaz son siempre públicos y no tienen cuerpo.
- Una clase puede implementar varias interfaces a la vez:
class Socio implements JsonSerializable, Countable. - Una interfaz puede tener constantes y puede heredar de otras interfaces con
extends. instanceoftambién funciona con interfaces:$canal instanceof Notificador.
Interfaces que trae PHP
PHP incluye interfaces que, si las implementas, hacen que tus objetos funcionen con funciones del lenguaje. Por ejemplo, Countable hace que count() funcione con tu objeto:
<?php
class ListaEspera implements Countable
{
private array $personas = [];
public function apuntar(string $nombre): void
{
$this->personas[] = $nombre;
}
public function count(): int
{
return count($this->personas);
}
}
$espera = new ListaEspera();
$espera->apuntar('Nerea');
$espera->apuntar('Óscar');
echo count($espera), " personas en espera\n";
// 2 personas en espera
Otras que usarás: Stringable (la cumple automáticamente cualquier clase con __toString()), JsonSerializable (decide cómo se convierte el objeto a JSON; la verás en JSON y APIs) e IteratorAggregate (permite recorrer el objeto con foreach).
¿Clase abstracta o interfaz?
| Clase abstracta | Interfaz | |
|---|---|---|
| Puede tener código en los métodos | Sí | No |
| Puede tener propiedades | Sí | No (solo constantes) |
| Cuántas puede usar una clase | Una (extends) | Varias (implements) |
| Relación que expresa | “Es un tipo de” | “Sabe hacer” |
| Cuándo usarla | Clases muy parecidas que comparten código | Clases distintas que comparten un comportamiento |
Son compatibles: una clase abstracta puede implementar una interfaz y dejar a sus hijas parte del trabajo.
Traits: reutilizar código sin herencia
Imagina que quieres que socios, reservas y pagos guarden su fecha de creación. No tienen nada que ver entre sí, así que no tiene sentido que hereden de un padre común (y además cada uno puede necesitar ya otro padre). Un trait es un bloque de propiedades y métodos que se “copia” dentro de cualquier clase con use:
<?php
trait ConFechaCreacion
{
private ?DateTimeImmutable $creadoEn = null;
public function marcarCreacion(string $fecha = 'now'): void
{
$this->creadoEn = new DateTimeImmutable($fecha);
}
public function creadoEl(): string
{
return $this->creadoEn?->format('d/m/Y') ?? 'sin fecha';
}
}
class Socio
{
use ConFechaCreacion;
public function __construct(public string $nombre) {}
}
class Pago
{
use ConFechaCreacion;
}
$socio = new Socio('Hugo');
$socio->marcarCreacion('2026-09-15');
echo "$socio->nombre: {$socio->creadoEl()}\n";
echo 'Pago: ', (new Pago())->creadoEl(), "\n";
// Hugo: 15/09/2026
// Pago: sin fecha
Una clase puede usar varios traits separándolos con comas. Si dos traits tienen un método con el mismo nombre, se resuelve con insteadof (use A, B { A::guardar insteadof B; }), aunque si te pasa a menudo es señal de que el diseño se está complicando.
Consejo: los traits son cómodos, pero esconden código: al leer la clase no ves los métodos que trae. Úsalos para piezas pequeñas y genéricas (fechas, registro de mensajes), no para meter la lógica principal.
Espacios de nombres (namespaces)
En un proyecto real tendrás cientos de clases, muchas escritas por otras personas (librerías). ¿Qué pasa si tu clase Usuario choca con la Usuario de una librería? Los espacios de nombres funcionan como carpetas para las clases: dos clases pueden llamarse igual si están en espacios distintos.
Se declara con namespace en la primera línea del archivo (justo después de <?php y de declare, si lo hay), y lo habitual es que coincida con la carpeta:
<?php
// src/Modelos/Socio.php
namespace App\Modelos;
class Socio
{
public function __construct(public string $nombre) {}
}
El nombre completo de esa clase pasa a ser App\Modelos\Socio. Para usarla desde otro archivo, la importas con use y ya puedes escribir solo Socio:
<?php
// public/index.php
use App\Modelos\Socio;
use App\Servicios\Mailer as CorreoSaliente; // alias con "as"
require_once __DIR__ . '/../src/Modelos/Socio.php';
$socio = new Socio('Hugo');
echo $socio::class, "\n";
// App\Modelos\Socio
::class devuelve el nombre completo de una clase, muy útil para depurar. Fíjate en que use no carga el archivo: solo dice “cuando escriba Socio, me refiero a App\Modelos\Socio”. Cargar el archivo sigue siendo cosa de require o del autoload.
Dentro de un archivo con namespace, las clases de PHP como DateTimeImmutable o Exception se buscan primero en tu espacio. Para indicar que son las globales, impórtalas con use DateTimeImmutable; o escríbelas con barra inicial: new \DateTimeImmutable(). Las funciones (strlen, count…) sí se encuentran solas.
Autoload: adiós a los require
Con namespaces que coinciden con las carpetas, PHP puede cargar cada clase automáticamente. Registras una función con spl_autoload_register() y PHP la llama cada vez que usas una clase que aún no conoce:
<?php
// public/index.php
spl_autoload_register(function (string $clase): void {
// App\Modelos\Socio -> src/Modelos/Socio.php
$relativa = str_replace(['App\\', '\\'], ['', '/'], $clase);
$archivo = __DIR__ . '/../src/' . $relativa . '.php';
if (is_file($archivo)) {
require $archivo;
}
});
use App\Modelos\Socio;
$socio = new Socio('Hugo'); // carga src/Modelos/Socio.php automáticamente
echo $socio->nombre, "\n";
// Hugo
Esta convención (espacio de nombres = carpeta, clase = archivo) se llama PSR-4 y la sigue casi todo el ecosistema PHP. En la práctica no escribirás este autoload a mano: Composer lo genera por ti.
Errores frecuentes
- Olvidar
parent::__construct()en el constructor de la hija: las propiedades del padre se quedan sin inicializar. - Usar
privateen el padre y luego intentar leer la propiedad desde la hija: no existe para ella. Usaprotected. - Heredar solo para reutilizar código cuando la relación no es “es un”: acabas con jerarquías absurdas (
Pago extends Socio). Usa composición, interfaces o un trait. - Cambiar la firma al sobrescribir: si el método del padre devuelve
float, el de la hija no puede devolverstringni pedir más parámetros obligatorios. PHP lanza un error de compatibilidad. - Intentar hacer
newde una clase abstracta o de una interfaz: son moldes de moldes. Instancia una de sus hijas. - Creer que
usecarga el archivo: solo crea un alias. Necesitasrequireo autoload. - Olvidar la barra inicial con clases globales dentro de un namespace:
new DateTime()buscaApp\Modelos\DateTime. Escribe\DateTimeo impórtala conuse.
Resumen
| Concepto | Código |
|---|---|
| Heredar | class Curso extends Actividad |
| Llamar al padre | parent::__construct(...), parent::descripcion() |
| Visible para las hijas | protected |
| Prohibir herencia | final class, final public function |
| Clase abstracta | abstract class Tarifa { abstract public function precio(int $d): float; } |
| Interfaz | interface Notificador { public function enviar(...): bool; } |
| Implementar | class NotificadorEmail implements Notificador, Countable |
| Trait | trait ConFecha { ... } y dentro de la clase use ConFecha; |
| Espacio de nombres | namespace App\Modelos; |
| Importar | use App\Modelos\Socio; / use ... as Alias; |
| Nombre completo | Socio::class |
| Carga automática | spl_autoload_register(...) o el autoload de Composer |
En la siguiente lección aprenderás a gestionar los errores de tus programas lanzando y capturando excepciones: Excepciones.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.