Kodokon kodokon.com

PHP 8 avanzado: atributos, readonly, callables

Explora el funcionamiento interno de los atributos, readonly, los callables de primera clase y el tipado estricto de PHP 8.

10 min · 3 preguntas

Abrir esta lección en Kodokon

Los atributos reemplazan las anotaciones de docblock por clases reales, validadas por el motor. Un atributo no es más que metadatos latentes: escribir #[Route('/users')] no crea ningún objeto. Solo cobra vida cuando algún código lo consulta mediante reflexión y llama a newInstance() - que es exactamente lo que hacen los frameworks al arrancar para construir sus tablas de enrutamiento. El destino permitido (TARGET_METHOD, TARGET_CLASS...) se declara a su vez mediante un atributo sobre la clase del atributo.

PHP
<?php
declare(strict_types=1);

#[Attribute(Attribute::TARGET_METHOD)]
final class Route
{
    public function __construct(
        public readonly string $path,
        public readonly string $method = 'GET'
    ) {}
}

final class UserController
{
    #[Route('/users', method: 'GET')]
    public function index(): array
    {
        return ['users' => []];
    }
}

$ref = new ReflectionMethod(
    UserController::class,
    'index'
);

foreach ($ref->getAttributes(Route::class) as $attr) {
    $route = $attr->newInstance();
    echo $route->method, ' ', $route->path, PHP_EOL;
}
El atributo solo se instancia en el momento de newInstance().

readonly - propiedades en 8.1, clases enteras en 8.2 - garantiza que una propiedad se escribe una sola vez, desde dentro del ámbito de la clase. Es la herramienta para los objetos de valor: cualquier "modificación" se convierte en la construcción de un nuevo objeto, lo que elimina toda una familia de errores de estado compartido. Una clase readonly exige que todas sus propiedades estén tipadas y no sean estáticas.

PHP
<?php
declare(strict_types=1);

final readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency
    ) {}

    public function add(Money $other): self
    {
        if ($other->currency !== $this->currency) {
            throw new InvalidArgumentException(
                'Currency mismatch'
            );
        }
        return new self(
            $this->amount + $other->amount,
            $this->currency
        );
    }
}

$total = new Money(500, 'EUR');
$total = $total->add(new Money(250, 'EUR'));
echo $total->amount;
Un objeto de valor: sumar crea una nueva instancia.

La sintaxis de callable de primera clase strtoupper(...) (PHP 8.1) crea un Closure a partir de cualquier función o método. A diferencia de las cadenas como 'strtoupper' o los arrays como [$obj, 'method'], el símbolo se verifica en el momento de la creación y las herramientas de análisis estático siguen los tipos de principio a fin. Para un método de instancia, el closure captura el objeto; para un método estático, solo la clase.

PHP
<?php
declare(strict_types=1);

$words = ['php', 'rust', 'go'];

$upper = strtoupper(...);
var_dump($upper('php'));

print_r(array_map(strtoupper(...), $words));

final class Mailer
{
    public function send(string $to): bool
    {
        return $to !== '';
    }

    public static function ping(): string
    {
        return 'pong';
    }
}

$mailer = new Mailer();
$send = $mailer->send(...);
$ping = Mailer::ping(...);

var_dump($send('dev@example.org'), $ping());
Cada expresión f(...) produce un Closure verificado en el momento de la creación.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Cuándo se instancia la clase de un atributo #[Route(...)]?
    • Cuando se compila el archivo que lo contiene
    • En cuanto se llama al método decorado
    • Solo cuando algún código lo consulta mediante reflexión y llama a newInstance()
  2. ¿Qué garantiza exactamente una propiedad readonly?
    • El objeto que referencia se vuelve completamente inmutable
    • Se escribe una sola vez, desde dentro del ámbito de la clase; el objeto referenciado en sí sigue siendo mutable
    • Solo se puede leer desde la clase que la declara
  3. ¿Cuál es el alcance de declare(strict_types=1)?
    • Todo el proyecto en cuanto un solo archivo lo declara
    • Las llamadas escritas en el archivo que contiene la declaración
    • Solo las funciones definidas en ese archivo, dondequiera que se las llame