Guía completa para tipar fuertemente tu aplicación en Laravel

Publicado por Andrés López

Guía completa para tipar fuertemente tu aplicación en Laravel

Primero que nada ¿Qué es el tipado fuerte (específicamente en PHP)? ☝🏻🤓

Según ChatGPT... es bromaaaa, el contenido de este post es totalmente orgánico en base a mi experiencia como desarrollador.

El tipado fuerte a palabras simples es la declaración estricta de tipos en tu código en donde dices esta variable es estritamente una cadena de texto, un número e incluso casos más complejos como es un objeto tipo Auto. Hay lenguajes de programación como Java y C# en donde esto es la norma, sin embargo en otros lenguajes como puede ser Javascript y Python es totalmente libre de este tipado, y luego tenemos lenguajes que están entre medio, puedes o no usar tipado fuerte, PHP es uno de ellos y durante mucho tiempo estuvo más cerca de python que de Java.

Sin embargo con las nuevas versiones, específicamente a partir la versión 7.0, PHP fue introduciendo el tipado poco a poco. Con cada versión fueron agregando nuevas características de tipado:

PHP 7.0Argumentos de funciones: int, float, string, bool. Uso de declare(strict_types=1)
PHP 7.1Tipado para void y iterable. Soporte para nullables ?int
PHP 7.4Propiedades tipadas public int $id
PHP 8.0Union Types: int|string. Nuevo tipo mixed
PHP 8.1Implementación de readonly
PHP 8.2true como tipo

Y es por ello que a día de hoy te topas con mucho código dinamico en proyectos antiguos, pues antes esto no era un estándar y a día de hoy se podría decir que sigue sin serlo, nada te detiene de declarar funciones de esta manera:

function add($a, $b) {
    return $a + $b;
}

Incluso el mismo Laravel no te obliga a tipar muchas de las cosas, simplemente puedes usar la función app() o alguna Facade para todo y no tendrás nigún inconveniente:

use Illuminate\Support\Facades\Cache;

Cache::set('key', 'value');

app('cache')->set('key', 'value');

Y ese es el breve contexto del porque quizá aunque ya programes en Laravel el tipado fuerte no ha sido parte de tu día a día, ahora si comencemos. aquí tienes una tabla de contenidos para tu referencia:

Primeros pasos del tipado fuerte

Como todo lenguaje de programación es mejor empezar por los primitivos, declará parámetros y propiedades siempre que uses estos tipos de datos int, float, string, bool

class MyClass
{
    public int $id; // número
    public string $name; // cadena de carácteres
    public bool $isAdmin; // valor true o false

    public function setId(int $id)
    {
        $this->id = $id;
    }

    public function setName(string $name)
    {
        $this->name = $name;
    }
}

Declarar retornos

Haz que el retorno de tus funciones también esten tipadas:

function isAdmin(): bool // true o false
{
    return $this->isAdmin;
}

Adicional a esto, es muy habitual que retornemos $this en nuestras funciones de clase, si haces esto solo recuerda tipar con static:

class MyClass
{
    public string $name;

    public function setName(string $name): static // devuelve a sí mismo
    {
        $this->name = $name;

        return $this;
    }
}

Tipos compuestos

En ocasiones no siempre nuestras variables y funciones pueden tener un valor, a veces simplemente es null, PHP cuenta con el nullable operator ? para estos casos:

class MyClass
{
    public ?string $lastName = null; // debe tener un valor inicial si no al intentar acceder nos dará error

                                // en este caso no usamos ? porque si o si requerimos que sea un valor
    public function setLastName(string $lastName): static
    {
        $this->lastName = $lastName;

        return $this;
    }

    public function getLastName(): ?string // o retorna un string o nada
    {
        return $this->lastName;
    }
}

Uniones

Si es nesario manejar más de un tipo de dato (no recomendable) puedes usar la unión:

                      // número o cadena de carácteres
public function count(int|string $value): int
{
    is_int($value) ? $value : count($value);
}
                                    // número o cadena de carácteres
public function getRandomValue(): int|string
{
    return $this->generate();
}

Tipos especiales

A partir de aqui, todo lo anterior se sigue repitiendo, solo que con nuevos tipos:

  • array [Lista de elementos]
  • object [Un objeto con multiples propiedades, no confundir con instancia de clase]
  • callable [Una función anónima]
  • iterable [Algo que se puede recorrer como lista]
public function getFistElement(array $values): int
{
    return $values[0];
}

public function getData(object $data): array
{
    return get_object_vars($data);
}

public function call(callable $callback)
{
    $callback();
}

public function printList(iterable $values)
{
    foreach($values as $value) {
        echo $value + "\n";
    }
}

Tipado inusual

De aquí estos aunque son útiles son un poco raros y son utilizados para contextos muy, muy específicos:

  • mixed no tienes ni idea que de pueda ser
  • never nunca es nada
  • void no retorna nada, este si es muy útil
  • true siempre será true
public function setData(mixed $data) // puede ser un string, un int, un objeto, un array...
{
    $this->data = $data;
}

public function abortProcess(): never
{
    exit('Fatal error'); // termina el proceso
}

public function sendNotificarion(string $email): void // no tiene ningún return dentro
{
    sendEmail($email, "Helloooo");
}

public function isAvaialable(): true
{
    if($this->isWorking()) {
        return true:
    }

    exit('Fatal error');
}

Tipado avanzado

Ahora que ya sabes como funciona el tipado fuerte, puedes profundizar en códigos más complejos que interacturen con clases, interfaces y hasta enums, mismos principios tipos más complejos:

use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use App\Enums\Role;

public function authenticate(User $user): bool
{
    return $this->doLogin($user, true);
}

public function getUsers(): Collection
{
    return User::where('authorized', true)
                ->where('is_active', true)
                ->orderBy('created_at')
                ->limit(50)
                ->get();
}

public function chooseRole(string $roleName): ?Role
{
    return Role::tryFrom($roleName);
}

¿Qué hacer si no puedes tipar?

Aunque esto es demasiado raro, ya que en teoría todo se puede tipar, a veces Laravel hace unas cosas graciosas a nivel de implementación de código, como las Facades, o las reglas de validación string y en general todo lo que tenga que ver con el Service Container:

use Illuminate\Support\Facades\Cache;

public function storeData(array $data): bool
{
    $value = Cache::put('key', $data); // wtf is $value??

    return $value;
}

Qué aunque podemos aún así tipar bastante bien a veces queremos evitar el dolor de cabeza con el tipado, y es donde entran los PHPDocs. Si quieres iniciar lo más rápido posible a entenderlos te recomiendo extensamamente la documentación de PHPStan.

PHPDocs

Los PHP docs es una manera de declarar tipos sin necesidad de forzar la interpretación en tiempo de ejecución, es decir de que no te arroje errores fatales si es el tipo equivocado, pero muy útil para tu interprete de código y en general es muy fácil compartir documentación técnica con otros:

/**
 * The description of the class
 */
class MyClass
{
    /**
     * The description of my property
     */
    public $myProperty;

    /**
     * The description of my method
     */
    public function myMethod($param)
    {
        //
    }
}

Hasta aquí todo es muy fácil de entender, pero ¿y el tipado? no solo podemos describir nuestras implementaciones sino que podemos aplicar una especie de tipado suave a la misma, mediante las tags @var, @param y @return, retomando nuestra implementación anterior:

class MyClass
{
    /**
     * The description of my property
     * 
     * @var string
     */
    public $myProperty;

    /**
     * The description of my method
     * 
     * @param string $param
     * @return \App\Models\MyModel
     */
    public function myMethod($param)
    {
        //
    }
}

Dentro de la definición de cada tipo puedes colocar cualquiera de los valores previamente vistos, la diferencia es que estos no forzaran a limitar los tipos, es decir no arrojará niguna excepción si no pasas el tipo adecuado. en caso de que tengas métodos mágicos como __get(), __set() y __call(), quizá sepas que tu editor de código no te ayudará a autocompletar estos valores, aquí los PHPDocs son muy útiles, mediante las tags @method y @property:

/**
 * @method static name(string $value)
 * @method void lastName()
 * @property string $name
 * @property int $age
 */
class MyClass
{
    public array $data = [];
    
    public function __call($name, $arguments)
    {
        if(isset($arguments[0])) {
            $this->data[$name] = $arguments[0];
            return $this;
        }

        if(array_key_exists($name, $this->data)) {
            return $this->data[$name];
        }
        
    }

    public function __get(string $name): mixed {
        return array_key_exists($name, $this->data) ? $this->data[$name] : null;
    }
}

Y por último el tipado general, basicamente puedes decir que cualquier variable es de un tipo, ejemplo:

/** @var \Illuminate\Config\Repository $config */
$config = config();

El código no queda tan bonito así, por eso casi siempre recomiendo tipar solo funciones, métodos y clases.

De esta manera tu editor de código será capaz de reconocer las propiedades y métodos y ayudarte a autocompletar.

Tipados habituales en Laravel

Con todo lo anterior, aún así es díficil saber como tipar en algunas secciones de Laravel, sobre todo aquellas que funcionan con "magia", como los controladores que pueden devolver varios tipos de datos, o los Listeners que tienen propiedades dependientes de los eventos.

Facades

Las Facades (fachadas) son las más díficiles de tipar, debido a su dinamismo y el concepto mismo del patron Facade, sin embargo Laravel ofrece una guía en su documentación.

Habitualmente se ofrecen 3 métodos para acceder a las clases de las facades:

  1. Usar la inyección de dependencias autómatica ofrecida por laravel (métodos como controladores o handle)
  2. Utilizar el método getFacadeRoot y colocarlo en un método con el tipado fuerte de la facade correspondiente
  3. Utilizar manualmente el método app para resolver clases y colocarlo en un método con el tipado fuerte correspondiente al resolved

App

use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\App as ApplicationFacade;

public function index(Application $app): void
{
    $app;
    $this->getApp();
}

public function getApp(): Application
{
    return ApplicationFacade::getFacadeRoot();
}

public function getApp(): Application
{
    return app('app');
}

Guard

use Illuminate\Contracts\Auth\Guard;
use Illuminate\Support\Facades\Auth as AuthFacade;


public function index(Guard $auth):void
{
    $auth;
    $this->getAuth();

}

public function getAuth(): Guard
{
    return AuthFacade::getFacadeRoot();
}

public function getAuth(): Guard
{
    return app('auth.driver');
}

Auth

use Illuminate\Auth\AuthManager;
use Illuminate\Support\Facades\Auth as AuthFacade;

public function index(AuthManager $auth): void
{
    $auth;
    $this->getAuth();
}

public function getAuth(): AuthManager
{
    return AuthFacade::user();
}

public function getAuth(): AuthManager
{
    return app('auth');
}

Cache

use Illuminate\Cache\Repository as CacheRepository;
use Illuminate\Support\Facades\Cache as CacheFacade;

public function index(CacheRepository $cache): void
{
    $cache;
    $this->getCache();
}

public function getCache(): CacheRepository
{
    return app('cache.store')
}

Config

use Illuminate\Config\Repository as ConfigRepository;
use Illuminate\Support\Facades\Config as ConfigFacade;

public function index(ConfigRepository $config): void
{
    $config;
    $this->getConfig();
}

public function getConfig(): ConfigRepository
{
    return AuthFacade::getFacadeRoot();
}

public function getConfig(): ConfigRepository
{
    return app('config');
}

Storage

use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Support\Facades\Storage as StorageFacade;

public function index(FilesystemManager $storage): void
{
    $storage;
    $this->getStorage();
}

public function getStorage(): FilesystemManager
{
    return StorageFacade::getFacadeRoot();
}

public function getStorage(): FilesystemManager
{
    return app('filesystem');
}

Session

use Illuminate\Session\SessionManager;
use Illuminate\Support\Facades\Session as SessionFacade;

public function index(SessionManager $session): void
{
    $session;
    $this->getSession();
}

public function getStorage(): FilesystemManager
{
    return SessionFacade::getFacadeRoot();
}

public function getStorage(): FilesystemManager
{
    return app('session');
}

Estás serían algunas de las maneras típicas de tipar las clases más usadas de Laravel, siguiendo la misma lógica puedes deducir el resto desde la referencia de facades de la documentación de Laravel

Controllers

Los controladores son las cosas más sencillas de tipar, existen 3 tipos de retorno habituales:

use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\View\View;


class MyController
{
    public function regularResponse(): Response
    {
        return response('
            <h1>
                html content
            </h1>
        ');
    }

    public function jsonResponse(): JsonResponse
    {
        return response()->json([
            'data' => 'json content'
        ]);
    }

    public function viewReponse(): View
    {
        return view('welcome');
    }
}

Comandos

Hay algo muy curioso que no se suele profundizar, y es que los comandos de laravel deben de tener retorno para indicar si un proceso fue adecuado o no, incluso existen constantes en los comandos para que sea más fácil:


public function handle(): int
{
    if($this->success()) {
        return static::SUCCESS;
    }

    if($this->invalid()) {
        return static::INVALID;
    }

    return static::FAILURE;
}

Modelos

Aquí la cosa se empieza a complicar, ya que los modelos son bastante flexibles, y eso impide un correcto tipado, por ejemplo de las propiedades, los mutators y accessors, el casting, etc. ¿La solución? Los php docs:

use Illuminate\Database\Eloquent\Casts\Attribute;

/**
 * @property int $id
 * @property string $email
 * @property string $password
 * @property array $config
 * @property string $domain
 * @property ?\Illuminate\Support\Carbon $email_verified_at
 * @property ?\Illuminate\Support\Carbon $created_at
 * @property ?\Illuminate\Support\Carbon $uptaded_at
 */
class User extends Model
{
    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
            'config' => 'array'
        ];
    }

    /**
     * Get the domain from the email
     * 
     * @return \Illuminate\Database\Eloquent\Casts\Attribute
     */
    protected function domain(): Attribute
    {
        return Attribute::make(
            get: fn(mixed $value, array $attributes) => Str::of($attributes['email'])->afterLast('@')->toString()
        );
    }
}

Llamadas anonimas

Muchas de las funcionalidades de Laravel funcionan en base a funciones anónimas, aquí algunos ejemplos de como tiparlas.

DB y Eloquent

use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Query\JoinClause;

User::query()
    ->when($name, function(Builder $query, string $name): void {
        $query->where('name', $name);
    })
    ->where(function(Builder $query): void {
        $query->where(...)
                ->orWhere(...);
    })
    ->join(function(JoinClause $join): void {
        $join->on(...);
    })
    ->chunk(function(Collection $users): void {
        $users->map(function(User $user): User {
            return $user;
        });
    });

La utilidad Str

La utilidad Str es de lo mejor que Laravel cuenta, sin embargo es poco usado y por lo mismo poco se sabe tipar, solo hay que hacer una aclaración

  • Str es la clase inicial
  • Stringable es la clase devuelta por Str
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;

$name = 'andres lopez  ';

Str::of($name)
    ->trim()
    ->title()
    ->pipe(function(Stringable $str): Stringable|string {
        if($str->cointains('Lopez')) {
            return $str;
        };

        return 'default name';
    });

Collections

Las colecciones de Laravel también cuentan con un montón de métodos que utilizan funciones anonimas, estás son mucho más fácil de tipar ya que casi siempre es Collection:

use Illuminate\Support\Collection;

Collection::make([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
            ->map(function(int $number): int {
                return $number;
            })
            ->pipe(function(Collection $collection): Collection {
                return $collection;
            })
            ->before(function (int $item, int $key) {
                return $item > 5;
            });

Aún faltan más detalles respecto al tipado, sin embargo con esta guía es bastante intuitivo continaur aprendiendo acerca, espero te pueda servir y te unas al lado bueno, donde los desarrolladores tipan.

Andrés López

Andrés López

Gran fan de Laravel, entusiasta de Vue y escritor de cualquier cosa que suene interesante