🔎 Buscar

🏛️ 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.

Wiki / Apuntes📖 Contenido

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

💡 ::class devuelve 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 de use.

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) 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 Persistible funciona 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 insteadof o as. Ú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/int y se reconstruyen con from() (lanza ValueError) o tryFrom() (devuelve null). 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\UsuarioRepositorio se resuelve a src/Repositorios/UsuarioRepositorio.php. Regla: un archivo por clase, y la ruta coincide con el namespace. Con use 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->nomrbe devolverá null en 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

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.