Tema
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.2Se 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; // 25Visibilidad: public, protected, private
| Modificador | ¿Quién puede acceder? |
|---|---|
public | Cualquiera, desde fuera de la clase |
protected | La clase y sus clases hijas (ver herencia) |
private | Só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 propertyBuena 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 propertyDesde 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 MXNMiembros 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.0Mé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'); // OFERTAOtros: __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; // 200Caso 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 $480final impide que otra clase herede de ésta. Es una buena opción por defecto para clases que no fueron diseñadas para extenderse.
Ejercicios
- Crea una clase
Rectangulocon propiedadesreadonlyanchoyalto(promoción en el constructor), y métodosarea(),perimetro()yesCuadrado(). - Crea un enum
Rol: stringcon los casosAdmin,EditoryLector, y un métodopuedeEditar(): bool. Luego una claseUsuarioque reciba unRol. - Crea una clase
Temperaturacon un constructor privado y dos fábricas estáticas:desdeCelsius(float)ydesdeFahrenheit(float). Añade métodoscelsius()yfahrenheit().
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; // 212Resumen
- 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
privatey métodos públicos que validan. readonly(propiedad o clase completa) evita modificaciones accidentales.staticpertenece 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
clonepara copiarlos.