PHPlay

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, $this es "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";
Salida
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);
Salida
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;
Salida
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: $a y $b señalan al mismo objeto. Cambiar $b->bateria cambia "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.
Pergamino del porque

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);
Salida
5
5 77
bool(true)
bool(false)
bool(false)
Pregunta

$x = new Dron(); $y = $x; $y->bateria = 1; ¿Cuánto vale $x->bateria?

  1. 100 (el valor inicial)
  2. 1
  3. 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";
Salida
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.
  • protected permite que las hijas usen algo sin hacerlo público.
  • instanceof comprueba si un objeto es de una clase (o de sus hijas).
Truco

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);
Salida
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";
Salida
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 con self::$total.
  • Los métodos static se llaman sin objeto: Dron::crear(). Dentro no hay $this.
  • Las constantes de clase: const VELOCIDAD = 2; y se leen con Dron::VELOCIDAD o self::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";
Salida
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";
}
Salida
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";
Salida
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 $bateria que empiece en 100;
  • un método volar(): bool que, si la batería es al menos 30, reste 30 y devuelva true; si no, devuelva false sin 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): void que lance InvalidArgumentException si la cantidad no es positiva;
  • retirar(int $cantidad): void que lance una Exception con el mensaje Saldo insuficiente si no hay bastante;
  • getSaldo(): int.
Pistas
  • public function __construct(private string $titular) {} y private 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 = $a comparte el objeto; clone crea 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 clases abstract.
    • 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.