Guía completa para tipar fuertemente tu aplicación en Laravel
Publicado por Andrés López

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.0 | Argumentos de funciones: int, float, string, bool. Uso de declare(strict_types=1) |
PHP 7.1 | Tipado para void y iterable . Soporte para nullables ?int |
PHP 7.4 | Propiedades tipadas public int $id |
PHP 8.0 | Union Types: int|string . Nuevo tipo mixed |
PHP 8.1 | Implementación de readonly |
PHP 8.2 | true 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
- Tipado avanzado
- ¿Qué hacer si no puedes tipar?
- Tipados habituales en Laravel
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:
- Usar la inyección de dependencias autómatica ofrecida por laravel (métodos como controladores o
handle
) - Utilizar el método
getFacadeRoot
y colocarlo en un método con el tipado fuerte de la facade correspondiente - 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 inicialStringable
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
Gran fan de Laravel, entusiasta de Vue y escritor de cualquier cosa que suene interesante