G1 - Guía de estándar de codificación en laravel
Objetivo(s)#
Generar una guía que permita entender con ejemplos los estandartes a utilizar en la codificación de laravel
Pre-requisitos#
N/A
Pasos a seguir#
Tabla de contenido#
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
Principio de propósito único#
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;}Modelos gordos, controladores delgados#
Coloca toda la lógica relacionada a la base de datos en los modelos de Eloquent o en una clase Repositorio 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(); }}Validación#
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', ]; }}La lógica de negocios debe estar en una clase de servicio#
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 de servicio.
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'); } }}No te repitas (DRY)#
Reutiliza código cada vez que puedas. El SRP (Principio de Propósito Único) te ayuda a evitar la duplicación. Reutiliza también las plantillas Blade, utiliza scopes de Eloquent, etcétera.
Como estándar el uso de scopes para funciones específicas o de grado mayor seguido de las de menor
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` DESCBueno:
Article::has('user.profile')->verified()->latest()->get();Asignación en masa#
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());No ejecutes consultas en las plantillas Blade y utiliza el cargado prematuro (Problema N + 1)#
Malo (Para 100 usuarios, se ejecutarán 101 consultas):
@foreach (User::all() as $user) {{ $user->profile->name }}@endforeachBueno (Para 100 usuarios, se ejecutarán 2 consultas):
$users = User::with('profile')->get();
...
@foreach ($users as $user) {{ $user->profile->name }}@endforeachComenta 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ónif (count((array) $builder->getQuery()->joins) > 0)Bueno:
if ($this->hasJoins())No coloques JS ni CSS en las plantillas Blade y no coloques HTML en clases de PHP#
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 un archivo JavaScript:
let article = $('#article').val();La mejor ruta es utilizar algún paquete especializado para transferir información de PHP a JS.
Utiliza las herramientas estándar de Laravel aceptadas por la comunidad#
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 |
| 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 |
| Estructura de la base de datos | Migraciones | Trabajar directamente con la estructura |
| Generación de información de prueba | Clases Seeder, Fábricas de modelos, Faker | Crear la información manualmente |
| Base de datos | MySQL, PostgreSQL, SQLite, SQL Server | MongoDB |
Sigue la convención de Laravel para los nombres#
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 modelo | 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 | AuthenticationInterface | |
| Trait | adjetivo | Notifiable |
Sintaxis recomendadas
| 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') |
Utiliza contenedores IoC o fachadas en lugar de new Class#
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());No saques información directamente del archivo .env#
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:
// Modeloprotected $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 }}Otras buenas prácticas#
No coloques ningún tipo de lógica en los archivos de rutas.
Minimiza el uso de PHP vanilla en las plantillas Blade.
Autores#
- Juan Manuel Amador Perez Flores
- Sebastian Gonzalez Tafoya
- José Carlos Pacheco Sánchez
Auditoría#
- Adolfo Acosta Castro
Bitácora de cambios
Versión 1.0#
- Se creó la guía