Código organizado, elegante e de fácil manutenção: dicas de Laravel

Rodrigo Barboza
sysvale
Published in
14 min readJun 1, 2023

O Laravel é muito mais do que apenas um framework de desenvolvimento de aplicações web em PHP. Ele se destaca pela sua robustez e pela ampla gama de ferramentas que oferece, permitindo que os desenvolvedores escrevam códigos de forma moderna, elegante e seguindo as melhores práticas da programação.

O objetivo deste artigo é de auxiliar principalmente os desenvolvedores iniciantes, fornecendo dicas valiosas para que eles possam explorar mais o potencial do Laravel e aproveitar ao máximo suas ferramentas, fazendo com que olhem para o Laravel com uma nova perspectiva e se sintam confiantes ao utilizá-lo de maneira adequada.

Ao longo deste texto, vou compartilhar conhecimentos e experiências para que você possa aprimorar suas habilidades no desenvolvimento com Laravel. Abordaremos algumas dicas, passando por recursos essenciais do framework.

Sabemos o quão apaixonados somos pelo Laravel e entendemos o impacto que ele tem na nossa produtividade e na qualidade dos nossos projetos. Portanto, vamos abordar dicas que vão impulsionar sua produtividade, melhorar a eficiência do seu código e elevar a qualidade das suas aplicações Laravel.

Dica 1: Utilize arquivos de rotas separados

Quem já utilizou o Laravel seja para criar uma aplicação web ou api, sabe que facilmente um arquivo de rotas começa a ficar muito grande, e isso acaba tornando a manutenção e o trabalho mais difícil.

Um exemplo simples é: quem nunca criou uma rota e quando foi testar a rota ela estava dando um problema que não fazia sentido nenhum e ao final você se dá conta que o problema é que existia uma rota que tinha sido declarada previamente, que sua estrutura era muito parecida ou de nome igual, e a requisição ao invés de cair na sua rota estava caindo em outra rota que não tinha nada a ver? Pois é, essa é uma das situações que um arquivo lotado de rotas pode ocasionar. Para isso o Laravel nos dá a possibilidade de separar nossas rotas em arquivos diferentes.

E como podemos fazer isso? Temos duas formas:

1 — Adicionando os caminhos dos arquivos de rota no app/Providers/RouteServiceProvider.php :

No método boot é possível encontrar um trecho de código da seguinte forma:

Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));

Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));

Se você quiser adicionar múltiplos arquivos de rota nas rotas “web”, você pode passar no método group uma closure:

Route::middleware('web')
->namespace($this->namespace)
->group(function() {
require base_path('routes/users.php');
require base_path('routes/products.php');
});

2 — Adicionando os caminhos dos arquivos de rota no próprio web.php ou api.php:

Este método é o mais simples, basta apenas adicionar o seguinte trecho ao final do arquivo:

Route::prefix('users')->group(base_path('routes/users'));
Route::prefix('products')->group(base_path('routes/products'));

Neste caso é necessário declarar um prefixo antes, com isso todas as rotas de users virão com users/{path da rota}, da mesma forma que o nome da rota também receberá o prefixo, por exemplo:

Se eu tenho a rota em routes/users.php:

// users.php
Route::get('posts', [PostController::class, 'index'])->name('index');

Ao adicionar no arquivo de rotas com o prefixo users eu terei:

  • path: users/posts
  • name: users.index

Dica 2: Utilize form request ao invés de validação no controller

O Form Request no Laravel é uma funcionalidade que permite validar os dados recebidos de uma requisição HTTP antes que eles sejam processados pelo controlador. É uma forma de centralizar e organizar as regras de validação em uma classe separada, conhecida como classe de Form Request.

Normalmente, ao lidar com formulários ou requisições de entrada de dados, é necessário validar os dados recebidos para garantir que eles atendam aos critérios desejados. O Laravel fornece várias maneiras de realizar essa validação, e uma delas é através do Form Request.

Utilizar o form request traz uma série de vantagens, como: a separação de responsabilidades, reutilização das regras de validação, centralização das regras de validação, validação automática, melhor segurança e facilidade de personalização.

Não é intuito deste artigo explicar em detalhes o funcionamento do form request, porque ele possui muitas ferramentas e essas informações estão disponíveis na documentação. Dito isto vamos trazer um exemplo simples de utilização:

Vamos supor que você tenha uma página de cadastro de usuários, e você enviará informações do usuário ao qual se deseja cadastrar para o back através de uma requisição POST, estes dados chegarão no Laravel através do Request, porém sem o form request eles chegarão ao controller sem validação alguma, normalmente faríamos:

public function store(Request $request)
{
$validated_data = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|string|min:6',
]);

if ($validated_data) {
// ...
}
}

Apesar de funcional, esta não é uma boa prática e podemos melhorar este método para que nosso controller fique bem mais enxuto, com o comando: php artisan make:request StoreUserRequest podemos criar uma classe FormRequest, que é uma classe semelhante a esta:

<?php

namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends FormRequest
{
public function authorize()
{
return true;
// Defina a lógica de autorização de acordo com suas necessidades
}

public function rules()
{
return [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|string|min:6',
];
}
}

Os método authorize e rules vêm por padrão ao criar um form request com o artisan. O método authorize pode ser utilizado para verificar por exemplo uma permissão para o usuário autenticado, se ele não tiver autorização o método retornará false, e a requisição retornará HTTP 403 Forbidden (Acesso Negado). Já o rules é onde você define as regras (para mais detalhes consulte a documentação).

Com isso, no nosso exemplo agora teremos:

public function store(StoreUserRequest $request)
{
// se chegou aqui, os dados foram validados corretamente
}

Desta forma nós teremos um controller muito mais limpo e passamos a responsabilidade de validação para uma classe que só vai fazer a validação dos dados do usuário.

Estendendo um pouco mais nosso exemplo e melhorando ainda mais este método, podemos utilizar a técnica do model bind e adicionar os tipos dos parâmetros e do retorno:

public function store(StoreUserRequest $request, User $user): UserResource
{
$new_user = $user->create($request->validated());
return new UserResource($new_user);
// o resource será explicado na próxima dica
}

Dica 3: Utilize resources

Em alguns casos é comum não enviar por exemplo o id de um registro porque para o front ele não interessa, ou é comum fazer uma transformação em algum dado no front quando a gente poderia fazer essa transformação no back e deixar essa responsabilidade para apenas uma classe.

No Laravel, uma resource (recurso) é uma classe que permite estruturar e formatar os dados de um modelo para serem enviados como resposta em uma API ou em uma representação específica, como JSON. As resources são usadas para transformar os dados do modelo em um formato adequado para a saída.

Quando usar o resource? Em cenários em que você precise transformar e formatar os dados do seu modelo para serem retornados como resposta em uma API ou em uma representação específica.

Qual tipo de resource eu devo utilizar?

Você quer retornar do controller um registro ou um conjunto de registros?

  • Retornando apenas um registro: com o seguinte comando artisan é possível criar um resource: php artisan make:resource UserStoreResource

A classe gerada será semelhante a esta:

<?php

namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;

class UserStoreResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

No método toArray é possível especificar os atributos que quer retornar, e também é possível alterar o nome deles se for o interesse. Nessa etapa também é possível fazer transformações nos dados como por exemplo no nome do usuário:

return [
'id' => $this->id,
'name' => strtoupper($this->name),
'email' => $this->email,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];

Utilizando o resource criado no controller (lembre-se de importar a classe):

public function store(UserStoreRequest $request): UserStoreResource
{
$user = User::create($request->validated());
return new UserStoreResource($user);
}

// ou de forma alternativa se preferir, você pode utilizar o método
// statico da classe JsonResource, o make()
return UserStoreResource::make($user);
  • Retornando uma coleção de registros:

Para retornar uma coleção de registro, ao invés de utilizar o resource que estende a classe JsonResponse, utilizaremos o resource collection que estende a classe ResourceCollection. Podemos criar uma resource collection da mesma forma pelo artisan: php artisan make:resource UserCollection

O artisan é inteligente o suficiente para entender (se você nomear suas classes seguindo as convenções do Laravel, neste caso se no nome da classe tiver “Collection”), que você está criando um resource do tipo collection e já adiciona a classe ResourceCollection ao invés de JsonResouce. Se por acaso você não queira seguir a convenção, você pode adicionar uma flag ao comando artisan: php artisan make:resource User --collection

Com isso você terá uma classe semelhante a esta:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
public function toArray($request)
{
return [
'data' => $this->collection->map(function ($user) {
return [
'nome' => $user->nome,
'email' => $user->email,
];
}),
];
}
}

No contexto da classe UserCollection, $this->collection refere-se à coleção de recursos (usuários) que está sendo manipulada pela resource. É como se eu tivesse $user = User::all() e fizesse um map em $user→map(). Os métodos mais utilizados no resource collection são o map e o transform, a diferença principal entre map() e transform() é a forma como eles afetam os itens da coleção original:

  1. map(): O método map() cria uma nova coleção a partir dos itens da coleção original, aplicando uma transformação a cada item. A coleção original permanece inalterada. O resultado é uma nova coleção com os itens transformados.
  2. transform(): O método transform() modifica diretamente os itens da coleção original. Ele aplica uma transformação a cada item, modificando-os na própria coleção original. Não é criada uma nova coleção separada.

Portanto, a escolha entre map() e transform() depende do seu caso de uso específico e dos requisitos do seu código. Se você precisa modificar a coleção original diretamente, use transform(). Se você precisa retornar uma nova coleção com os itens transformados, sem modificar a coleção original, use map().

Como utilizar esse resource collection no controller:

public function store(): UserCollection
{
$users = User::all();
return new UserCollection($users);
}

// ou de maneira alternativa, também utilizando o método statico make
return UserCollection::make($users);

Mais um exemplo interessante para o dia dia: vamos supor que agora quero retornar todos os meus usuários com seus respectivos endereços, onde um usuário possui N endereços e um endereço pertence a um usuário. Vamos supor também que temos a seguinte query:

// pega todos usuários do tipo colaborador com seus respectivos endereços
$collaborators = User::whereType('collaborator')->with('address')->get();

Esta query vai me retornar todos os usuários colaboradores com seus respectivos endereços (o with é da técnica eager loading que veremos na dica 6).

Agora criando o resource:

<?php

namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
use App\Http\Resources\AddressResource;

class UserCollection extends ResourceCollection
{
public function toArray($request)
{
return [
'data' => $this->collection->map(function ($collaborator) {
return [
'nome' => $collaborator->nome,
'email' => $collaborator->email,
'addresses' => AddressResource::collection($collaborator->address),
];
}),
];
}
}

Como um colaborador pode ter mais de um endereço, eu posso adicionar uma chave “addresses” e no seu valor adicionar um resource de endereço. Note que ao invés de utilizar um AddressCollection eu utilizei um AddressResource, porque a classe JsonResponse também possui um método estático chamado collection que pode ser utilizado para criar uma collection a partir de um resource. Assim, na minha saída eu teria algo assim:

"data": [
{
"nome":"fulaninho 1",
"email":"fulanilho1@mail.com",
"addresses":[
{
"rua":"tal",
"numero":123
}
]
},
{
"nome":"fulaninho 2",
"email":"fulanilho2@mail.com",
"addresses":[
{
"rua":"tal 2",
"numero":321
},
{
"rua":"tal 3",
"numero":555
}
]
}
]

Os resources do Laravel são uma ferramenta muito poderosa e você pode e deve utilizar em seus projetos. Existem várias coisas que podemos fazer em uma classe resource, portanto é sempre muito bem vindo explorar essas funcionalidades na documentação.

Dica 4: Utilize as collections

Se você já teve alguma experiência com PHP, especialmente sem o uso de grandes frameworks como o Laravel, é provável que esteja familiarizado com os métodos in_array, array_reduce, array_map, array_filter, array_keys, entre outros. Essas funções são nativas do PHP e não há problema em utilizá-las. No entanto, quando estamos trabalhando com o Laravel, que é uma framework robusta e completa, temos acesso a uma ferramenta poderosa chamada collections que são estruturas de dados poderosas que fornecem uma variedade de métodos para manipular e trabalhar com conjuntos de dados de forma concisa e expressiva. Então se o Laravel nos dá essa ferramenta, porque não utilizar, não é mesmo? Vamos explorar e aproveitar ao máximo das collections do Laravel, pois elas são incrivelmente úteis, e além de deixarem nosso código mais elegante, deixa com mais carinha de Laravel!

Uma vantagem incrível do Laravel é a ampla gama de métodos disponíveis nas collections. Embora não seja possível abordar todos os métodos aqui, é altamente recomendado explorar a documentação oficial do Laravel para descobrir a variedade de métodos que podem auxiliá-lo no desenvolvimento. Com certeza, você encontrará métodos úteis que atendam às suas necessidades.

Dica 5: Utilize injeção de dependência

No contexto do Laravel, a injeção de dependência refere-se à prática de fornecer as dependências necessárias para uma classe ou objeto, em vez de criar ou gerenciá-las internamente. Isso é feito por meio do mecanismo de injeção de dependência fornecido pelo contêiner de serviços do Laravel.

Ao usar a injeção de dependência, você pode aproveitar vários benefícios, como a modularidade, reutilização de código, testabilidade e flexibilidade. Ele ajuda a reduzir o acoplamento entre as classes e torna o código mais extensível e fácil de manter.

Exemplo de uso da injeção de dependência:

<?php

class ActivityController extends Controller
{
// sem injenção de dependência:

public function index()
{
$activity_service = new ActivityService();
// ...
}

// com injeção de dependência
public function index(ActivityService $activity_service):
{
/// ...
}
}

Embora as duas formas sejam corretas e funcionais, utilizar a injeção de dependência vai nos dá algumas vantagens como: desacoplamento de código, reutilização de código, testabilidade, manutenção simplificada, extensibilidade, código mais limpo e legível. Vamos para mais um exemplo:

<?php

class ActivityController extends Controller
{
public function index()
{
$activity_repository = new ActivityRepository();
return $activity_repository->get();
}

public function store(Request $request)
{
$activity_repository = new ActivityRepository();
return $activity_repository->create($request->all());
}
}

Para melhorar este código eu posso utilizar um construtor:

class ActivityController extends Controller
{
private ActivityRepository $activity_repository;

public function __construct()
{
$this->activity_repository = new ActivityRepository();
}

public function index()
{
return $this->activity_repository->get();
}

public function store(Request $request)
{
return $this->activity_repository->create($request->all());
}
}

Assim já está bem bacana, mas pode ficar ainda melhor! Por isso utilizaremos a injeção de dependência:

class ActivityController extends Controller
{
private ActivityRepository $activity_repository;

public function __construct(ActivityRepository $activity_repository)
{
$this->activity_repository = $activity_repository;
}

public function index()
{
return $this->activity_repository->get();
}

public function store(Request $request)
{
return $this->activity_repository->create($request->all());
}
}

A dependência ActivityRepository é injetada no construtor através da injeção de dependência. Isso permite que a instância da classe ActivityRepository seja fornecida de fora, em vez de criar uma nova instância dentro do construtor. A injeção de dependência é preferível, pois torna o código mais flexível, testável e reutilizável.

Com essas melhorias, a classe ActivityController agora depende da abstração ActivityRepository em vez de uma implementação concreta específica. Isso permite que você substitua facilmente a implementação do repositório por outra, se necessário, e também torna a classe mais fácil de testar, pois você pode injetar um mock do repositório nos testes.

Extra: deixando nossa classe exemplo mais “gatinha” com a inicialização da propriedade no construtor (disponível a partir do php 8):

class ActivityController extends Controller
{
public function __construct(
private ActivityRepository $activity_repository
){}

public function index()
{
return $this->activity_repository->get();
}

public function store(Request $request)
{
return $this->activity_repository->create($request->all());
}
}

Dica 6: Evite a query N + 1 com Eager Loading

Eager loading é um conceito do Laravel que se refere ao carregamento antecipado de relacionamentos entre modelos. No Laravel, quando você recupera um model que possui relacionamentos definidos, o framework carrega automaticamente os dados dos relacionamentos quando você acessa esses relacionamentos.

No entanto, quando você tem um cenário em que precisa recuperar vários modelos com seus relacionamentos, o carregamento automático de relacionamentos pode resultar em um problema conhecido como “N+1 queries”. Isso significa que o Laravel executará uma consulta para recuperar os models principais e em seguida, uma consulta adicional para cada modelo relacionado, resultando em um número excessivo de consultas ao banco de dados, por exemplo:

$users = User::all();

foreach ($users as $user) {
echo "Usuário: " . $user->name . "\\n";

// consulta adicional para cada post de cada usuário
foreach ($user->posts as $post) {
echo "Post: " . $post->title . "\\n";
}
}

Para resolver esse problema e melhorar o desempenho, o Laravel oferece o eager loading. Com o eager loading, você pode carregar antecipadamente os dados dos relacionamentos associados aos modelos principais em uma única consulta, em vez de executar consultas separadas para cada relacionamento.

$users = User::with('posts')->get();

foreach ($users as $user) {
echo "Usuário: " . $user->name . "\\n";

foreach ($user->posts as $post) {
echo "Post: " . $post->title . "\\n";
}
}

Nesse exemplo, usamos o método with('posts') para carregar antecipadamente os posts relacionados aos usuários em uma única consulta. Dessa forma, o Laravel executará apenas duas consultas: uma para recuperar os usuários e outra para recuperar todos os posts relacionados. Isso evita o problema de "N+1 queries" e melhora o desempenho da aplicação.

Com o with, também é possível carregar vários relacionamentos antecipadamente:

$users = User::with(['posts', 'comments', 'profile'])->get();

O uso do eager loading é especialmente útil quando você tem uma grande quantidade de dados e relacionamentos complexos. Ele permite que você carregue antecipadamente os dados relacionados necessários de forma eficiente, reduzindo o número de consultas e melhorando o desempenho geral da aplicação.

Dica 7: Utilize o método chunk para lidar com grandes volumes de dados

O método chunk() do Laravel é utilizado para processar um grande conjunto de resultados de consulta em partes menores (chunks) em vez de recuperar todos os registros de uma só vez. Ele é útil quando você precisa realizar operações em lotes em grandes conjuntos de dados, evitando a sobrecarga de memória e melhorando o desempenho da sua aplicação.

Exemplo:

User::query()->chunk(100, function ($users) {
// Processar os resultados da consulta
});

O primeiro parâmetro do método chunk é a quantidade de registros que quero pegar por lote, o segundo parâmetro é uma closure que recebe como parâmetro uma instância do conjunto de resultados (users) que contém os registros a serem processados em cada iteração.

Dentro da função de retorno do chunk(), você pode realizar qualquer operação que precise ser feita nos registros retornados. Por exemplo, você pode atualizar os registros, executar cálculos complexos ou realizar outras operações de processamento em lote.

Dica 8: Reduza o consumo de memória com o método cursor

O método cursor() do Laravel é usado para recuperar registros do banco de dados de forma eficiente e em um modo de "lazy loading" (carregamento sob demanda). Em vez de retornar todos os registros de uma só vez, o cursor() permite que você itere sobre os registros um por um, mantendo um consumo mínimo de memória.

Exemplo:

foreach (User::cursor() as $user) {
// Processar cada usuário individualmente
echo "Usuário: " . $user->name . "\\n";
}

Neste exemplo, estamos usando o cursor() para iterar sobre cada registro da tabela de usuários. A cada iteração, um único registro é recuperado do banco de dados e o loop é executado. Isso permite que você trabalhe com os registros de forma eficiente, sem carregar todos eles na memória de uma vez.

Certo, e qual a diferença de fazer esse cursor do exemplo a cima e fazer:

User::query()->chunk(1, function ($user) {
echo "Usuário: " . $user->name . "\\n";
});

O resultado seria o mesmo, porém, o cursor() é mais apropriado quando você precisa iterar sobre registros individualmente com o mínimo consumo de memória, enquanto o chunk() é mais adequado para operações em lotes, mesmo que o tamanho do lote seja pequeno.

Este artigo apresenta algumas dicas e exemplos que tiveram um grande impacto na minha experiência como desenvolvedor Laravel. Embora eu tenha abordado apenas alguns pontos, tentei ser breve e objetivo. Gostaria de explorar mais algumas das dicas, mas o artigo ficaria muito grande. Espero que essas dicas sejam úteis para você e que você comece a enxergar o Laravel de uma maneira diferente, assim como aconteceu comigo. Vamos expandir nosso conhecimento e nos desafiar a explorar novas ferramentas e abordagens, abandonando o modo automático de programar. Acredito que isso nos ajudará a evoluir como desenvolvedores.

Sobre o autor: Meu nome é Rodrigo Barboza, sou estudante de Engenharia de Computação na Universidade Federal do Vale do São Francisco e desenvolvedor na Sysvale Softgroup.

--

--