Cargando…

Esto está tardando más de lo esperado.

Estándares de código

PHP

Estos son los estándares que aplica el analizador. Cada regla dice qué revisa y por qué existe.

Todos los estándares

Idioma y texto

Los identificadores de código y los comentarios deben estar en inglés.
El texto no inglés visible para el usuario debe vivir en archivos de traducción, no directamente en el código fuente PHP/Blade. Informa esta regla solo cuando el texto se muestre claramente al usuario, como etiquetas, botones, encabezados, notificaciones, diálogos, mensajes de validación, correos o salida de Blade.
No uses esta regla para exigir wrappers de traducción para texto que ya está en inglés. El uso de helpers de traducción es una cuestión de consistencia aparte.
Al agregar nuevo texto localizado visible para el usuario, usa helpers de traducción y actualiza cada archivo JSON de locale del proyecto que exista para la UI, normalmente lang/en.json y lang/es.json. No agregues el helper en PHP/Blade sin verificar o agregar las entradas correspondientes en esos archivos de locale.
Al corregir texto no inglés visible para el usuario, prefiere una clave de traducción literal como __('Format saved'), __('Edit') o __('Remote'). No inventes claves de traducción abstractas, numeradas, con forma de variable o con namespace como __('validator.text1'), __('message_1') o __('module.section.label') a menos que el proyecto ya tenga esa convención o el cambio actual agregue explícitamente las entradas de traducción requeridas como parte de un cambio deliberado de i18n.
Al reemplazar texto no inglés visible para el usuario con un helper de traducción, actualiza el archivo JSON de traducción apropiado en el mismo cambio, a menos que la clave ya exista. Conserva el texto visible original exactamente como valor de traducción, incluidos acentos, puntuación, mayúsculas y espacios. No dejes el código fuente cambiado a __('English key') sin agregar o verificar la entrada JSON correspondiente.
El texto largo es la excepción a la clave literal. Una página legal, una política o el cuerpo de un correo es un bloque de prosa que no tiene un literal corto que pueda nombrarlo, y se reescribe con el tiempo sin que cambie el código. Dale una clave con nombre en un archivo de locale propio, como lang/es/legal.php, en lugar de una entrada JSON. El bloque conserva el marcado inline que le corresponde — un <strong> alrededor de un término definido, una etiqueta en negrita al abrir un elemento de lista — y la página lo renderiza sin escape, pasando cualquier valor que interpole ya escapado.
Una fecha que le dice al lector cuándo se revisó por última vez un documento pertenece al texto, junto al texto cuya fecha indica. No la derives del sistema de archivos: filemtime() en un archivo Blade lee la vista compilada, así que limpiar la caché de vistas vuelve a marcar silenciosamente un documento legal como revisado hoy, e incluso contra la fuente se movería cada vez que se toque el marcado por razones no relacionadas.
Mueve el texto largo en el idioma en que fue escrito y luego tradúcelo en un cambio separado. Reubicar y traducir a la vez deja un diff donde no se puede comprobar nada: una cláusula que se perdió en el movimiento se ve exactamente igual que una cláusula reformulada en la traducción. Mover primero mantiene el texto verificable frente a la página de la que salió.
No apliques la regla de texto visible para el usuario a tests que afirman el texto renderizado esperado. Las aserciones de tests pueden contener o referenciar exactamente el texto no inglés que verifican porque no se renderizan a los usuarios.
No apliques la regla de texto visible para el usuario a valores internos persistidos, valores de enum/base de datos, claves de arrays, nombres de rutas, nombres de eventos, config keys o tokens de comparación que no se renderizan directamente a los usuarios. Por ejemplo, un mapa de estado a color con claves de valores de estado en español almacenados es lógica interna, no texto visible.

Limpieza del código

Mantén el código directo y legible:

  • Integra variables de un solo uso cuando no aporten claridad al significado.
  • Integra métodos helper pequeños de un solo uso cuando el nombre no aporte un concepto útil.
  • Limita un comentario a una etiqueta de cuatro palabras o menos; lo que habría explicado debe estar en el nombre, o en docs/{project}. Consulta Comments Earn Their Place.
  • Omite el tipo de retorno del método, salvo cuando lo exija un contrato/anulación del framework. Omitirlo no es motivo para escribir el mismo tipo en un PHPDoc en su lugar.
  • Usa closures con function en formato largo; nunca uses funciones flecha (fn).

Los tipos de retorno están permitidos cuando los requiere una clase padre, una interfaz, un método abstracto, un contrato del framework, o cuando la convención del proyecto los usa claramente para pequeños helpers de predicado/cálculo.

Los comentarios se ganan su lugar

El nombre es la documentación. Un método cuyo nombre dice lo que hace no necesita nada encima, y un comentario escrito de todos modos es una segunda descripción que envejece por su cuenta, hasta que contradice el código que nadie actualizó con él. Cuando el código necesita explicación, la explicación pertenece a docs/{project}, donde puede encontrarla un lector que no esté abriendo este archivo; para eso existe la documentación para desarrolladores.
Salvo las anotaciones —un bloque que solo señala algo a una herramienta, más abajo—, una sola prueba decide cada comentario de un archivo: ¿es una etiqueta o es una explicación?
Una etiqueta nombra algo en una sola línea, en cuatro palabras como máximo, sin punto final y sin etiqueta @. Es demasiado corta para contener una explicación, así que no tiene nada que pueda desfasarse del código. Es válida en cualquier parte: encima de la declaración de la clase, entre métodos, dentro de un cuerpo o dentro de un método dejado vacío a propósito. El código comentado nunca es una etiqueta, sea cual sea su longitud: no es un nombre, y eliminarlo es la solución completa. Un // vacío que mantiene abierto un bloque, como un catch vacío, es una etiqueta sin contenido y pasa igual, pero no deja constancia de nada, así que no marca un método como vacío a propósito, y No Empty Methods sigue reportando ese cuerpo. La forma de una etiqueta es libre en todas partes salvo a nivel de clase —dentro del cuerpo de una clase pero fuera de un método—, donde sirve para dividir y se escribe en la forma /* */ indicada más abajo.
Una explicación es cualquier otra cosa: más de una línea, más de cuatro palabras, un punto final o una etiqueta @. Se reporta como advertencia, así que nada se bloquea por ella, pero eso no es permiso para dejarla. Primero traslada la explicación y después elimina el comentario: lo que contiene —por qué el flujo tiene esa forma, cuál es la restricción externa, qué se rompe si cambia— va al documento de docs/{project} que ya es dueño del tema, nunca a una página propia. Escribirlo corresponde al paso de documentación y no a la ejecución que eliminó el comentario, así que indícalo como documentación para desarrolladores pendiente y deja que /develop la escriba en la ejecución que hace el cambio, /finish cuando un barrido descubra que nunca se escribió, y /document para una pasada propia. La mayoría de los comentarios no contienen nada que el código no diga ya: no hay nada que trasladar, y eliminarlos es la solución completa. Cuando lo que falló fue el nombre, renombra el método hasta que diga lo que hace el código.
La configuración, las migraciones, las pruebas y los archivos de rutas están exentos, el mismo conjunto que ignora Class Size: un archivo de configuración publicado por un paquete trae la documentación del proveedor sobre cada opción, y eliminarla separa el archivo del original que restaurará la siguiente publicación. En cualquier otro lugar, ninguna parte de un archivo queda fuera de esta regla. Un bloque encima de la declaración de la clase se juzga como cualquier otro: cuatro palabras o menos y es una etiqueta; más largo y es una explicación que pertenece a docs/{project}. Hay un lugar del archivo que se juzga en otro sitio: un comentario encima de un registro de ruta pertenece a Laravel Routes, que exige un comentario de excepción que explique por qué la ruta no es un flujo de interfaz de Livewire, así que un bloque situado encima de una llamada Route:: se conserva sea cual sea su longitud.

// Correcto — el nombre lo dice, así que no hay nada encima
public function forgetCachedPermissionsForTeam($teamId)

// Correcto — una etiqueta que divide una clase, en la forma `/* */` que usa una división
/*
 * Relationships
 */

// Incorrecto — una explicación encima de la clase, y `docs/{project}` es donde va
/*
 * The country decides the currency and the price a user is offered, so a
 * project that sells subscriptions cannot let anyone in without one.
 */

class EnsureHasCountry

// Incorrecto — describe el método en lugar de nombrarlo
/**
 * Bump the generation so every team key changes at once, which is how a
 * single write invalidates all teams on a cache store without tags.
 */
public function forgetCachedPermissionsForAllTeams()

Una etiqueta que divide los métodos de una clase se escribe en la forma /* */ de varias líneas que usan el resto de estos estándares. Los estándares de modelos, traits y observadores exigen esa división en los casos que nombran, y el estándar de Livewire la permite; la forma se reporta por sí sola, como advertencia para reescribirla, no para eliminarla. Esto se aplica solo a una etiqueta situada a nivel de clase: dentro del cuerpo de un método, una etiqueta pasa en cualquier forma, porque allí marca un paso y no divide una clase.

// Incorrecto — a nivel de clase, una división escrita como docblock
/**
 * Functions
 */

// Incorrecto — a nivel de clase, una división dibujada con líneas
// ────────────────────────────────
// Subscription actions
// ────────────────────────────────

Una anotación no es un comentario sobre el código: es una señal que leen otras herramientas. Un bloque que contiene @see, @link, @deprecated o una URL suelta —y nada más— pasa, y también pasa un @var sobre una variable local dentro del cuerpo de un método, porque eliminarlo desactiva una advertencia del editor o una pista de tipo que docs/{project} no puede reemplazar. Un @var encima de una declaración no entra en ese caso; falla, como se explica más abajo. La prosa no viaja con la anotación: un bloque que trae ambas cosas se reporta, la explicación pasa a docs/{project} y el bloque queda solo con la anotación.

// Correcto — una referencia que ningún código lleva
/**
 * @see https://heroicons.com/
 */
public $icon = 'circle-stack';

// Incorrecto — una explicación pegada a la referencia
/**
 * @see https://stripe.com/docs/billing
 *
 * The charge is retried three times because the gateway reports every
 * attempt separately and the last one decides the cancellation.
 */

Un @param, @return o @var encima de una declaración es el único caso que falla en lugar de advertir. Repite lo que la firma ya contiene: elimina las líneas de la etiqueta, y el bloque entero cuando no queda nada más. Omitir un tipo de retorno no es una invitación a escribir ese tipo encima del método, y una forma tampoco lo salva: @return array{type: string, value: string} sigue siendo la firma escrita dos veces, en un lugar que envejece por separado. El @var que se conserva es el que va sobre una variable local dentro del cuerpo de un método, porque le indica al editor algo que ninguna declaración expresa.

// Incorrecto — repite la firma
/**
 * @param  int|string|Model|null  $teamId
 */
public function forgetCachedPermissionsForTeam($teamId)

Estructuras de Control en Múltiples Líneas

Las estructuras de control deben poner su cuerpo en su propia línea, envuelto entre llaves. Nunca escribas la condición, la llave de apertura, el cuerpo y la llave de cierre en la misma línea.
Esto se aplica a if, elseif, else, for, foreach, while, do ... while, switch, try y catch. También se aplica a un cuerpo sin llaves escrito en la línea de la condición, como if (! $user) return null;.
Una llave de cierre puede compartir su línea con la palabra clave que continúa la estructura, como } else {, } elseif (...) {, } catch (...) { o } while (...);. Ese es el resultado del formateador y es correcto. Lo que nunca debe compartir una línea es el cuerpo.
Reporta esta regla siempre que el cuerpo de una estructura de control comparta una línea con su condición, incluso cuando el cuerpo sea una sola sentencia corta, incluso cuando el código por lo demás sea correcto, e incluso cuando no se haya ejecutado el formateador. Esta es una regla de legibilidad, no de espacios en blanco controlados por el formateador: se puede reportar por sí sola.
No extiendas esta regla a expresiones. Los pipelines encadenados de colecciones, los closures pasados como argumentos, los brazos de match y los literales de array no son cuerpos de estructuras de control.

// Correct
if ($user->referral_code) {
    return $user->referral_code;
}

do {
    $code = strtoupper(Str::random(8));
} while (User::where('referral_code', $code)->exists());

// Incorrect
if ($user->referral_code) { return $user->referral_code; }

do { $code = strtoupper(Str::random(8)); } while (User::where('referral_code', $code)->exists());

if (! $user) return null;

Arrays de varias líneas

Los arrays cortos vacíos, los arrays de un solo elemento, los arrays de dos elementos que quepan dentro del límite de longitud de línea y las listas simples pueden mantenerse en una sola línea. Un array corto con tres o más elementos directos debe colocar [ y ] en líneas separadas, con cada elemento directo comenzando en su propia línea, cuando tenga una clave => o un valor directo de más de 30 caracteres de origen. Los atributos de PHP son metadatos y pueden mantener sus arrays en línea.
Un array corto cuyos elementos todos lleven claves => también debe dividirse cuando la línea en la que está escrito supera los 100 caracteres, incluso cuando solo tenga dos elementos. Lo que se mide ahí es la línea completa y no el array, porque la sangría forma parte del problema: dos pares => que caben en una línea corta se leen bien, y esos mismos dos, empujados más allá del borde de la pantalla, no. Un array de un solo elemento no se mide de esta forma, porque su longitud es la de su único valor y moverlo a otra línea arrastra la misma longitud. Un array con cualquier elemento sin clave puede permanecer en línea sin importar la longitud de la línea.
Esta es una excepción explícita de legibilidad frente a la mecánica controlada por el formateador. Repórtala incluso cuando el formateador no se haya ejecutado. No la apliques a la sintaxis larga array() ni al acceso a arrays.

// Correct
return [
    'success' => false,
    'message' => __('The meter :key does not exist in this project.', ['key' => $key]),
];

['required', 'string', 'max:255'];

['name' => $eventName];

// Incorrect
return ['success' => false, 'message' => __('The meter :key does not exist in this project.', ['key' => $key])];

['name' => $eventName, 'title' => $eventTitle, 'type' => $eventType];

Estilo controlado por el formatter

  • Sigue el formatter configurado del proyecto como referencia de estilo para espacios en blanco y estilo de tokens puramente mecánicos. Esto no autoriza ejecutar el formatter.
  • No reportes el estilo controlado por el formatter como un hallazgo de limpieza manual, a menos que contradiga la salida del formatter o que el formatter no esté disponible.
  • Las estructuras de control en múltiples líneas y los arrays en múltiples líneas son excepciones explícitas: repórtalos bajo sus reglas independientemente de la disponibilidad del formatter. El formatter produce el mismo resultado, así que esto no contradice su salida.
  • No ejecutes Pint, PHP-CS-Fixer ni ningún comando de formatter para corregir o validar estos estándares, a menos que el usuario pida explícitamente el formateo. La ejecución del formatter se gestiona por separado por los responsables del proyecto.
  • Para la concatenación de strings, sigue la configuración activa del formatter de Weblabor Coding Standards. En proyectos que usan weblabormx/weblabor-cs, la concatenación usa un espacio alrededor del operador .: $prefix . '_suffix'.
  • No "limpies" la concatenación quitando esos espacios en proyectos que usan la configuración del formatter de Weblabor.
// Correct
public static function all()
{
    return Plan::all();
}

// Incorrect
public static function all(): Collection
{
    return Plan::all();
}

Excepción: Cuando un método debe conservar un tipo de retorno para cumplir con un contrato de compatibilidad de PHP, debes mantener exactamente el mismo tipo de retorno. Esto incluye sobrescribir un método padre, implementar un método de una interfaz, cumplir un método abstracto o preservar un contrato de framework/biblioteca que la clase declara explícitamente. Quitarlo provoca un error fatal o rompe la integración declarada.
Los métodos de migración de Laravel up(): void y down(): void están permitidos porque siguen las convenciones de stubs/contratos de migraciones de Laravel. No marques esos tipos de retorno como fallo.

// Correct — parent declares BelongsToMany, override must match
public function roles(): BelongsToMany
{
    return $this->belongsToMany(Role::class, 'role_has_permissions');
}

// Incorrect — omitting the type breaks compatibility with the parent
public function roles()
{
    return $this->belongsToMany(Role::class, 'role_has_permissions');
}

// Correct — interface declares array, implementation must match
public function set($model, string $key, $value, array $attributes): array
{
    return [$key => $value];
}

Antes de quitar un tipo de retorno, verifica si el método cumple algún contrato declarado por un padre, interfaz, método abstracto o framework explícito. Si es así, conserva el tipo.

Declaraciones de clases (solo namespaces)

Usa imports para referencias reales a clases. No escribas nombres de clase completamente calificados inline en código ejecutable, tipos, catches ni referencias ::class.
Esta regla solo se aplica a referencias reales de clases completamente calificadas como new \App\Services\Billing\PlanSyncService(), \Throwable $e o \App\Models\User::class. No se aplica a strings entre comillas, nombres de rutas/recursos, claves de config, claves de traducción, llamadas a helpers/funciones, nombres cortos de clases que se resuelven en el namespace actual ni nombres calificados relativos sin barra inicial. Por ejemplo, Commands\OptimizeBladeIndentation::class dentro de namespace App\Console; se resuelve como App\Console\Commands\OptimizeBladeIndentation y no requiere un import.
Tampoco se aplica a archivos dentro de config/. Un archivo de config devuelve un array simple y nombra las clases inline por convención del framework, incluidos los archivos publicados por Laravel y sus paquetes. bootstrap/providers.php es esa misma lista con otro nombre: Laravel le agrega entradas él mismo cuando se genera un provider, y escribe el nombre completamente calificado cada vez, así que un import ahí lo deshace el siguiente make:provider.

// Correct
namespace App\Console;

use App\Services\Billing\PlanSyncService;

return new PlanSyncService();
Commands\OptimizeBladeIndentation::class;
Route::front('Team\\Supplier');

// Incorrect
return new \App\Services\Billing\PlanSyncService();

Evita las constantes

No declares constantes para valores ordinarios del proyecto. Una constante sube el valor hasta el inicio de la clase, lejos del único código al que le importa, y cada lector tiene que viajar hasta ahí para saber qué contiene.
Cuando un solo lugar lee el valor, escríbelo en ese lugar. Un nombre con un único lector repite lo que la línea ya dice.

// Correcto
public function scopeVisible($query)
{
    return $query->whereIn('status', ['draft', 'sent']);
}

// Incorrecto
const VISIBLE_STATUSES = ['draft', 'sent'];

public function scopeVisible($query)
{
    return $query->whereIn('status', self::VISIBLE_STATUSES);
}

Cuando varios lugares lo leen, asígnalo a una variable donde ocurre el trabajo, o devuélvelo desde un método que lo nombre. Cualquiera de las dos mantiene el valor junto a su uso y permite construirlo, filtrarlo o pasarlo como cualquier otro valor, cosa que una constante no puede.

// Correcto
public static function visibleStatuses(): array
{
    return ['draft', 'sent'];
}

Un valor que cambia con el entorno tampoco es una constante: pertenece a config/, y se lee con config().
define() es la misma declaración con otra escritura, y la regla la lee igual.

Sin excepciones explícitas

Un modelo o un observer no deben lanzar ValidationException. La validación pertenece a donde llega la entrada —un componente Livewire, un Form Request, una clase de regla o el servicio que orquesta la escritura— y para cuando un registro llega al modelo, la pregunta de si esto se permite ya está resuelta.
Un modelo que valida tiene dos llamadores a los que no puede servir a la vez: el formulario, que necesita el mensaje en el campo, y el job o el seeder, que solo necesitaban que se escribiera la fila.

// Correcto — el componente decide, el modelo escribe
public function save()
{
    $this->validate();
    $this->order->markPaid();
}

// Incorrecto — dentro del modelo
public function markPaid()
{
    if (! $this->total) {
        throw ValidationException::withMessages(['total' => __('Missing total')]);
    }
}

Replantear un valor predeterminado

No escribas lo que el framework, el package o la clase padre ya hacen. Un override que devuelve el valor que heredó, un argumento al que se le pasa lo que ya tiene por defecto, una propiedad a la que se le asigna el valor con el que nació: cada uno se lee como una decisión cuando no lo es, y quien lee invierte la consulta para descubrir al final que no cambió nada.
Declara el override donde la respuesta sea distinta. Replantear un valor predeterminado también oculta el momento en que deja de serlo: cuando el padre cambia su respuesta, la copia sigue devolviendo la anterior, y nada lo indica.

// Correct — the action inherits hasPermissions() returning true
class GoToCategories extends Action
{
    public function handle()
    {
        return redirect('/app/categories');
    }
}

// Incorrect
class GoToCategories extends Action
{
    public function hasPermissions($object)
    {
        return true;
    }

    public function handle()
    {
        return redirect('/app/categories');
    }
}

Esto no es lo mismo que un no-op que exige un contrato, que es lo que cubre No Empty Methods: ese método tiene que existir, y la etiqueta encima o dentro de él es lo que deja constancia de que el cuerpo vacío es deliberado.

Sin métodos vacíos

Un método con un cuerpo vacío no hace nada y no dice nada. Elimínalo.
El scaffolding es de donde salen estos casos: un observer generado con los siete hooks, una clase creada a partir de una plantilla. Un hook que no existe se comporta exactamente igual que un hook vacío, así que dejarlo obliga al lector a hacer una comprobación y no devuelve nada.
Hay dos casos que no son infracciones, y ambos se reconocen. Un no-op exigido por un padre, una interfaz o un contrato del framework se mantiene, porque eliminarlo rompe la declaración. Y un cuerpo vacío marcado con una etiqueta se mantiene, porque la etiqueta es la parte que faltaba — una etiqueta, como define Comments Earn Their Place, de cuatro palabras o menos, para dejar constancia de que el cuerpo está vacío a propósito sin convertirse en una explicación que envejece.

// Correct
public function down(): void
{
    // Nothing to reverse
}

// Incorrect
public function updated(Order $order)
{
}

Orden de los métodos de ciclo de vida

Los métodos de ciclo de vida corren en un orden fijo, y la clase se lee en ese mismo orden:

  1. __construct()
  2. mount()
  3. todos los demás métodos
  4. render()

render() va al final porque es el desenlace: todo lo de arriba es lo que la clase prepara, y quien abre el archivo quiere ver la preparación antes que la salida. Declararlo antes empuja el trabajo por debajo de lo que lo consume.

Qué puede llamar un punto de entrada

Un punto de entrada llama a un método del modelo. El modelo alcanza al servicio o al job.
Un componente Livewire, un controlador o un comando es donde llega una petición, no donde vive una acción de dominio. Cuando uno de ellos llama a un servicio directamente, lo resuelve del contenedor o despacha un job por su cuenta, la acción deja de tener nombre en el registro al que pertenece, y el siguiente llamador no tiene forma de encontrarla.

// Correcto
$subscription->cancel();

// Incorrecto — desde un componente
app(SubscriptionService::class)->cancel($subscription);
CancelSubscriptionJob::dispatch($subscription);

Un observer no es un punto de entrada: reaccionar a un registro guardado es justo donde llamar a un servicio corresponde. Un modelo puede alcanzar al servicio que le pertenece. Y un servicio que no recibe ningún registro no está ejecutando una acción de dominio, así que llamarlo está bien.

Preferencia de Framework y Helpers

Prefiere Laravel Collections, helpers de Laravel, facades y helpers existentes del proyecto cuando sean claramente más expresivos o más consistentes que PHP nativo para esa necesidad.
Informa esta regla solo cuando una abstracción concreta ya existente encaje mejor en el contexto revisado. PHP nativo está bien para operaciones locales simples como formateo de escalares, manipulación de arrays y lecturas/escrituras puntuales de archivos, salvo que el proyecto ya estandarice exactamente esa necesidad detrás de un helper/facade.

Las utilidades compartidas pertenecen a los helpers

Una función pura que solo transforma sus argumentos —da formato, normaliza o parsea un valor— no pertenece a ninguna clase. Cuando más de un lugar la necesita, colócala en un helper del proyecto.
No la dejes en la clase que casualmente la llamó primero. Un normalizador de teléfonos en el modelo User, un sanitizador de cadenas en un servicio o un formateador de fechas en un controlador son el mismo error: la función describe un valor, no esa clase.
Tampoco la extraigas como un trait. Un trait la convierte en un método de cada clase que lo usa, que es precisamente el resultado que se quiere evitar. Reserva los traits para comportamiento que opera sobre $this.

// Correct — app/helpers.php
function normalize_phone_number($phoneNumber) { ... }

// Incorrect — parked on the first caller
User::normalizePhoneNumber($phoneNumber);

Cuando solo un lugar necesita la utilidad, mantenla privada junto al código que la usa. Muévela a un helper cuando aparezca un segundo punto de uso.

Creación de modelos basada en relaciones

Al crear un modelo relacionado, usa la relación Eloquent propietaria.
Aplica esta regla en cualquier parte de la aplicación PHP/Laravel cuando la instancia del modelo padre ya esté disponible y el registro relacionado pertenezca a esa relación. No se limita al código escrito dentro de la propia clase del modelo.

// Correct
$plan->limits()->create(['limit_key' => 'activities', 'limit_value' => 100]);

// Incorrect
StripeLimit::create(['limitable_type' => Plan::class, 'limitable_id' => $plan->id, ...]);

Construcción de consultas Eloquent

Una consulta se construye de la misma manera sin importar dónde se escriba. Esta regla se aplica a cualquier archivo PHP que componga una cadena de Eloquent: modelos, recursos de Laravel Front como indexQuery(), componentes de Livewire, servicios y jobs por igual.
No uses Model::query() en cadenas ordinarias de Eloquent. Prefiere llamar scopes y métodos del builder directamente sobre el modelo, como Project::active()->orderBy('name')->get(), en lugar de Project::query()->active()->orderBy('name')->get().
Model::query() solo está permitido cuando el código realmente necesita una instancia explícita de Eloquent\Builder, como al pasar un builder a otra API, devolver un builder tipado desde un método, o construir una consulta a partir de una clase de modelo dinámica como $modelClass::query(). No agregues query() solo para iniciar una cadena normal.

// Wrong
return Project::query()
    ->active()
    ->orderBy('name')
    ->get();

// Correct
return Project::active()
    ->orderBy('name')
    ->get();

Para filtros condicionales simples de Eloquent, mantén la consulta como una cadena fluida y usa when() en lugar de crear una variable mutable $query y aplicar filtros mediante bloques if separados.
Usa closures function en formato largo en los callbacks de when(), siguiendo la regla de closures de PHP. Usa una variable $query separada solo cuando la composición condicional sea demasiado compleja para una cadena de when() legible, cuando múltiples ramas devuelvan formas de consulta materialmente diferentes, o cuando una API externa requiera una instancia de builder.

// Wrong
$query = Project::active();

if ($search) {
    $query->where('name', 'like', "%{$search}%");
}

if ($planId) {
    $query->where('plan_id', $planId);
}

return $query->orderBy('name')->get();

// Correct
return Project::active()
    ->when($search, function ($query) use ($search) {
        $query->where('name', 'like', "%{$search}%");
    })
    ->when($planId, function ($query) use ($planId) {
        $query->where('plan_id', $planId);
    })
    ->orderBy('name')
    ->get();

Reasignar la variable es la misma infracción que mutarla. $query = $this->applyFilters($query); dentro de un if igualmente deja la cadena estacionada en una variable, y when() sigue expresándolo en una sola expresión: el callback recibe el builder y puede devolverlo.

// Wrong
$query = $query->with('roles');

if (billingCoreAvailable()) {
    $query = app('billing-core.user-resource')->applyIndexQuery($query);
}

return $query->latest();

// Correct
return $query->with('roles')
    ->when(billingCoreAvailable(), function ($query) {
        return app('billing-core.user-resource')->applyIndexQuery($query);
    })
    ->latest();

El acceso a paquetes opcionales pasa por un helper

A un paquete opcional se accede a través de un único helper del proyecto que devuelve la integración o null. Ese helper es responsable de la comprobación de disponibilidad, así que quien llama no puede resolver la integración sin pasar por él.
No resuelvas un binding del contenedor con clave de cadena en el punto de uso, ni combines una comprobación de disponibilidad separada con la resolución. Cuando la comprobación y la resolución son dos expresiones, quien llama puede comprobar una cosa y resolver otra: comprobar que el paquete principal está instalado mientras resuelve un adaptador que tiene su propia marca de activación. Nada de eso parece incorrecto a simple vista, y precisamente por eso no debería poder expresarse.

// Wrong — the check and the binding can drift apart
if (billingCoreAvailable()) {
    app('billing-core.user-resource')->validateFeatures($object, $request);
}

// Correct — one helper, and the wrong check is not available to write
billingUser()?->validateFeatures($object, $request);
// app/helpers.php
function billingUser(): ?object
{
    if (! billingCoreAvailable() || ! config('billing-core.user_adapter.enabled', true)) {
        return null;
    }
    return app('billing-core.user-resource');
}

Esto trata sobre bindings con clave de cadena para integraciones opcionales. Resolver una clase concreta desde el contenedor, como en app(StripeSyncService::class), es una resolución de dependencias normal y no entra en esta regla. Los alias del framework como app('config') o app('cache.store') tampoco entran en esta regla.
El accesor puede ser un método en lugar de un helper global. Una resolución que está junto a app()->bound('<the same binding>') y puede devolver null ya mantiene unidas la comprobación y la resolución, que es lo que pide la regla. Una prueba que afirma que el contenedor está cableado nombra el binding por la misma razón: el binding es la afirmación.

Uso de la sintaxis de función larga

Usa siempre la sintaxis de función larga, nunca funciones flecha.
Falla esta regla solo cuando el código fuente realmente contiene el token fn ( o fn( para una función flecha. Si todos los closures usan function (...) { ... }, esta regla se cumple.
Si el archivo contiene solo closures function (...) { ... }, esta regla debe cumplirse incluso cuando el closure tiene una sola línea o está encadenado inmediatamente en un pipeline de colección.
No identifiques function ($item) { ... }, each(function ($item) { ... }), map(function ($item) { ... }) ni ningún otro closure de formato largo como una función flecha. Esos son closures de formato largo válidos.
No falles con un razonamiento contradictorio. La presencia o ausencia del token literal fn es la evidencia decisiva para esta regla.

// Correct
->whereHas('plan', function ($query) {
    return $query->where('slug', 'free');
})

// Incorrect
->whereHas('plan', fn($q) => $q->where('slug', 'free'))

Consultar tablas de la aplicación a través de modelos

Las tablas de la aplicación se consultan a través de su model de Eloquent, no con DB::table(). DB::table() solo es aceptable para consultas complejas (joins, SQL crudo, updates, stored procedures) y dentro de migraciones. Si la tabla no tiene model, créalo (o el analyzer te lo advierte). Esta regla corre en archivos PHP, excluyendo migraciones.

Tamaño de Clase

Una clase de producción se mantiene por debajo de 300 líneas. Una clase de más de 300 líneas debe dividirse por propósito: una funcionalidad por trait, servicio o componente hijo. No hay ninguna excepción escrita dentro del archivo: una clase que se mantiene intacta deliberadamente registra el motivo en docs/{project}, porque un comentario sobre la declaración sería una explicación, y Comments Earn Their Place las reporta. Las clases entre 250 y 300 líneas generan una advertencia: se están acercando al límite. Un trait de más de 300 líneas también genera una advertencia: comprueba si su nombre agrupa demasiadas responsabilidades; una revisión puede decidir dejarlo tal como está.
A dónde va el código extraído depende del tipo de clase:

  • Componentes Livewire: traits de funcionalidad en app/Livewire/Traits/, o componentes hijo.
  • Modelos: traits de funcionalidad (HasPlans, HasPrices).
  • Servicios: servicios especializados; el código que no es lógica de dominio (p. ej., etiquetas y colores de botones) sale del servicio.
  • Comandos de consola: delega la mecánica a un servicio; el comando se mantiene ligero.

Los helpers (archivos de funciones independientes), migraciones, tests, configuración y archivos de rutas están exentos: no miden una clase.

Weblabor Logo Desarrollado por Weblabor