Skip to content

POO: clases y objetos ​

La Programación Orientada a Objetos (POO) es la forma en que se escribe el PHP moderno: Laravel, Symfony, WordPress (en buena parte) y casi cualquier librería de Composer están hechos con clases. En esta lección aprenderás a crear tus propias clases con todas las herramientas de PHP 8.3: promoción de constructor, readonly, tipos, miembros estáticos y enums.

Concepto ​

  • Una clase es un molde o plano. Describe qué datos tiene algo (propiedades) y qué puede hacer (métodos).
  • Un objeto es una cosa concreta construida con ese molde. Se llama también instancia.

Analogía: la clase Auto es el plano de la fábrica; tu auto rojo con placas ABC-123 y el auto azul de tu vecino son dos objetos distintos creados con el mismo plano.

¿Por qué no seguir con arrays y funciones? Porque las clases:

  • Agrupan datos y comportamiento relacionados en un solo lugar.
  • Protegen los datos (nadie puede poner un precio negativo si la clase lo impide).
  • Se reutilizan y se prueban más fácil.
  • Son la base de frameworks como Laravel.

Tu primera clase ​

php
<?php
declare(strict_types=1);

class Producto
{
    // Propiedades: datos que tendrá cada objeto
    public string $nombre;
    public float $precio;
    public int $stock = 0; // valor por defecto

    // Método: una función que pertenece a la clase
    public function precioConIva(): float
    {
        // $this se refiere al objeto actual
        return $this->precio * 1.16;
    }
}

// Crear objetos con "new"
$cafe = new Producto();
$cafe->nombre = 'Café de Chiapas';
$cafe->precio = 180;

$te = new Producto();
$te->nombre = 'Té verde';
$te->precio = 95;

echo $cafe->nombre . ': $' . $cafe->precioConIva() . PHP_EOL; // Café de Chiapas: $208.8
echo $te->nombre . ': $' . $te->precioConIva() . PHP_EOL;     // Té verde: $110.2

Se usa -> para acceder a propiedades y métodos de un objeto.

El constructor ​

El constructor (__construct) se ejecuta automáticamente al hacer new. Sirve para que el objeto nazca con todos sus datos obligatorios.

php
<?php
class Producto
{
    public string $nombre;
    public float $precio;

    public function __construct(string $nombre, float $precio)
    {
        $this->nombre = $nombre;
        $this->precio = $precio;
    }
}

$cafe = new Producto('Café de Chiapas', 180);

Promoción de propiedades en el constructor (PHP 8) ​

Lo anterior es tan común que PHP 8 permite declarar y asignar las propiedades directamente en los parámetros:

php
<?php
class Producto
{
    // Esto declara las propiedades Y las asigna. Mismo resultado, mucho menos código.
    public function __construct(
        public string $nombre,
        public float $precio,
        public int $stock = 0,
    ) {}
}

$cafe = new Producto('Café de Chiapas', 180, stock: 25); // argumentos con nombre
echo $cafe->stock; // 25

Visibilidad: public, protected, private ​

Modificador¿Quién puede acceder?
publicCualquiera, desde fuera de la clase
protectedLa clase y sus clases hijas (ver herencia)
privateSólo la propia clase

La encapsulación consiste en ocultar los datos internos y ofrecer métodos para manipularlos de forma controlada:

php
<?php
declare(strict_types=1);

class CuentaBancaria
{
    private float $saldo = 0; // nadie de fuera puede modificarlo directamente

    public function __construct(public readonly string $titular) {}

    public function depositar(float $monto): void
    {
        if ($monto <= 0) {
            throw new InvalidArgumentException('El depósito debe ser positivo.');
        }
        $this->saldo += $monto;
    }

    public function retirar(float $monto): void
    {
        if ($monto > $this->saldo) {
            throw new RuntimeException('Saldo insuficiente.');
        }
        $this->saldo -= $monto;
    }

    // "Getter": permite leer pero no escribir
    public function getSaldo(): float
    {
        return $this->saldo;
    }
}

$cuenta = new CuentaBancaria('Ana López');
$cuenta->depositar(1000);
$cuenta->retirar(300);
echo $cuenta->getSaldo(); // 700

// $cuenta->saldo = 1000000; // Error: Cannot access private property

Buena práctica

Empieza con todo private y abre sólo lo necesario. Es más fácil hacer pública una propiedad después que deshacer el daño de que todo el código dependa de ella.

Propiedades y clases readonly ​

Una propiedad readonly sólo se puede asignar una vez (normalmente en el constructor). Ideal para datos que no deben cambiar: un ID, un email de registro, un valor monetario.

php
<?php
class Usuario
{
    public function __construct(
        public readonly int $id,
        public readonly string $email,
        public string $nombre, // éste sí puede cambiar
    ) {}
}

$u = new Usuario(1, 'ana@correo.com', 'Ana');
$u->nombre = 'Ana María'; // OK
// $u->email = 'otro@correo.com'; // Error: Cannot modify readonly property

Desde PHP 8.2 puedes declarar toda la clase como readonly:

php
<?php
// Todas las propiedades son readonly automáticamente
readonly class Dinero
{
    public function __construct(
        public int $centavos,   // guardar dinero en centavos evita errores de redondeo
        public string $moneda = 'MXN',
    ) {}

    // Como no se puede modificar, las operaciones devuelven un objeto NUEVO
    public function sumar(Dinero $otro): static
    {
        if ($otro->moneda !== $this->moneda) {
            throw new InvalidArgumentException('Monedas distintas.');
        }
        return new static($this->centavos + $otro->centavos, $this->moneda);
    }

    public function formatear(): string
    {
        return '$' . number_format($this->centavos / 100, 2) . ' ' . $this->moneda;
    }
}

$total = (new Dinero(15050))->sumar(new Dinero(4950));
echo $total->formatear(); // $200.00 MXN

Miembros estáticos ​

Lo static pertenece a la clase, no a cada objeto. Se accede con ::.

php
<?php
class Contador
{
    private static int $instancias = 0; // compartido por todos los objetos

    public const VERSION = '1.0'; // constante de clase

    public function __construct()
    {
        self::$instancias++; // self:: se refiere a la propia clase
    }

    public static function total(): int
    {
        return self::$instancias;
    }
}

new Contador();
new Contador();
echo Contador::total();   // 2
echo Contador::VERSION;   // 1.0

Métodos de fábrica (named constructors) ​

Un uso muy común de static es crear objetos de distintas formas:

php
<?php
class Usuario
{
    private function __construct( // constructor privado: obliga a usar las fábricas
        public readonly string $nombre,
        public readonly string $email,
    ) {}

    public static function desdeArray(array $fila): self
    {
        return new self($fila['nombre'], $fila['email']);
    }

    public static function invitado(): self
    {
        return new self('Invitado', 'invitado@ejemplo.com');
    }
}

$u1 = Usuario::desdeArray(['nombre' => 'Luis', 'email' => 'luis@correo.com']);
$u2 = Usuario::invitado();

Error común

Usar $this dentro de un método static. Un método estático no tiene objeto, así que $this no existe. Usa self:: o static::.

Enums (PHP 8.1+) ​

Un enum define un conjunto cerrado de valores posibles. Perfecto para estados de pedidos, roles de usuario, métodos de pago...

php
<?php
declare(strict_types=1);

// Enum "respaldado" (backed) por string: cada caso tiene un valor para guardar en BD
enum EstadoPedido: string
{
    case Pendiente = 'pendiente';
    case Pagado    = 'pagado';
    case Enviado   = 'enviado';
    case Cancelado = 'cancelado';

    // Los enums pueden tener métodos
    public function etiqueta(): string
    {
        return match ($this) {
            self::Pendiente => 'Pendiente de pago',
            self::Pagado    => 'Pagado',
            self::Enviado   => 'En camino',
            self::Cancelado => 'Cancelado',
        };
    }

    public function color(): string
    {
        return match ($this) {
            self::Pendiente => 'orange',
            self::Pagado, self::Enviado => 'green',
            self::Cancelado => 'red',
        };
    }
}

class Pedido
{
    public function __construct(
        public readonly int $id,
        public EstadoPedido $estado = EstadoPedido::Pendiente,
    ) {}

    public function marcarPagado(): void
    {
        if ($this->estado !== EstadoPedido::Pendiente) {
            throw new LogicException('Sólo se pueden pagar pedidos pendientes.');
        }
        $this->estado = EstadoPedido::Pagado;
    }
}

$pedido = new Pedido(1001);
$pedido->marcarPagado();
echo $pedido->estado->etiqueta(); // Pagado
echo $pedido->estado->value;      // pagado  (lo que guardarías en la BD)

// Convertir desde la BD
$estado = EstadoPedido::from('enviado');        // EstadoPedido::Enviado
$otro   = EstadoPedido::tryFrom('inexistente'); // null (from() lanzaría ValueError)

// Listar todos los casos (útil para un <select>)
foreach (EstadoPedido::cases() as $caso) {
    echo $caso->value . ' => ' . $caso->etiqueta() . PHP_EOL;
}

Buena práctica

Antes de los enums se usaban strings sueltos ('pagado', 'Pagado', 'pagada'...) y cada error de tipeo era un bug. Con un enum, el editor te autocompleta y PHP rechaza cualquier valor inválido.

Métodos mágicos útiles ​

php
<?php
class Etiqueta
{
    public function __construct(private string $texto) {}

    // Se llama al usar el objeto como string (echo, concatenación)
    public function __toString(): string
    {
        return strtoupper($this->texto);
    }
}

echo new Etiqueta('oferta'); // OFERTA

Otros: __get, __set (acceso a propiedades inexistentes), __call (métodos inexistentes), __invoke (usar el objeto como función). Úsalos con moderación: hacen el código menos predecible.

Objetos: se pasan por "manejador" ​

php
<?php
$a = new Producto('Café', 100);
$b = $a;          // $b apunta al MISMO objeto
$b->precio = 200;
echo $a->precio;  // 200  ¡cambió también!

$c = clone $a;    // clone crea una copia independiente
$c->precio = 50;
echo $a->precio;  // 200

Caso práctico: un carrito de compras ​

php
<?php
declare(strict_types=1);

final class Producto
{
    public function __construct(
        public readonly string $sku,
        public readonly string $nombre,
        public readonly float $precio,
    ) {}
}

final class Carrito
{
    /** @var array<string, array{producto: Producto, cantidad: int}> */
    private array $items = [];

    public function agregar(Producto $producto, int $cantidad = 1): self
    {
        if ($cantidad < 1) {
            throw new InvalidArgumentException('Cantidad inválida.');
        }
        $sku = $producto->sku;
        $actual = $this->items[$sku]['cantidad'] ?? 0;
        $this->items[$sku] = ['producto' => $producto, 'cantidad' => $actual + $cantidad];
        return $this; // devolver $this permite encadenar llamadas
    }

    public function quitar(string $sku): self
    {
        unset($this->items[$sku]);
        return $this;
    }

    public function total(): float
    {
        return array_reduce(
            $this->items,
            fn (float $suma, array $item) => $suma + $item['producto']->precio * $item['cantidad'],
            0.0
        );
    }

    public function cantidadArticulos(): int
    {
        return array_sum(array_column($this->items, 'cantidad'));
    }
}

$carrito = (new Carrito())
    ->agregar(new Producto('CAF-01', 'Café', 180), 2)
    ->agregar(new Producto('TAZ-01', 'Taza', 120));

echo $carrito->cantidadArticulos() . ' artículos, total $' . $carrito->total(); // 3 artículos, total $480

final impide que otra clase herede de ésta. Es una buena opción por defecto para clases que no fueron diseñadas para extenderse.

Ejercicios ​

  1. Crea una clase Rectangulo con propiedades readonly ancho y alto (promoción en el constructor), y métodos area(), perimetro() y esCuadrado().
  2. Crea un enum Rol: string con los casos Admin, Editor y Lector, y un método puedeEditar(): bool. Luego una clase Usuario que reciba un Rol.
  3. Crea una clase Temperatura con un constructor privado y dos fábricas estáticas: desdeCelsius(float) y desdeFahrenheit(float). Añade métodos celsius() y fahrenheit().
Ver solución
php
<?php
declare(strict_types=1);

// 1
final class Rectangulo
{
    public function __construct(
        public readonly float $ancho,
        public readonly float $alto,
    ) {
        if ($ancho <= 0 || $alto <= 0) {
            throw new InvalidArgumentException('Las medidas deben ser positivas.');
        }
    }

    public function area(): float { return $this->ancho * $this->alto; }
    public function perimetro(): float { return 2 * ($this->ancho + $this->alto); }
    public function esCuadrado(): bool { return $this->ancho === $this->alto; }
}

$r = new Rectangulo(4, 4);
echo $r->area(), ' ', $r->perimetro(), ' ', var_export($r->esCuadrado(), true), PHP_EOL; // 16 16 true

// 2
enum Rol: string
{
    case Admin = 'admin';
    case Editor = 'editor';
    case Lector = 'lector';

    public function puedeEditar(): bool
    {
        return match ($this) {
            self::Admin, self::Editor => true,
            self::Lector => false,
        };
    }
}

final class Usuario
{
    public function __construct(
        public readonly string $nombre,
        public Rol $rol = Rol::Lector,
    ) {}
}

$u = new Usuario('Carla', Rol::from('editor'));
echo $u->rol->puedeEditar() ? 'Puede editar' : 'Sólo lectura', PHP_EOL;

// 3
final class Temperatura
{
    private function __construct(private float $celsius) {}

    public static function desdeCelsius(float $c): self { return new self($c); }
    public static function desdeFahrenheit(float $f): self { return new self(($f - 32) * 5 / 9); }

    public function celsius(): float { return round($this->celsius, 2); }
    public function fahrenheit(): float { return round($this->celsius * 9 / 5 + 32, 2); }
}

echo Temperatura::desdeFahrenheit(98.6)->celsius(), PHP_EOL; // 37
echo Temperatura::desdeCelsius(100)->fahrenheit(), PHP_EOL;  // 212

Resumen ​

  • Una clase es un molde; un objeto es una instancia creada con new.
  • Usa la promoción de propiedades en el constructor para escribir menos código.
  • Encapsula: propiedades private y métodos públicos que validan.
  • readonly (propiedad o clase completa) evita modificaciones accidentales.
  • static pertenece a la clase; úsalo para constantes, contadores y métodos de fábrica.
  • Los enums representan conjuntos cerrados de valores y pueden tener métodos.
  • Los objetos se comparten al asignarlos; usa clone para copiarlos.

Academia Web — aprende, copia y construye.