Saltar al contenido
herencia-e-interfaces.php · devschool

Herencia, interfaces, traits y namespaces en PHP

Lección 19 de 26 · 12 min de lectura · Actualizado el

En esta lección
  1. Herencia con extends
  2. Clases abstractas
  3. Polimorfismo
  4. Interfaces
  5. Traits: reutilizar código sin herencia
  6. Espacios de nombres (namespaces)
  7. Errores frecuentes
  8. Resumen

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 son protected, no private, para que la clase hija pueda usarlas con $this->nombre. Si fueran private, solo existirían para Actividad.
  • parent::__construct(...): si la hija tiene su propio constructor, el del padre no se ejecuta solo. Tienes que llamarlo tú. Si lo olvidas, $nombre y $precio se quedan sin valor.
  • Sobrescribir un método: Curso vuelve a escribir descripcion(). Con parent::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.
  • instanceof tambié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 abstractaInterfaz
Puede tener código en los métodosSíNo
Puede tener propiedadesSíNo (solo constantes)
Cuántas puede usar una claseUna (extends)Varias (implements)
Relación que expresa“Es un tipo de”“Sabe hacer”
Cuándo usarlaClases muy parecidas que comparten códigoClases 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 private en el padre y luego intentar leer la propiedad desde la hija: no existe para ella. Usa protected.
  • 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 devolver string ni pedir más parámetros obligatorios. PHP lanza un error de compatibilidad.
  • Intentar hacer new de una clase abstracta o de una interfaz: son moldes de moldes. Instancia una de sus hijas.
  • Creer que use carga el archivo: solo crea un alias. Necesitas require o autoload.
  • Olvidar la barra inicial con clases globales dentro de un namespace: new DateTime() busca App\Modelos\DateTime. Escribe \DateTime o impórtala con use.

Resumen

ConceptoCódigo
Heredarclass Curso extends Actividad
Llamar al padreparent::__construct(...), parent::descripcion()
Visible para las hijasprotected
Prohibir herenciafinal class, final public function
Clase abstractaabstract class Tarifa { abstract public function precio(int $d): float; }
Interfazinterface Notificador { public function enviar(...): bool; }
Implementarclass NotificadorEmail implements Notificador, Countable
Traittrait ConFecha { ... } y dentro de la clase use ConFecha;
Espacio de nombresnamespace App\Modelos;
Importaruse App\Modelos\Socio; / use ... as Alias;
Nombre completoSocio::class
Carga automáticaspl_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

[PHP] ¿Qué muestra este código?
class A {
    public function __construct(protected string $n) { echo "A "; }
    public function hola() { return "A:$this->n"; }
}
class B extends A {
    public function __construct(string $n) { parent::__construct($n); echo "B "; }
    public function hola() { return "B+" . parent::hola(); }
}
echo (new B("z"))->hola();

[PHP] ¿Qué ocurre al hacer new de una clase abstracta o de una interfaz?

[PHP] ¿Cuándo es mejor una interfaz que una clase abstracta?

[PHP] ¿Qué hace use App\Modelos\Socio; al principio de un archivo?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.