🏛️ Programación orientada a objetos en PHP
Clases, propiedades, métodos, visibilidad, herencia, interfaces, traits, enums, namespaces, autoloading PSR-4, inyección de dependencias, patrones de diseño y magic methods con ejemplos completos.
Programación orientada a objetos en PHP
PHP es un lenguaje híbrido: comenzó como procedural y maduró hacia la orientación a objetos. Los frameworks modernos (Laravel, Symfony) son POO pura. Dominar clases, interfaces, composición y patrones separa a quien “escribe PHP” de quien “construye software” con PHP. Este artículo recorre el modelo de objetos de PHP 8 con ejemplos de dominio reales.
Clases, propiedades y métodos
Una clase es una plantilla: define estado (propiedades) y comportamiento (métodos). Los objetos son instancias creadas con new.
<?php
class Producto
{
public function __construct(
public string $nombre,
public float $precio,
private int $stock,
) {}
public function stockDisponible(): int { return $this->stock; }
}
$portatil = new Producto('Portátil', 899.00, 5);
echo $portatil->nombre; // Portátil
echo $portatil->stockDisponible(); // 5
Visibilidad
| Modificador | Accesible desde |
|---|---|
public |
Cualquier parte del código |
protected |
La propia clase y sus subclases |
private |
Solo la propia clase |
<?php
class CuentaBancaria
{
private float $saldo = 0;
public function depositar(float $cantidad): void
{
if ($cantidad <= 0) throw new InvalidArgumentException('Cantidad inválida');
$this->saldo += $cantidad;
}
public function saldo(): float { return $this->saldo; }
}
💡 Regla de oro: encapsula el estado, expón el comportamiento. Si una propiedad se lee y se escribe desde fuera sin control, no tienes una propiedad, tienes una caja de pandora. Los setters te permiten validar antes de mutar.
Constructor y promoted properties
PHP 8 permite promover parámetros del constructor directamente a propiedades:
<?php
// PHP 8: promoted properties — declaran, tipan y asignan en el constructor
class Usuario
{
public function __construct(
private string $nombre,
private string $email,
private bool $activo = true,
) {}
public function nombre(): string { return $this->nombre; }
public function estaActivo(): bool { return $this->activo; }
}
Propiedades readonly
Una propiedad readonly (PHP 8.1) solo se escribe una vez, normalmente en el constructor. PHP 8.2 añadió clases readonly completas. Ideal para value objects y DTOs inmutables.
<?php
readonly class Direccion
{
public function __construct(
public string $calle,
public string $ciudad,
) {}
}
$dir = new Direccion('Gran Vía 1', 'Madrid');
// $dir->ciudad = 'Barcelona'; // ❌ Error: no se puede modificar readonly
Constantes de clase y ::class
<?php
class Configuracion
{
public const VERSION = '2.1.0';
}
echo Configuracion::VERSION . ' / ' . Configuracion::class; // 2.1.0 / Configuracion
💡
::classdevuelve el nombre completamente cualificado (con namespace) de la clase. Es la forma robusta de pasar nombres de clase a factories y contenedores, y se resuelve en tiempo de compilación, sin necesidad deuse.
Static
Los miembros static pertenecen a la clase, no a la instancia.
<?php
class Formateador
{
public static function moneda(float $valor): string
{
return number_format($valor, 2, ',', '.') . ' €';
}
}
echo Formateador::moneda(1234.5); // 1.234,50 €
⚠️ El estado estático (
public static $x) es global y difícil de testear: dos tests pueden pisarse entre sí. Úsalo para constantes y métodos puros; para estado mutable, prefiere inyección de dependencias.
Herencia
<?php
abstract class Vehiculo
{
public function __construct(
protected string $marca,
protected int $ruedas,
) {}
public function ruedas(): int { return $this->ruedas; }
abstract public function arrancar(): string; // obliga a las subclases
}
class Coche extends Vehiculo
{
public function __construct(string $marca) { parent::__construct($marca, 4); }
public function arrancar(): string { return "El $this->marca acelera"; }
}
$coche = new Coche('Seat');
echo $coche->arrancar() . ' (' . $coche->ruedas() . " ruedas)\n";
💡
parent::invoca el método de la clase padre. La herencia funciona bien para relaciones “es-un”; cuando solo quieres compartir código, prefiere composición o traits.
Abstract classes vs interfaces
| Clase abstracta | Interfaz | |
|---|---|---|
| Instanciable | No | No |
| Estado (propiedades) | Sí | No |
| Implementación concreta | Sí (parcial) | No |
| Herencia múltiple | No (una sola) | Sí (varias) |
| Cuándo usarla | Familia con estado compartido | Contrato / capacidad |
<?php
interface Persistible
{
public function guardar(): void;
public function eliminar(): void;
}
abstract class Entidad implements Persistible
{
public function __construct(protected DateTimeImmutable $creadoEl) {}
public function fechaCreacion(): DateTimeImmutable { return $this->creadoEl; }
}
class Pedido extends Entidad
{
public function guardar(): void { /* ... */ }
public function eliminar(): void { /* ... */ }
}
💡 Programar contra interfaces (el tipo más abstracto posible) es la base del acoplamiento débil: el código que consume
Persistiblefunciona con cualquier implementación sin cambiar nada.
Traits
Un trait es un bloque de código reutilizable que se “inserta” en una clase. Resuelve la falta de herencia múltiple sin introducir herencia real.
<?php
trait Loggeable
{
protected function log(string $mensaje): void { echo '[' . date('Y-m-d H:i:s') . "] $mensaje\n"; }
}
trait TieneTimestamps
{
public DateTimeImmutable $creadoEl;
public function marcarCreado(): void { $this->creadoEl = new DateTimeImmutable(); }
}
class Pedido
{
use Loggeable;
use TieneTimestamps;
public function confirmar(): void
{
$this->marcarCreado();
$this->log('Pedido confirmado');
}
}
⚠️ Si dos traits declaran el mismo método hay conflicto; se resuelve con
insteadofoas. Úsalos para comportamiento transversal (logging, timestamps), no para modelar el dominio.
Enumerations
Los enums (PHP 8.1) son tipos de primera clase con casos, métodos y valores asociados.
<?php
enum EstadoPedido
{
case Pendiente;
case Pagado;
case Enviado;
public function esFinal(): bool { return $this === self::Enviado; }
}
enum MetodoPago: string // backed enum: asociado a un valor de BD
{
case Tarjeta = 'card';
case PayPal = 'paypal';
public function etiqueta(): string
{
return match ($this) {
self::Tarjeta => 'Tarjeta de crédito',
self::PayPal => 'PayPal',
};
}
}
$metodo = MetodoPago::from('card'); // MetodoPago::Tarjeta
echo $metodo->value; // 'card'
echo $metodo->etiqueta(); // Tarjeta de crédito
💡 Los enums backed se guardan en BD como
string/inty se reconstruyen confrom()(lanzaValueError) otryFrom()(devuelvenull). Son la alternativa moderna a “constantes de estado”.
Namespaces y autoloading PSR-4
Los namespaces evitan colisiones y organizan el código. El autoloading PSR-4 de Composer resuelve cada clase a un archivo sin require manuales.
<?php
// src/Repositorios/UsuarioRepositorio.php
namespace App\Repositorios;
use App\Dominio\Usuario;
class UsuarioRepositorio
{
public function porId(int $id): ?Usuario
{
return new Usuario('Ana');
}
}
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
composer dump-autoload
💡 En PSR-4, el namespace
App\Repositorios\UsuarioRepositoriose resuelve asrc/Repositorios/UsuarioRepositorio.php. Regla: un archivo por clase, y la ruta coincide con el namespace. Conuse App\Dominio\Usuario;importas la clase sin escribir el FQCN completo.
Inyección de dependencias y Service Container
La inyección de dependencias (DI) pasa las dependencias por el constructor en lugar de crearlas dentro. El contenedor de servicios resuelve automáticamente ese grafo de dependencias (es el corazón de Laravel y Symfony).
<?php
interface Mailer
{
public function enviar(string $a, string $asunto, string $cuerpo): void;
}
class NotificadorDeBienvenida
{
public function __construct(
private UsuarioRepositorio $usuarios,
private Mailer $mailer,
) {}
public function notificar(int $usuarioId): void
{
$usuario = $this->usuarios->porId($usuarioId);
$this->mailer->enviar($usuario->email, 'Bienvenido', 'Gracias por registrarte');
}
}
// Contenedor mínimo con autowiring por reflexión
$container = new class {
private array $resoluciones = [];
public function registrar(string $clase, callable $fabricante): void { $this->resoluciones[$clase] = $fabricante; }
public function obtener(string $clase): mixed
{
return isset($this->resoluciones[$clase])
? ($this->resoluciones[$clase])($this)
: new $clase();
}
};
$container->registrar(Mailer::class, fn($c) => new SmtpMailer());
$notificador = $container->obtener(NotificadorDeBienvenida::class);
$notificador->notificar(1);
💡 Beneficios: el código no depende de implementaciones concretas, las dependencias se declaran explícitamente y en los tests se inyectan dobles (mocks/fakes) con facilidad.
Patrones de diseño esenciales
Repository y Factory
<?php
// Repository: separa el acceso a datos de la lógica de negocio
interface PedidoRepositorio
{
public function buscar(int $id): ?Pedido;
public function guardar(Pedido $pedido): void;
}
class PedidoRepositorioMysql implements PedidoRepositorio { /* ... */ }
// Factory: centraliza la creación con lógica
class PedidoFactory
{
public static function crear(array $lineas): Pedido
{
$pedido = new Pedido(new DateTimeImmutable(), MetodoPago::Tarjeta);
foreach ($lineas as $linea) $pedido->agregarLinea($linea);
return $pedido;
}
}
Strategy
Intercambia algoritmos en runtime mediante una interfaz común.
<?php
interface Descuento
{
public function aplicar(float $total): float;
}
class SinDescuento implements Descuento
{
public function aplicar(float $total): float { return $total; }
}
class DescuentoPorcentaje implements Descuento
{
public function __construct(private float $porcentaje) {}
public function aplicar(float $total): float
{
return $total * (1 - $this->porcentaje / 100);
}
}
class CalculadoraTotal
{
public function __construct(private Descuento $descuento) {}
public function calcular(float $total): float
{
return $this->descuento->aplicar($total);
}
}
echo (new CalculadoraTotal(new DescuentoPorcentaje(10)))->calcular(100); // 90
Singleton (y por qué evitarlo)
<?php
class Conexion
{
private static ?Conexion $instancia = null;
private function __construct() {}
public static function obtener(): self { return self::$instancia ??= new self(); }
}
⚠️ El Singleton introduce estado global: oculta dependencias, complica los tests (no puedes aislar una instancia) y viola el principio de responsabilidad única. La alternativa moderna: un Service Container registra la instancia como singleton (
shared) y la inyecta. Igual de eficiente, mucho más testeable.
Magic methods
Los magic methods (prefijados con __) se invocan automáticamente ante operaciones concretas.
| Método | Cuándo se dispara |
|---|---|
__construct() |
Al crear con new |
__invoke() |
Al usar el objeto como función |
__get() / __set() |
Acceso a propiedades inaccesibles |
__call() |
Llamada a métodos inaccesibles |
__toString() |
Conversión a string (echo $obj) |
<?php
class Caja
{
private array $datos = [];
public function __construct(array $datos = []) { $this->datos = $datos; }
public function __invoke(mixed $valor): mixed { return $this->datos[$valor] ?? null; }
public function __get(string $clave): mixed { return $this->datos[$clave] ?? null; }
public function __set(string $clave, mixed $valor): void { $this->datos[$clave] = $valor; }
public function __toString(): string
{
return json_encode($this->datos, JSON_UNESCAPED_UNICODE);
}
}
$caja = new Caja(['nombre' => 'Ana']);
echo $caja->nombre; // Ana (via __get)
$caja->ciudad = 'Madrid'; // via __set
echo $caja('nombre'); // Ana (via __invoke)
echo $caja; // {"nombre":"Ana","ciudad":"Madrid"}
⚠️ Los magic methods enmascaran errores tipográficos:
$obj->nomrbedevolveránullen vez de lanzar error. Úsalos con moderación y documenta el contrato.
Cheatsheet
| Concepto | Sintaxis |
|---|---|
| Clase e instancia | class X {}, new X() |
| Promoted properties | public function __construct(private string $a) {} |
| Inmutabilidad | public readonly string $x |
| Constantes | public const V = 1, X::class |
| Estáticos | public static function m(): T |
| Herencia | class B extends A, abstract, parent:: |
| Contratos | interface, implements A, B |
| Reutilización | trait T, use T; |
| Enums | enum E: string { case A = 'a'; } |
| Namespaces | namespace App\X;, use App\X\Y; |
| Autoload | PSR-4 con Composer |
| DI | constructor + contenedor |
| Magic | __construct, __invoke, __toString, … |
Para profundizar
- PHP Manual — Classes and Objects: la referencia oficial completa de POO en PHP.
- PHP Manual — Properties: visibilidad, readonly y typed properties.
- PHP Manual — Enumeration: enums puros y backed en profundidad.
- PHP The Right Way — Design Patterns: patrones esenciales con ejemplos de código.
- Composer — Autoloading (PSR-4): cómo configurar el autoloader por namespace.
- Ruta completa: Backend con PHP.