Curso de PHP gratis · Capítulo 13 de 13 · con Comandante Klass
Programación orientada a objetos en PHP: clases y objetos
La POO es como se organiza el PHP profesional (Laravel, Symfony...). Aprenderás clases, objetos, herencia e interfaces, y dónde viven los objetos en memoria.
Clases y objetos
Una clase es un plano: describe qué datos tendrá algo (propiedades) y qué sabrá hacer (métodos). Un objeto es una cosa concreta construida con ese plano (una instancia).
class Dron { public string $nombre = "sin nombre"; public int $bateria = 100; public function saludar(): string { return "Soy " . $this->nombre; } } $d = new Dron();
new Dron()crea un objeto.->accede a propiedades y métodos:$d->bateria,$d->saludar().- Dentro de un método,
$thises "el objeto sobre el que se ha llamado el método".
Puedes crear tantos objetos como quieras de la misma clase: cada uno tiene sus propias propiedades.
<?php
class Dron {
public string $nombre = "sin nombre";
public int $bateria = 100;
public function saludar(): string {
return "Soy " . $this->nombre . " y tengo " . $this->bateria . "%";
}
}
$a = new Dron();
$a->nombre = "BIT-8";
$b = new Dron();
$b->nombre = "BYTE-9";
$b->bateria = 42;
echo $a->saludar(), "\n";
echo $b->saludar(), "\n";Soy BIT-8 y tengo 100% Soy BYTE-9 y tengo 42%
El constructor
El constructor es un método especial, __construct, que se ejecuta automáticamente al hacer new. Sirve para dejar el objeto listo desde el principio:
class Dron { public function __construct(public string $nombre, public int $bateria = 100) {} } $d = new Dron("BIT-8");
Eso que ves (public string $nombre en los parámetros del constructor) es la promoción de propiedades de PHP 8: declara la propiedad y le asigna el argumento en una sola línea. Es equivalente a declarar la propiedad y hacer $this->nombre = $nombre; dentro.
<?php
class Dron {
public function __construct(
public string $nombre,
public int $bateria = 100,
) {}
public function gastar(int $cantidad): void {
$this->bateria -= $cantidad;
}
}
$d = new Dron("BIT-8");
$d->gastar(30);
echo "{$d->nombre}: {$d->bateria}%\n";
var_dump($d);BIT-8: 70%
object(Dron)#1 (2) {
["nombre"]=>
string(5) "BIT-8"
["bateria"]=>
int(70)
}Visibilidad y encapsulación
Cada propiedad y método tiene una visibilidad:
public: cualquiera puede usarlo.private: solo el código de la propia clase.protected: la propia clase y sus clases hijas (herencia).
¿Por qué esconder cosas? Para proteger las reglas del objeto. Si $bateria es pública, cualquiera puede poner $d->bateria = -500. Si es privada, la única forma de cambiarla es a través de métodos que validan. A esto se le llama encapsulación: el objeto es responsable de mantenerse en un estado correcto.
readonly (PHP 8.1) marca una propiedad que solo se puede asignar una vez.
<?php
class Dron {
private int $bateria = 100;
public function gastar(int $n): void {
$this->bateria = max(0, $this->bateria - $n);
}
public function getBateria(): int {
return $this->bateria;
}
}
$d = new Dron();
$d->gastar(250);
echo $d->getBateria(), "\n";
echo $d->bateria;0
Dónde viven los objetos
Aquí está la clave de todo. Un objeto no se guarda dentro de la variable. Los objetos viven en un almacén aparte (el montón, o heap) y cada uno tiene un número. La variable solo guarda ese número: un handle (identificador).
Por eso:
$b = $a;copia el handle:$ay$bseñalan al mismo objeto. Cambiar$b->bateriacambia "el de$a" (es el mismo).- Para tener un objeto independiente hay que clonarlo:
$c = clone $a;crea un objeto nuevo con una copia de las propiedades.
Compara con los arrays, que se copian al asignar. ¿Por qué PHP trata distinto a los objetos? Porque un objeto representa una entidad con identidad (este dron concreto), mientras que un array es un valor (una lista de datos). Si le cambias la batería a tu dron, quieres que cambie en todas partes donde se habla de ese dron.
Mira en la Lupa el montón de objetos: $a y $b apuntan a #1, y $c a #2.
<?php
class Dron {
public int $bateria = 100;
}
$a = new Dron();
$b = $a;
$b->bateria = 5;
echo $a->bateria, "\n";
$c = clone $a;
$c->bateria = 77;
echo $a->bateria, " ", $c->bateria, "\n";
var_dump($a === $b, $a === $c, $a == $c);5 5 77 bool(true) bool(false) bool(false)
$x = new Dron(); $y = $x; $y->bateria = 1; ¿Cuánto vale $x->bateria?
- 100 (el valor inicial)
- 1
- Da error
Ver respuesta
B. 1
$y = $x copió el handle: los dos apuntan al mismo objeto del montón.
Handles frente a referencias
En la Torre de los Espejos viste que un handle no es una referencia. Recordatorio con objetos de verdad:
- Una función que recibe un objeto (sin
&) recibe una copia del handle: puede modificar el objeto (es el mismo), pero si reasigna su parámetro a otro objeto, fuera no se entera. - Con
&, la función comparte la caja que contiene el handle: si la reasigna, la variable de fuera también cambia.
Por eso se dice (mal) que "en PHP los objetos se pasan por referencia". Lo correcto: se pasan por valor, pero el valor es un handle.
<?php
class Caja {
public function __construct(public string $contenido) {}
}
function modificar(Caja $c) {
$c->contenido = "heno";
}
function reemplazar(Caja $c) {
$c = new Caja("paja");
}
$mi = new Caja("vacía");
modificar($mi);
reemplazar($mi);
echo $mi->contenido, "\n";heno
Herencia
Una clase puede heredar de otra con extends: recibe todas sus propiedades y métodos, y puede añadir los suyos o sobrescribir (redefinir) los que quiera.
class Cultivo { ... } class Zanahoria extends Cultivo { ... }
parent::metodo()llama a la versión de la clase padre.protectedpermite que las hijas usen algo sin hacerlo público.instanceofcomprueba si un objeto es de una clase (o de sus hijas).
La herencia expresa una relación "es un": una zanahoria es un cultivo. Si la relación es "tiene un" (un dron tiene una batería), usa una propiedad, no herencia.
<?php
class Cultivo {
public function __construct(protected string $nombre) {}
public function describir(): string {
return "Soy {$this->nombre}";
}
public function rendimiento(): int {
return 1;
}
}
class Calabaza extends Cultivo {
public function __construct() {
parent::__construct("calabaza");
}
public function rendimiento(): int {
return 5;
}
public function describir(): string {
return parent::describir() . " y peso mucho";
}
}
$cs = [new Cultivo("hierba"), new Calabaza()];
foreach ($cs as $c) {
echo $c->describir(), " (", $c->rendimiento(), ")\n";
}
var_dump($cs[1] instanceof Cultivo);Soy hierba (1) Soy calabaza y peso mucho (5) bool(true)
Interfaces y clases abstractas
Una interfaz es un contrato: una lista de métodos que una clase promete tener. No tiene código, solo las firmas:
interface Cosechable { public function cosechar(): int; } class Trigo implements Cosechable { ... }
Una clase abstracta es una clase incompleta: no se puede instanciar con new, y puede tener métodos abstract (sin cuerpo) que las hijas deben implementar.
¿Para qué sirven? Para escribir código que funcione con cualquier objeto que cumpla el contrato, sin saber su clase concreta. A eso se le llama polimorfismo.
<?php
interface Cosechable {
public function cosechar(): int;
}
abstract class Planta implements Cosechable {
abstract public function nombre(): string;
public function informe(): string {
return $this->nombre() . " da " . $this->cosechar();
}
}
class Trigo extends Planta {
public function nombre(): string { return "trigo"; }
public function cosechar(): int { return 3; }
}
class Olivo extends Planta {
public function nombre(): string { return "olivo"; }
public function cosechar(): int { return 10; }
}
$total = 0;
foreach ([new Trigo(), new Olivo(), new Trigo()] as $p) {
echo $p->informe(), "\n";
$total += $p->cosechar();
}
echo "Total: $total\n";trigo da 3 olivo da 10 trigo da 3 Total: 16
static y constantes de clase
Hasta ahora, cada propiedad pertenecía a un objeto. Una propiedad static pertenece a la clase: la comparten todos los objetos y existe aunque no haya ninguno.
- Se accede con
:::Dron::$total, y desde dentro de la clase conself::$total. - Los métodos
staticse llaman sin objeto:Dron::crear(). Dentro no hay$this. - Las constantes de clase:
const VELOCIDAD = 2;y se leen conDron::VELOCIDADoself::VELOCIDAD.
<?php
class Dron {
const BATERIA_MAX = 100;
private static int $fabricados = 0;
public int $id;
public function __construct() {
self::$fabricados++;
$this->id = self::$fabricados;
}
public static function fabricados(): int {
return self::$fabricados;
}
}
$a = new Dron();
$b = new Dron();
echo "Dron {$b->id} de ", Dron::fabricados(), "\n";
echo "Batería máxima: ", Dron::BATERIA_MAX, "\n";Dron 2 de 2 Batería máxima: 100
Excepciones
Cuando algo va mal y un método no puede cumplir su tarea, lo correcto es lanzar una excepción: throw new Exception("mensaje"). La ejecución salta inmediatamente al catch más cercano que la capture:
try { $cuenta->retirar(500); } catch (Exception $e) { echo "Error: " . $e->getMessage(); } finally { // se ejecuta siempre, haya error o no }
Si nadie la captura, el programa se detiene con un Fatal error: Uncaught... (ya los has visto: los TypeError y DivisionByZeroError también son excepciones).
Puedes crear las tuyas heredando de Exception, y capturar varias con catch (A | B $e).
<?php
class SinBateria extends Exception {}
class Dron {
public function __construct(private int $bateria) {}
public function volar(int $minutos): void {
if ($minutos * 10 > $this->bateria) {
throw new SinBateria("Solo puedo volar " . intdiv($this->bateria, 10) . " minutos");
}
$this->bateria -= $minutos * 10;
echo "Volando $minutos minutos\n";
}
}
$d = new Dron(50);
try {
$d->volar(2);
$d->volar(4);
echo "Esto no se ejecuta\n";
} catch (SinBateria $e) {
echo "Aterrizaje de emergencia: ", $e->getMessage(), "\n";
} finally {
echo "Fin del vuelo\n";
}Volando 2 minutos Aterrizaje de emergencia: Solo puedo volar 3 minutos Fin del vuelo
Métodos mágicos: __toString
Los métodos que empiezan por __ son mágicos: PHP los llama solo en momentos concretos. Ya conoces __construct. Otro muy útil es __toString: define cómo se convierte el objeto en texto, por ejemplo al hacer echo $objeto.
Sin __toString, hacer echo de un objeto es un error: PHP no sabe cómo escribirlo.
Otros mágicos que verás por ahí: __get / __set (propiedades que no existen), __call (métodos que no existen), __clone (al clonar), __invoke (usar el objeto como función).
<?php
class Punto {
public function __construct(public int $x, public int $y) {}
public function __toString(): string {
return "({$this->x}, {$this->y})";
}
}
$p = new Punto(2, 3);
echo "BIT-8 está en $p\n";
echo "Coordenadas: " . $p . "\n";BIT-8 está en (2, 3) Coordenadas: (2, 3)
Ejercicios de Programación orientada a objetos en PHP resueltos
Intenta resolverlos antes de mirar la solución. En el juego puedes ejecutarlos y comprobarlos automáticamente.
Encargo 1. La clase Dron
El Gremio fabrica drones. Necesita el plano (la clase).
Declara la clase Dron con:
- una propiedad privada
$bateriaque empiece en100; - un método
volar(): boolque, si la batería es al menos 30, reste 30 y devuelvatrue; si no, devuelvafalsesin tocarla; - un método
getBateria(): int.
Pistas
private int $bateria = 100;- En
volar:if ($this->bateria >= 30) { $this->bateria -= 30; return true; } return false;
Ver solución
<?php
class Dron {
private int $bateria = 100;
public function volar(): bool {
if ($this->bateria >= 30) {
$this->bateria -= 30;
return true;
}
return false;
}
public function getBateria(): int {
return $this->bateria;
}
}
Encargo 2. La cuenta del Gremio
El Gremio guarda el dinero de sus miembros. Las reglas son estrictas.
Declara CuentaGremio con:
- constructor que reciba el
titular(string) y deje el saldo en 0; ingresar(int $cantidad): voidque lanceInvalidArgumentExceptionsi la cantidad no es positiva;retirar(int $cantidad): voidque lance unaExceptioncon el mensajeSaldo insuficientesi no hay bastante;getSaldo(): int.
Pistas
public function __construct(private string $titular) {}yprivate int $saldo = 0;throw new InvalidArgumentException("Cantidad no válida");- En
retirar:if ($cantidad > $this->saldo) { throw new Exception("Saldo insuficiente"); }
Ver solución
<?php
class CuentaGremio {
private int $saldo = 0;
public function __construct(private string $titular) {}
public function ingresar(int $cantidad): void {
if ($cantidad <= 0) {
throw new InvalidArgumentException("Cantidad no válida");
}
$this->saldo += $cantidad;
}
public function retirar(int $cantidad): void {
if ($cantidad > $this->saldo) {
throw new Exception("Saldo insuficiente");
}
$this->saldo -= $cantidad;
}
public function getSaldo(): int {
return $this->saldo;
}
}
Predicción 3. Gemelos y clones
La comandante alinea tres escudos. ¿Cuántos escudos hay de verdad?
Escribe exactamente la salida.
<?php
class Escudo {
public int $vida = 10;
}
$a = new Escudo();
$b = $a;
$c = clone $a;
$b->vida = 3;
$c->vida = 7;
$d = $c;
$d->vida--;
echo $a->vida, " ", $b->vida, " ", $c->vida, " ", $d->vida;Ver solución
3 3 6 6
Dos escudos de verdad: #1 (compartido por $a y $b) y #2 (por $c y $d).
Encargo 4. Cultivos polimórficos
El Gremio calcula lo que produce cada huerto sin saber qué cultivos tiene.
Declara una clase abstracta Cultivo con un método abstracto rendimiento(): int y un método informe(): string que devuelva NOMBRE rinde N (usa static::class para el nombre de la clase). Después declara Zanahoria (rinde 2) y Calabaza (rinde 5), que heredan de Cultivo.
Pistas
abstract public function rendimiento(): int;(sin cuerpo, con punto y coma).return static::class . " rinde " . $this->rendimiento();
Ver solución
<?php
abstract class Cultivo {
abstract public function rendimiento(): int;
public function informe(): string {
return static::class . " rinde " . $this->rendimiento();
}
}
class Zanahoria extends Cultivo {
public function rendimiento(): int {
return 2;
}
}
class Calabaza extends Cultivo {
public function rendimiento(): int {
return 5;
}
}
Misión del dron 5. La Cosechadora
La comandante quiere que el control de BIT-8 esté encapsulado en un objeto.
Declara una clase Cosechadora con una propiedad privada $cosechadas (empieza en 0), un método pasada(): void que recorra el campo 3x3 entero cosechando lo maduro (y contando cada cosecha) y un método total(): int. Crea una cosechadora, haz una pasada e imprime Total: 9.
Esta misión mueve al dron BIT-8 por un campo de cultivo: juégala en PHPlay para ver tu código animado.
Pistas
- El método
pasada()es tu recorrido de siempre, pero sumando a$this->cosechadas. - Al final:
$c = new Cosechadora(); $c->pasada(); echo "Total: " . $c->total();
Ver solución
<?php
class Cosechadora {
private int $cosechadas = 0;
public function pasada(): void {
$n = tam();
for ($y = 0; $y < $n; $y++) {
for ($x = 0; $x < $n; $x++) {
if (puede_cosechar()) {
cosechar();
$this->cosechadas++;
}
mover(ESTE);
}
mover(NORTE);
}
}
public function total(): int {
return $this->cosechadas;
}
}
$c = new Cosechadora();
$c->pasada();
echo "Total: " . $c->total();
Caza de bugs 6. El escudo fantasma
Un aprendiz quería probar un escudo de práctica sin estropear el original. Pero el original ha quedado destrozado.
Arregla el programa para que imprima Original: 100 | Práctica: 40.
Código de partida:
<?php
class Escudo {
public int $resistencia = 100;
}
$original = new Escudo();
$practica = $original;
$practica->resistencia -= 60;
echo "Original: {$original->resistencia} | Práctica: {$practica->resistencia}";
Pistas
$practica = $original;copia el handle, no el objeto.- Usa
clone.
Ver solución
<?php
class Escudo {
public int $resistencia = 100;
}
$original = new Escudo();
$practica = clone $original;
$practica->resistencia -= 60;
echo "Original: {$original->resistencia} | Práctica: {$practica->resistencia}";
Chuleta: lo esencial
class X { public $p = 1; public function m() { return $this->p; } }—$o = new X(); $o->m();- Constructor:
__construct. Promoción PHP 8:public function __construct(private int $n) {}. - Visibilidad:
public,private(solo la clase),protected(clase e hijas).readonly: se asigna una vez. - Los objetos viven en el montón; la variable guarda un handle (#id).
$b = $acomparte el objeto;clonecrea otro. - Se pasan por valor, pero el valor es un handle: la función puede modificar el objeto, no reasignarlo fuera (salvo con
&). - Herencia:
extends,parent::m(),instanceof. Interfaces (implements) y clasesabstract. static: de la clase (self::$n,X::m()). Constantes:const MAX = 5;→X::MAX.- Excepciones:
throw new Exception("..."),try { } catch (Tipo $e) { } finally { }. __toString()define cómo se imprime un objeto.