POO en PHP: clases, interfaces y composición paso a paso
La programación orientada a objetos en PHP permite organizar aplicaciones alrededor de responsabilidades, objetos y contratos, en lugar de acumular funciones y variables sin una estructura clara. Aprender POO no significa crear una clase para todo: significa comprender cuándo una clase ayuda, cómo encapsular estado, cómo separar responsabilidades y cómo hacer colaborar diferentes componentes sin generar dependencias innecesarias.
¿Qué es la programación orientada a objetos en PHP?
La POO en PHP es una forma de organizar programas mediante clases y objetos que agrupan información y comportamiento relacionado.
Una clase define una estructura y un objeto representa una instancia concreta de esa estructura.
Pero la POO profesional también incluye conceptos como encapsulación, interfaces, abstracción, composición, dependencias y separación de responsabilidades.
Su objetivo no es escribir más código, sino conseguir que un sistema sea más fácil de entender, modificar, probar y mantener cuando crece.
La POO en PHP se entiende mejor como un conjunto de decisiones de diseño
Clases
Definen estructura y comportamiento.
Objetos
Representan instancias concretas de una clase.
Encapsulación
Controla qué detalles se exponen.
Interfaces
Definen contratos entre componentes.
Composición
Construye objetos mediante colaboración.
Dependencias
Conecta servicios de forma explícita.
Aprende funciones antes de intentar resolver todo con clases
Si todavía estás aprendiendo variables, condiciones, arrays y funciones, no necesitas convertir inmediatamente cada ejercicio en orientación a objetos.
Las funciones ya enseñan una idea fundamental: separar una tarea en una responsabilidad concreta.
Primero comprende entradas y salidas
Una función recibe datos, realiza una operación y puede devolver un resultado.
Esa misma idea aparecerá después dentro de métodos y objetos.
Domina primero los fundamentos de PHP
Variables, funciones, arrays, formularios y errores.
Una clase define qué datos y operaciones pertenecen a un concepto
Imagina una aplicación que administra productos.
Un producto puede tener nombre, precio y determinadas operaciones.
<?php
class Producto
{
public string $nombre;
public float $precio;
}
La clase describe qué propiedades puede tener un producto.
Pero todavía no tenemos un producto concreto
Para eso necesitamos crear un objeto.
Un objeto es una instancia concreta de una clase
<?php
$producto = new Producto();
$producto->nombre = 'Teclado';
$producto->precio = 19990;
Ahora $producto representa un objeto concreto.
Podrías crear otro objeto de la misma clase con valores diferentes.
<?php
$otroProducto = new Producto();
$otroProducto->nombre = 'Monitor';
$otroProducto->precio = 129990;
Ambos objetos utilizan la misma definición, pero representan productos distintos.
El constructor permite crear objetos en un estado válido desde el comienzo
En el ejemplo anterior, podríamos crear un producto vacío y olvidar asignarle un precio.
Podemos exigir determinados datos durante la construcción.
<?php
class Producto
{
public function __construct(
public string $nombre,
public float $precio
) {}
}
$producto = new Producto(
'Teclado',
19990
);
Ahora nombre y precio son obligatorios
El objeto no puede construirse correctamente sin proporcionar esos valores.
public, protected y private controlan qué partes del objeto pueden utilizarse desde fuera
Hacer todas las propiedades públicas permite modificarlas desde cualquier lugar.
Eso puede convertirse en un problema cuando necesitas proteger determinadas reglas.
public
Puede utilizarse desde fuera de la clase.
private
Solo puede accederse directamente desde la propia clase.
protected
También puede estar disponible dentro de clases derivadas.
<?php
class Producto
{
public function __construct(
private string $nombre,
private float $precio
) {}
public function obtenerNombre(): string
{
return $this->nombre;
}
public function obtenerPrecio(): float
{
return $this->precio;
}
}
Ahora el estado interno queda controlado por la clase.
Un objeto debería proteger sus propias reglas cuando sea razonable
Supongamos que el precio nunca debería ser negativo.
<?php
class Producto
{
private float $precio;
public function __construct(
private string $nombre,
float $precio
) {
$this->cambiarPrecio($precio);
}
public function cambiarPrecio(
float $precio
): void {
if ($precio < 0) {
throw new InvalidArgumentException(
'El precio no puede ser negativo.'
);
}
$this->precio = $precio;
}
public function obtenerPrecio(): float
{
return $this->precio;
}
}
La clase ahora protege una condición
No dependemos de recordar esa validación cada vez que modificamos un precio.
El objetivo es proteger invariantes y comportamiento, no añadir métodos de manera mecánica.
$this representa el objeto actual dentro de sus métodos
Cuando escribimos:
$this->precio
estamos accediendo a la propiedad precio del objeto que actualmente ejecuta el método.
Si existen veinte objetos Producto, cada uno mantiene su propio estado.
Una interfaz define qué debe poder hacer un componente sin decidir cómo lo hará
Imagina una aplicación que necesita enviar notificaciones.
Puede enviarlas por email, SMS o cualquier otro canal.
<?php
interface Notificador
{
public function enviar(
string $destinatario,
string $mensaje
): void;
}
La interfaz define un contrato.
Cualquier clase que implemente Notificador deberá proporcionar ese comportamiento.
Varias clases pueden cumplir el mismo contrato de maneras diferentes
<?php
final class NotificadorEmail
implements Notificador
{
public function enviar(
string $destinatario,
string $mensaje
): void {
// Enviar correo.
}
}
<?php
final class NotificadorSms
implements Notificador
{
public function enviar(
string $destinatario,
string $mensaje
): void {
// Enviar SMS.
}
}
El resto de la aplicación puede depender del contrato Notificador en lugar de una implementación concreta.
Un objeto puede recibir sus colaboradores en lugar de crearlos internamente
Supongamos que un servicio registra usuarios y después envía una notificación.
<?php
final class RegistroUsuario
{
public function __construct(
private Notificador $notificador
) {}
public function registrar(
string $email
): void {
// Crear usuario.
$this->notificador->enviar(
$email,
'Tu cuenta fue creada.'
);
}
}
RegistroUsuario no crea el notificador
Lo recibe desde el exterior.
Eso permite utilizar distintas implementaciones sin modificar la lógica de registro.
<?php
$notificador = new NotificadorEmail();
$registro = new RegistroUsuario(
$notificador
);
En su forma más simple, consiste simplemente en proporcionar a un objeto los colaboradores que necesita.
Composición significa construir comportamiento combinando objetos
En el ejemplo anterior, RegistroUsuario utiliza un Notificador.
No necesita heredar de Notificador.
Simplemente colabora con otro objeto.
“Tiene un” frente a “es un”
Un servicio de registro tiene un notificador.
No tiene sentido decir que un registro es un notificador.
Esta distinción ayuda mucho a decidir entre composición y herencia.
Herencia puede ser útil, pero no debería ser la primera respuesta para reutilizar código
<?php
class Usuario
{
public function __construct(
protected string $nombre
) {}
}
class Administrador extends Usuario
{
public function obtenerNombre(): string
{
return $this->nombre;
}
}
Administrador hereda comportamiento y estado de Usuario.
El problema aparece cuando la jerarquía crece demasiado
Si comienzas a crear muchas capas de herencia, las clases pueden quedar fuertemente acopladas.
Cambiar una clase base puede producir efectos inesperados en múltiples clases hijas.
Primero pregunta si realmente existe una relación conceptual de tipo “es un”.
Una clase abstracta puede compartir estructura y exigir comportamiento a sus clases hijas
<?php
abstract class MetodoPago
{
abstract public function pagar(
float $monto
): bool;
public function montoValido(
float $monto
): bool {
return $monto > 0;
}
}
Una clase abstracta puede proporcionar implementación compartida y también declarar métodos abstractos.
¿Interface o abstract?
Una interfaz suele expresar principalmente un contrato.
Una clase abstracta puede además compartir estado y comportamiento.
No son sustitutos perfectos: resuelven problemas diferentes.
final puede expresar que una clase o método no está diseñado para extenderse
<?php
final class CalculadoraImpuestos
{
public function calcular(
float $neto
): float {
return $neto * 0.19;
}
}
Esto comunica una decisión de diseño: la clase no está pensada como punto de extensión mediante herencia.
Los miembros static pertenecen a la clase, no a una instancia concreta
<?php
final class Calculadora
{
public static function sumar(
float $a,
float $b
): float {
return $a + $b;
}
}
$total = Calculadora::sumar(
10,
20
);
No necesitamos crear un objeto Calculadora para ejecutar ese método.
Pero no conviertas todo en static
El uso excesivo puede dificultar sustituir dependencias, mantener estado y probar componentes de forma aislada.
Los traits permiten reutilizar implementación entre clases sin crear una jerarquía de herencia
<?php
trait RegistraActividad
{
public function registrar(
string $mensaje
): void {
error_log($mensaje);
}
}
class PedidoService
{
use RegistraActividad;
}
Un trait puede resultar útil para compartir determinado comportamiento.
Pero también puede ocultar dependencias si se utiliza indiscriminadamente.
También importa que las dependencias y responsabilidades resulten claras.
Una clase enorme que hace todo sigue siendo una mala organización
Introducir POO no soluciona automáticamente los problemas de arquitectura.
Puedes tener una clase de cinco mil líneas que:
Consulta la base de datos.
Envía emails.
Genera HTML.
Procesa pagos.
Valida formularios.
Gestiona archivos.
Técnicamente sería POO, pero no necesariamente buen diseño.
Divide por responsabilidades
Cada componente debería tener una razón clara para cambiar.
Separar acceso a datos y lógica con PDO
Podemos aprovechar lo aprendido en nuestra guía de PHP con MySQL para crear una estructura un poco más limpia.
Repository para usuarios
<?php
final class UsuarioRepository
{
public function __construct(
private PDO $pdo
) {}
public function buscarPorEmail(
string $email
): ?array {
$sql = "
SELECT
id,
nombre,
email,
activo
FROM usuarios
WHERE email = :email
LIMIT 1
";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
'email' => $email
]);
$usuario = $stmt->fetch();
return $usuario ?: null;
}
}
Servicio de usuarios
<?php
final class UsuarioService
{
public function __construct(
private UsuarioRepository $usuarios
) {}
public function existeEmail(
string $email
): bool {
return $this->usuarios
->buscarPorEmail($email) !== null;
}
}
El servicio ya no necesita conocer detalles de SQL.
Puede concentrarse en reglas relacionadas con usuarios.
POO cobra mucho más sentido cuando comienzas a construir sistemas reales
Combínala con PDO, SQL y separación de responsabilidades.
Una interfaz puede separar aún más el servicio de la implementación de datos
<?php
interface Usuarios
{
public function buscarPorEmail(
string $email
): ?array;
}
Después, una implementación puede utilizar PDO.
<?php
final class UsuariosPdo
implements Usuarios
{
public function __construct(
private PDO $pdo
) {}
public function buscarPorEmail(
string $email
): ?array {
// Consulta PDO.
return null;
}
}
Ahora el servicio puede depender de Usuarios y no necesariamente de PDO.
Las dependencias explícitas facilitan probar componentes de forma aislada
Si una clase crea dentro de sí misma una conexión, un cliente HTTP, un sistema de email y otras dependencias, probarla se vuelve más difícil.
Cuando recibe sus colaboradores desde fuera, puedes sustituirlos durante una prueba.
Puedes crear una implementación de prueba
<?php
final class UsuariosMemoria
implements Usuarios
{
public function buscarPorEmail(
string $email
): ?array {
if ($email === 'test@ciborg.cl') {
return [
'id' => 1,
'email' => $email
];
}
return null;
}
}
El servicio puede probarse sin consultar una base de datos real.
La POO también puede ayudarte a estructurar plugins WordPress grandes
Un plugin de unas pocas líneas puede funcionar perfectamente con funciones.
Pero cuando incorpora administración, WooCommerce, endpoints, base de datos, cron, emails y servicios externos, separar componentes comienza a ser útil.
Una clase para administración
Puede registrar menús, formularios y configuración.
Otra para APIs
Puede gestionar endpoints e integraciones.
Otra para datos
Puede concentrar operaciones específicas de persistencia.
El objetivo no es convertir cada hook en una clase, sino evitar un plugin imposible de mantener.
Aplica PHP moderno dentro del ecosistema WordPress
Hooks, plugins, seguridad, REST API y arquitectura.
Las interfaces son especialmente útiles alrededor de servicios externos
Una aplicación puede depender de un proveedor de pagos, correo, almacenamiento o inteligencia artificial.
Si todo el sistema depende directamente de una implementación, cambiar de proveedor puede ser costoso.
Define el comportamiento que necesitas
Por ejemplo:
<?php
interface ProcesadorPago
{
public function cobrar(
int $monto,
string $referencia
): ResultadoPago;
}
Luego puedes adaptar diferentes proveedores a ese contrato.
Explorar APIs e integraciones →POO mal aplicada puede hacer un proyecto más complejo, no menos
En qué orden aprender POO en PHP
Funciones
Parámetros, retorno y responsabilidades.
Clases y objetos
Estructura e instancias.
Constructores
Inicializa objetos correctamente.
Visibilidad
public, protected y private.
Encapsulación
Protege estado y reglas.
Interfaces
Define contratos.
Composición
Conecta objetos pequeños.
Herencia
Úsala cuando la relación lo justifique.
Dependencias
Haz explícitos los colaboradores.
Proyecto real
Aplica POO con base de datos o APIs.
Aprender POO en PHP significa aprender a diseñar responsabilidades, no solamente aprender la palabra class
Empieza por clases, objetos y constructores.
Aprende visibilidad para controlar el estado interno.
Utiliza encapsulación para proteger reglas importantes.
Aprende interfaces para expresar contratos claros.
Prefiere composición cuando varios objetos simplemente necesitan colaborar.
Utiliza herencia solamente cuando exista una relación conceptual adecuada.
Haz explícitas las dependencias para facilitar mantenimiento y testing.
Y evita añadir abstracción solamente porque “parece más profesional”: el objetivo de la POO sigue siendo producir software comprensible y mantenible.
Continúa profundizando en PHP
Preguntas sobre POO en PHP
POO significa programación orientada a objetos. En PHP permite organizar aplicaciones mediante clases y objetos que agrupan estado y comportamiento, además de utilizar conceptos como encapsulación, interfaces, composición y herencia.
Una clase define una estructura y comportamiento. Un objeto es una instancia concreta creada a partir de esa clase y mantiene su propio estado.
Encapsulación consiste en controlar el acceso al estado interno de un objeto y exponer únicamente las operaciones necesarias. PHP dispone de visibilidad public, protected y private para ayudar a expresarlo.
Una interfaz define un contrato que las clases implementadoras deben cumplir. Permite que distintos componentes ofrezcan el mismo comportamiento mediante implementaciones diferentes.
Depende del problema. La composición suele ser adecuada cuando un objeto necesita colaborar con otros componentes. La herencia tiene más sentido cuando existe una relación conceptual clara de tipo “es un” y la jerarquía resulta estable.
Es una técnica donde un objeto recibe desde el exterior los servicios o componentes que necesita, en lugar de crearlos directamente dentro de sí mismo. Esto puede reducir acoplamiento y facilitar testing.
No para cada personalización. Un plugin pequeño puede utilizar funciones. Sin embargo, POO puede ayudar a organizar plugins mayores que incluyen administración, APIs, WooCommerce, datos e integraciones.
Conviene aprenderla después de comprender variables, arrays, condiciones, funciones, parámetros, valores de retorno y tipos. Después puedes aplicar POO dentro de pequeños proyectos reales.
La POO empieza a tener sentido cuando tus programas dejan de ser ejercicios aislados
Aprende clases, encapsulación, interfaces y composición. Después aplícalas junto con PDO, APIs o WordPress dentro de proyectos que necesiten una estructura mantenible.
Continuar con PHP y MySQL →