No se trata de una adaptación a Laravel de los principios SOLID ni de patrones, etcétera. Aquí encontrarás las mejores prácticas que, por lo general, son ignoradas en proyectos Laravel de la vida real.
Modelos gordos, controladores delgados
La lógica de negocios debe estar en una clase ayudante
No ejecutes consultas en las plantillas blade y utiliza el cargado prematuro (Problema N + 1))
No coloques JS ni CSS en las plantillas blade y no coloques HTML en clases de PHP
Utiliza los archivos de configuración y lenguaje en lugar de texto en el código
Utiliza las herramientas estándar de Laravel aceptadas por la comunidad
Sigue la convención de Laravel para los nombres
Utiliza sintaxis cortas y legibles siempre que sea posible
Utiliza contenedores IoC o fachadas en lugar de new Class
No saques información directamente del archivo .env
Las clases y los métodos deben tener un solo propósito.
Malo:
public function getFullNameAttribute()
{
if (auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified()) {
return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
} else {
return $this->first_name[0] . '. ' . $this->last_name;
}
}
Bueno:
public function getFullNameAttribute()
{
return $this->isVerifiedClient() ? $this->getFullNameLong() : $this->getFullNameShort();
}
public function isVerifiedClient()
{
return auth()->user() && auth()->user()->hasRole('client') && auth()->user()->isVerified();
}
public function getFullNameLong()
{
return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
}
public function getFullNameShort()
{
return $this->first_name[0] . '. ' . $this->last_name;
}
Coloca toda la lógica relacionada a la base de datos en los modelos de Eloquent o en un repositorio de clases si estás utilizando el constructor de consultas o consultas SQL puras.
Malo:
public function index()
{
$clients = Client::verified()
->with(['orders' => function ($q) {
$q->where('created_at', '>', Carbon::today()->subWeek());
}])
->get();
return view('index', ['clients' => $clients]);
}
Bueno:
public function index()
{
return view('index', ['clients' => $this->client->getWithNewOrders()]);
}
class Client extends Model
{
public function getWithNewOrders()
{
return $this->verified()
->with(['orders' => function ($q) {
$q->where('created_at', '>', Carbon::today()->subWeek());
}])
->get();
}
}
Quita las validaciones de los controladores y colócalas en clases Request
Malo:
public function store(Request $request)
{
$request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
'publish_at' => 'nullable|date',
]);
....
}
Bueno:
public function store(PostRequest $request)
{
....
}
class PostRequest extends Request
{
public function rules()
{
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
'publish_at' => 'nullable|date',
];
}
}
Un controlador solo debe tener un propósito, así que mueve la lógica de negocio fuera de los controladores y colócala en clases ayudantes.
Malo:
public function store(Request $request)
{
if ($request->hasFile('image')) {
$request->file('image')->move(public_path('images') . 'temp');
}
....
}
Bueno:
public function store(Request $request)
{
$this->articleService->handleUploadedImage($request->file('image'));
....
}
class ArticleService
{
public function handleUploadedImage($image)
{
if (!is_null($image)) {
$image->move(public_path('images') . 'temp');
}
}
}
Reutiliza código cada vez que puedas. El SRP te ayuda a evitar la duplicación. Reutiliza también las plantillas blade, utiliza eloquent scope, etcétera.
Malo:
public function getActive()
{
return $this->where('verified', 1)->whereNotNull('deleted_at')->get();
}
public function getArticles()
{
return $this->whereHas('user', function ($q) {
$q->where('verified', 1)->whereNotNull('deleted_at');
})->get();
}
Bueno:
public function scopeActive($q)
{
return $q->where('verified', 1)->whereNotNull('deleted_at');
}
public function getActive()
{
return $this->active()->get();
}
public function getArticles()
{
return $this->whereHas('user', function ($q) {
$q->active();
})->get();
}
Prioriza el uso de Eloquent por sobre el constructor de consultas y consultas puras. Prioriza las colecciones sobre los arreglos
Eloquent te permite escribir código legible y mantenible. Eloquent también tiene muy buenas herramientas preconstruidas como los borrados leves, eventos, scopes, etcétera.
Malo:
SELECT *
FROM `articles`
WHERE EXISTS (SELECT *
FROM `users`
WHERE `articles`.`user_id` = `users`.`id`
AND EXISTS (SELECT *
FROM `profiles`
WHERE `profiles`.`user_id` = `users`.`id`)
AND `users`.`deleted_at` IS NULL)
AND `verified` = '1'
AND `active` = '1'
ORDER BY `created_at` DESC
Bueno:
Article::has('user.profile')->verified()->latest()->get();
Malo:
$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->verified = $request->verified;
// Add category to article
$article->category_id = $category->id;
$article->save();
Bueno:
$category->article()->create($request->validated());
Malo (Para 100 usuarios, se ejecutarán 101 consultas):
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
Bueno (Para 100 usuarios, se ejecutarán 2 consultas):
$users = User::with('profile')->get();
...
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach
Comenta tu código, pero prioriza los métodos y nombres de variables descriptivas por sobre los comentarios
Malo:
if (count((array) $builder->getQuery()->joins) > 0)
Mejor:
// Determina si hay alguna unión
if (count((array) $builder->getQuery()->joins) > 0)
Bueno:
if ($this->hasJoins())
Malo:
let article = `{{ json_encode($article) }}`;
Mejor:
<input id="article" type="hidden" value='@json($article)'>
Or
<button class="js-fav-article" data-article='@json($article)'>{{ $article->name }}<button>
En el archivo JavaScript:
let article = $('#article').val();
La mejor ruta es utilizar algún paquete especializado para transferir información de PHP a JS.
Malo:
public function isNormal()
{
return $article->type === 'normal';
}
return back()->with('mensaje', '¡Su artículo ha sido agregado!');
Bueno:
public function isNormal()
{
return $article->type === Article::TYPE_NORMAL;
}
return back()->with('mensaje', __('app.articulo_agregado'));
Prioriza la utilización de funcionalidades integradas y los paquetes de la comunidad en lugar de utilizar paquetes o herramientas de terceros ya que cualquier desarrollador que vaya a trabajar a futuro en tu aplicación necesitará aprender a utilizar nuevas herramientas. También, las probabilidades de recibir ayuda de la comunidad son significativamente más bajas cuando utilizas herramientas o paquetes de terceros. No hagas que tu cliente pague por ello.
Tarea | Herramienta estándar | Herramientas de terceras personas |
---|---|---|
Autorización | Policies | Entrust, Sentinel y otros paquetes |
Compilar assets | Laravel Mix | Grunt, Gulp, paquetes de terceros |
Entorno de desarrollo | Homestead | Docker |
Deployment | Laravel Forge | Deployer y otras soluciones |
Unit testing | PHPUnit, Mockery | Phpspec |
Testeo en el navegador | Laravel Dusk | Codeception |
Base de datos | Eloquent | SQL, Doctrine |
Plantillas | Blade | Twig |
Trabajar con data | Laravel collections | Arreglos |
Validación de formularios | Clases Request | Paquetes de terceros, validación en el controlador |
Autenticación | Integrada | Paquetes de terceros, solución propia |
Autenticación para API's | Laravel Passport | Paquetes oAuth y JWT de terceros |
Creación de API's | Integrado | Dingo API y paquetes similares |
Estructura de la base de datos | Migraciones | Trabajar directamente con la estructura |
Localización | Integrada | Paquetes de terceros |
Interfaces en tiempo real | Laravel Echo, Pusher | Paquetes de terceros y trabajar directamente con WebSockets. |
Generación de información de prueba | Clases Seeder, Fábricas de modelos, Faker | Crear la información manualmente |
Programación de tareas | Laravel Task Scheduler | Scripts y paquetes de terceros |
Base de datos | MySQL, PostgreSQL, SQLite, SQL Server | MongoDB |
Sigue los estándares PSR.
También, sigue la convención aceptada por la comunidad:
Qué | Cómo | Bueno | Malo |
---|---|---|---|
Controlador | singular | ControladorArticulo | |
Ruta | plural | articulos/1 | |
Nombres de rutas | snake_case con notación de puntos | usuarios.mostrar_activos | |
Modelo | singular | Usuario | |
Relaciones hasOne o belongsTo | singular | comentarioArticulo | |
Cualquier otra relación | plural | comentariosArticulo | |
Tabla | plural | comentarios_articulo | |
Tabla de pivote | Nombres de modelos en singular y en orden alfabético | articulo_usuario | |
Columna de tabla | snake_case sin el nombre del modelo | meta_titulo | |
Propiedad de mdelo | snake_case | $model->created_at | |
Clave foránea | Nombre en singular del modelo con el sufijo _id | articulo_id | |
Clave primaria | - | id | |
Migración | - | 2017_01_01_000000_create_articles_table | |
Método | camelCase | traerTodo | |
Método en controlador de recursos | table | guardar | |
Método en clase de pruebas | camelCase | testGuestCannotSeeArticle | |
Variable | camelCase | $articulosConAutor | |
Colección | descriptiva, plural | $usuariosActivos = Usuario::active()->get() | |
Objeto | descriptivo, singular | $usuarioActivo = Usuario::active()->first() | |
Índice de archivos de configuración y lenguaje | snake_case | articulos_habilitados | |
Vistas | kebab-case | show-filtered.blade.php | |
Configuración | snake_case | google_calendar.php | |
Contrato (interface) | adjetivo o sustantivo | Autenticable | |
Trait | adjetivo | Notifiable |
Malo:
$request->session()->get('cart');
$request->input('name');
Bueno:
session('cart');
$request->name;
Más ejemplos
Sintaxis común | Sintaxis corta y legible |
---|---|
Session::get('cart') |
session('cart') |
$request->session()->get('cart') |
session('cart') |
Session::put('cart', $data) |
session(['cart' => $data]) |
$request->input('name'), Request::get('name') |
$request->name, request('name') |
return Redirect::back() |
return back() |
is_null($object->relation) ? null : $object->relation->id |
optional($object->relation)->id |
return view('index')->with('title', $title)->with('client', $client) |
return view('index', compact('title', 'client')) |
$request->has('value') ? $request->value : 'default'; |
$request->get('value', 'default') |
Carbon::now(), Carbon::today() |
now(), today() |
App::make('Class') |
app('Class') |
->where('column', '=', 1) |
->where('column', 1) |
->orderBy('created_at', 'desc') |
->latest() |
->orderBy('age', 'desc') |
->latest('age') |
->orderBy('created_at', 'asc') |
->oldest() |
->select('id', 'name')->get() |
->get(['id', 'name']) |
->first()->name |
->value('name') |
La sintaxis new Class crea acoplamientos estrechos y complica las pruebas. Utiliza contenedores IoC o fachadas en lugar de ello.
Malo:
$user = new User;
$user->create($request->validated());
Bueno:
public function __construct(User $user)
{
$this->user = $user;
}
....
$this->user->create($request->validated());
En lugar de ello, pasa la información a un archivo de configuración y luego utiliza el ayudante config()
para obtener la información en tu aplicación.
Malo:
$apiKey = env('API_KEY');
Bueno:
// config/api.php
'key' => env('API_KEY'),
// Utiliza la información
$apiKey = config('api.key');
Guarda las fechas en los formatos estándares. Utiliza los accessors y mutators para modificar el formato
Malo:
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->toDateString() }}
{{ Carbon::createFromFormat('Y-d-m H-i', $object->ordered_at)->format('m-d') }}
Bueno:
// Modelo
protected $dates = ['ordered_at', 'created_at', 'updated_at'];
public function getSomeDateAttribute($date)
{
return $date->format('m-d');
}
// Vista
{{ $object->ordered_at->toDateString() }}
{{ $object->ordered_at->some_date }}
No coloques ningún tipo de lógica en los archivos de rutas.
Minimiza el uso de PHP vanilla en las plantillas blade.