Clases y objetos en PHP
En esta lección
- Clases y objetos
- Métodos y $this
- El constructor
- Visibilidad: public, private y protected
- Propiedades readonly
- Los objetos se comparten, no se copian
- static y constantes de clase
- Métodos mágicos y encadenamiento
- Enums: valores de una lista cerrada
- Comprobar e inspeccionar objetos
- Organizar las clases en archivos
- Errores frecuentes
- Resumen
Hasta ahora has guardado los datos en variables y arrays y los has procesado con funciones sueltas. Cuando un programa crece, esa separación se vuelve incómoda: el array de un socio va por un lado y las funciones que lo manejan, por otro. La programación orientada a objetos (POO) junta los datos y las operaciones sobre esos datos en una misma pieza. En esta lección aprenderás a crear clases y objetos en PHP 8, a escribir constructores cortos, a proteger los datos con la visibilidad y readonly, a usar static y constantes, y a sustituir listas de valores fijos por enums.
Clases y objetos
Una clase es un molde: describe qué datos tendrá algo y qué podrá hacer. Un objeto es una pieza concreta fabricada con ese molde. La clase Ruta describe cualquier ruta de senderismo; “Pico Dobra, 11 km” es un objeto de esa clase.
<?php
class Ruta
{
public string $nombre;
public float $km;
}
$ruta = new Ruta();
$ruta->nombre = 'Pico Dobra';
$ruta->km = 11.5;
echo "$ruta->nombre: $ruta->km km\n";
// Pico Dobra: 11.5 km
- La clase se declara con
classy un nombre en PascalCase (cada palabra empieza en mayúscula):Ruta,ReservaClase. - Las variables de dentro se llaman propiedades. Desde PHP 7.4 llevan tipo, igual que los parámetros de una función.
new Ruta()crea un objeto, también llamado instancia.- Con la flecha
->accedes a las propiedades y métodos de un objeto. Fíjate en que la propiedad se escribe sin$:$ruta->nombre, no$ruta->$nombre.
Puedes crear tantos objetos como quieras con la misma clase, y cada uno guarda sus propios valores.
Métodos y $this
Las funciones que se escriben dentro de una clase se llaman métodos. Dentro de un método, $this es el objeto con el que se ha llamado a ese método:
<?php
class Ruta
{
public string $nombre;
public float $km;
public function duracionEstimada(): string
{
$horas = $this->km / 4; // unos 4 km por hora
$h = intdiv((int) round($horas * 60), 60);
$min = (int) round($horas * 60) % 60;
return "{$h} h {$min} min";
}
}
$ruta = new Ruta();
$ruta->nombre = 'Senda del Pas';
$ruta->km = 9;
echo $ruta->duracionEstimada(), "\n";
// 2 h 15 min
Si mañana cambias la forma de calcular la duración, solo tocas el método. Todo el código que llama a duracionEstimada() sigue igual. Esa es una de las grandes ventajas de la POO.
El constructor
Rellenar las propiedades una a una después de new es pesado y peligroso: si olvidas una, el objeto queda a medias. El constructor es un método especial, __construct(), que se ejecuta automáticamente al hacer new y recibe los datos iniciales:
<?php
class Socio
{
public string $nombre;
public string $email;
public int $bonos;
public function __construct(string $nombre, string $email, int $bonos = 0)
{
$this->nombre = $nombre;
$this->email = $email;
$this->bonos = $bonos;
}
}
$lucia = new Socio('Lucía', 'lucia@ejemplo.es', 10);
echo "$lucia->nombre tiene $lucia->bonos bonos\n";
// Lucía tiene 10 bonos
Promoción de propiedades en el constructor
El código anterior repite cada dato tres veces: al declarar la propiedad, en el parámetro y en la asignación. Desde PHP 8 puedes poner la visibilidad directamente en los parámetros del constructor y PHP crea y rellena las propiedades por ti. Se llama promoción de propiedades (constructor property promotion):
<?php
class Socio
{
public function __construct(
public string $nombre,
public string $email,
public int $bonos = 0,
) {}
}
$pablo = new Socio('Pablo', 'pablo@ejemplo.es');
echo "$pablo->nombre tiene $pablo->bonos bonos\n";
// Pablo tiene 0 bonos
Es exactamente lo mismo que el ejemplo anterior, en la mitad de líneas. Es la forma habitual en PHP moderno. La coma después del último parámetro está permitida y facilita añadir parámetros más tarde.
Como con cualquier función, puedes usar argumentos con nombre, muy útiles cuando hay varios parámetros del mismo tipo:
<?php
class Socio
{
public function __construct(
public string $nombre,
public string $email,
public int $bonos = 0,
) {}
}
$ana = new Socio(email: 'ana@ejemplo.es', nombre: 'Ana', bonos: 5);
echo $ana->email, "\n";
// ana@ejemplo.es
Nota: en código antiguo (PHP 4) verás constructores con el mismo nombre que la clase, como
function Socio(). Hoy eso no es un constructor, es un método normal. Usa siempre__construct.
Visibilidad: public, private y protected
Con public, cualquiera puede leer y cambiar una propiedad. Eso permite cosas como $socio->bonos = -50;, que no tiene sentido. La visibilidad decide desde dónde se puede usar cada propiedad o método:
| Palabra | Accesible desde |
|---|---|
public | Cualquier sitio |
protected | La propia clase y sus clases hijas (lo verás en Herencia e interfaces) |
private | Solo la propia clase |
La idea se llama encapsulación: los datos se guardan como private y solo se cambian a través de métodos que comprueban que el cambio es válido.
<?php
class Bono
{
private int $usos;
public function __construct(private string $titular, int $usos = 10)
{
$this->usos = $usos;
}
public function usar(): void
{
if ($this->usos === 0) {
echo "{$this->titular} no tiene entradas disponibles\n";
return;
}
$this->usos--;
}
public function getUsos(): int
{
return $this->usos;
}
}
$bono = new Bono('Marcos', 2);
$bono->usar();
$bono->usar();
$bono->usar();
echo "Quedan {$bono->getUsos()}\n";
// Marcos no tiene entradas disponibles
// Quedan 0
Si intentas hacer $bono->usos = 100; desde fuera, PHP lanza un error: Cannot access private property Bono::$usos. Los métodos como getUsos() que solo devuelven un dato privado se llaman getters; los que lo cambian con comprobaciones, setters.
No crees getters y setters para todo “por si acaso”: usar() dice mucho más que setUsos($usos - 1).
Consejo: en vez de imprimir un aviso cuando algo no es válido, lo correcto es lanzar una excepción para que quien llama decida qué hacer. Lo verás en Excepciones.
Propiedades readonly
Muchos datos no deberían cambiar nunca después de crear el objeto: el DNI de un cliente, el importe de un pedido ya cobrado. Una propiedad readonly (PHP 8.1) se puede asignar una sola vez, desde dentro de la clase, y después solo se puede leer:
<?php
class Pedido
{
public function __construct(
public readonly int $id,
public readonly float $total,
) {}
}
$pedido = new Pedido(1042, 59.90);
echo "Pedido $pedido->id: $pedido->total €\n";
try {
$pedido->total = 0.01;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
// Pedido 1042: 59.9 €
// Cannot modify readonly property Pedido::$total
Así puedes dejar la propiedad public (es cómodo leerla sin getter) sin miedo a que alguien la cambie. Si todas las propiedades son de solo lectura, puedes escribir readonly class Pedido (PHP 8.2) y te ahorras repetir la palabra.
Nota: PHP 8.4 añadió la visibilidad asimétrica:
public private(set) int $bonosse lee desde fuera pero solo se cambia desde dentro de la clase. También añadió los property hooks, que permiten ejecutar código al leer o escribir una propiedad. Los verás en código reciente, pero con lo de esta lección tienes de sobra para empezar.
Los objetos se comparten, no se copian
Al asignar un array a otra variable, PHP hace una copia. Con los objetos no: la variable guarda un identificador del objeto, y al asignarla ambas variables apuntan al mismo objeto.
<?php
class Carrito
{
public array $productos = [];
}
$a = new Carrito();
$b = $a; // el mismo carrito
$b->productos[] = 'Pies de gato';
$c = clone $a; // una copia independiente
$c->productos[] = 'Magnesio';
echo count($a->productos), ' ', count($c->productos), "\n";
// 1 2
Lo mismo pasa al pasar un objeto a una función: si la función lo modifica, el cambio se ve fuera. Si necesitas una copia, usa clone.
Para comparar objetos, == comprueba que sean de la misma clase y tengan los mismos valores; === comprueba que sean el mismo objeto.
static y constantes de clase
Hasta ahora todo pertenecía a cada objeto. A veces un dato o una operación pertenece a la clase en general, no a un objeto concreto. Para eso está static, y se usa con :: en lugar de ->. Dentro de la clase, self:: se refiere a la propia clase.
Las constantes de clase se declaran con const y también se leen con ::. Desde PHP 8.3 pueden llevar tipo.
<?php
class Reserva
{
public const int MAX_PERSONAS = 8;
private static int $creadas = 0;
public function __construct(public string $actividad, public int $personas)
{
self::$creadas++;
}
public static function totalCreadas(): int
{
return self::$creadas;
}
public static function desdeArray(array $datos): self
{
return new self($datos['actividad'], (int) $datos['personas']);
}
}
new Reserva('Bautizo de escalada', 4);
$r = Reserva::desdeArray(['actividad' => 'Yoga', 'personas' => '2']);
echo $r->actividad, ' para ', $r->personas, "\n";
echo 'Máximo por reserva: ', Reserva::MAX_PERSONAS, "\n";
echo 'Reservas creadas: ', Reserva::totalCreadas(), "\n";
// Yoga para 2
// Máximo por reserva: 8
// Reservas creadas: 2
desdeArray() es un patrón muy habitual llamado constructor con nombre: un método estático que crea objetos a partir de otra forma de datos, como una fila de la base de datos o un formulario.
Cuidado: dentro de un método
staticno existe$this, porque no hay ningún objeto. Usa los métodos estáticos para utilidades y creación de objetos, no como sustituto de las funciones normales “porque sí”.
Métodos mágicos y encadenamiento
Los métodos que empiezan por dos guiones bajos son métodos mágicos: PHP los llama solo en momentos concretos. Ya conoces __construct. Otro muy útil es __toString(), que indica qué texto mostrar cuando usas el objeto como cadena (por ejemplo, en un echo).
Si un método devuelve $this, puedes encadenar llamadas, un estilo muy habitual en librerías:
<?php
class Factura
{
private array $lineas = [];
public function agregar(string $concepto, float $precio): static
{
$this->lineas[] = [$concepto, $precio];
return $this;
}
public function total(): float
{
return array_sum(array_column($this->lineas, 1));
}
public function __toString(): string
{
return sprintf('Factura con %d líneas: %.2f €', count($this->lineas), $this->total());
}
}
$factura = (new Factura())
->agregar('Cuota mensual', 42)
->agregar('Alquiler de arnés', 3.5);
echo $factura, "\n";
// Factura con 2 líneas: 45.50 €
Enums: valores de una lista cerrada
Muchos datos solo pueden tomar unos pocos valores: la dificultad de una ruta, el estado de un pedido, el día de la semana. Antes se usaban cadenas sueltas ('facil', 'dificil'), y bastaba un error al escribir ('dificl') para tener un fallo silencioso. Un enum (PHP 8.1) define un tipo con una lista cerrada de valores posibles:
<?php
enum Dificultad: string
{
case Facil = 'facil';
case Media = 'media';
case Dificil = 'dificil';
public function color(): string
{
return match ($this) {
Dificultad::Facil => 'verde',
Dificultad::Media => 'naranja',
Dificultad::Dificil => 'rojo',
};
}
}
function etiqueta(string $ruta, Dificultad $nivel): string
{
return "$ruta ({$nivel->value}, {$nivel->color()})";
}
echo etiqueta('Castro Valnera', Dificultad::Dificil), "\n";
$desdeFormulario = Dificultad::from('media');
echo $desdeFormulario->name, "\n";
var_dump(Dificultad::tryFrom('extrema'));
echo count(Dificultad::cases()), " niveles\n";
// Castro Valnera (dificil, rojo)
// Media
// NULL
// 3 niveles
- Cada
casees un valor posible. Al llevar: string, es un enum respaldado: cada caso tiene un valor (->value) que puedes guardar en la base de datos o recibir de un formulario. from('media')convierte el valor en el caso correspondiente y lanza un error si no existe;tryFrom()devuelvenullen ese caso, ideal para datos de usuario.cases()devuelve todos los casos, perfecto para generar las opciones de un<select>.- Un parámetro de tipo
Dificultadsolo acepta uno de esos tres casos. Los errores de escritura desaparecen.
Fíjate en que el match no necesita default: PHP sabe que no hay más casos.
Comprobar e inspeccionar objetos
instanceof comprueba si un objeto es de una clase, y var_dump() o print_r() muestran su contenido (incluidas las propiedades privadas, útil para depurar):
<?php
class Material
{
public function __construct(public string $nombre, private int $stock) {}
}
$cuerda = new Material('Cuerda 60 m', 3);
var_dump($cuerda instanceof Material);
print_r($cuerda);
// bool(true)
// Material Object
// (
// [nombre] => Cuerda 60 m
// [stock:Material:private] => 3
// )
Organizar las clases en archivos
La convención es escribir una clase por archivo, con el mismo nombre que la clase: Socio.php, Reserva.php. Los archivos de clase no llevan HTML ni etiqueta de cierre ?>. Para usarlas, las cargas con require_once (lo viste en include y require):
<?php
// index.php
require_once __DIR__ . '/clases/Socio.php';
require_once __DIR__ . '/clases/Reserva.php';
$socio = new Socio('Lucía', 'lucia@ejemplo.es');
Con muchas clases, esa lista de require_once se hace eterna. La solución es la carga automática (autoload): PHP carga cada clase la primera vez que la usas. La verás con los espacios de nombres en la siguiente lección y, de la forma profesional, con Composer.
Errores frecuentes
- Poner
$en la propiedad al acceder:$ruta->$nombreusa el valor de la variable$nombrecomo nombre de propiedad. Lo correcto es$ruta->nombre. - Olvidar
$this->dentro de un método:$kmes una variable local (que no existe); la propiedad es$this->km. - Leer una propiedad tipada sin valor: si una propiedad con tipo no tiene valor por defecto ni se asigna en el constructor, leerla da el error must not be accessed before initialization. Inicialízala siempre.
- Hacerlo todo
public: cualquiera podrá dejar el objeto en un estado imposible. Empieza porprivatey abre solo lo necesario. - Esperar una copia al asignar un objeto:
$b = $acomparte el objeto. Usaclonesi necesitas otro. - Usar
$thisen un métodostatic: no existe. Usaself::para lo estático. - Usar cadenas sueltas para estados y categorías: un enum evita errores de escritura y documenta los valores posibles.
Resumen
| Concepto | Código |
|---|---|
| Declarar una clase | class Ruta { public string $nombre; } |
| Crear un objeto | $r = new Ruta(); |
| Propiedad o método | $r->nombre, $r->duracion() |
| El objeto actual | $this->km |
| Constructor con promoción | public function __construct(private string $nombre) {} |
| Visibilidad | public, protected, private |
| Solo lectura | public readonly int $id / readonly class Pedido |
| Copiar un objeto | $copia = clone $original; |
| Estático y constantes | self::$creadas, Reserva::MAX_PERSONAS, Reserva::totalCreadas() |
| Objeto como texto | public function __toString(): string |
| Enum respaldado | enum Dificultad: string { case Facil = 'facil'; } |
| Del valor al caso | Dificultad::from('facil') / Dificultad::tryFrom($x) |
| Comprobar la clase | $r instanceof Ruta |
En la siguiente lección aprenderás a reutilizar y organizar clases con herencia, clases abstractas, interfaces, traits y espacios de nombres: Herencia e interfaces.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.