Ctrl+C, Ctrl+V, e mais uma vez
Toda aplicação Laravel tem essa linha. Geralmente várias cópias dela:
if ($post->user_id !== auth()->id()) {
abort(403);
}
Ela nasce inocente, no update() do controller. Aí você precisa da mesma checagem no destroy(). Copia. Depois no edit(). Copia. Aí o Blade precisa esconder o botão "Editar" de quem não pode. Copia de novo, agora em outro formato.
Quando você percebe, a regra "quem pode mexer nesse post" está espalhada em oito, dez, doze lugares. Nenhum deles sabe da existência dos outros.
E aí chega a segunda-feira em que o cliente pede: "admin também precisa poder editar qualquer post". Você já sentiu o frio na barriga? Porque agora você precisa achar todas as cópias. E vai esquecer uma. Sempre esquece uma.
O problema não é o if, é o endereço dele
Olha o controller depois de umas três rodadas de "só mais uma regrinha":
public function update(Request $request, Post $post)
{
if ($post->user_id !== auth()->id() && ! auth()->user()->isAdmin()) {
abort(403);
}
if ($post->publicado_em && $post->publicado_em->diffInHours(now()) > 24) {
abort(403, 'Posts publicados há mais de 24h não podem ser editados.');
}
$post->update($request->validated());
return back();
}
Metade do método é autorização. E essa mesma metade precisa existir, idêntica, no destroy(), no edit() e na view.
Repara no que aconteceu: uma regra de negócio de verdade — "quem, e em que condições, pode alterar um post" — virou detalhe de implementação de um método HTTP. Ela não tem casa própria. Mora de favor em todo canto.
O conceito: autorização merece um endereço
Policy é uma classe que concentra as regras de autorização de um model. Um arquivo, um assunto: "o que cada usuário pode fazer com um Post".
O controller volta a fazer o que deveria: receber requisição, chamar o serviço, devolver resposta. E quando o cliente mudar de ideia sobre quem pode editar o quê, você abre um arquivo.
A solução: cria a policy e esquece o if
php artisan make:policy PostPolicy --model=Post
O --model já gera o esqueleto com os métodos padrão (viewAny, view, create, update, delete...). Preenche o que interessa:
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
public function update(User $user, Post $post): bool
{
if ($user->isAdmin()) {
return true;
}
if ($post->user_id !== $user->id) {
return false;
}
// autor só edita nas primeiras 24h após publicar
return ! $post->publicado_em
|| $post->publicado_em->diffInHours(now()) <= 24;
}
public function delete(User $user, Post $post): bool
{
return $user->isAdmin() || $post->user_id === $user->id;
}
}
Não precisa registrar nada: o Laravel descobre a policy sozinho pela convenção de nomes (Post → PostPolicy). Se sua estrutura de pastas fugir do padrão, aí sim você registra manualmente com Gate::policy(Post::class, PostPolicy::class) no AppServiceProvider.
E o controller emagrece:
public function update(Request $request, Post $post)
{
Gate::authorize('update', $post);
$post->update($request->validated());
return back();
}
Uma linha. Se não puder, o Laravel lança AuthorizationException e devolve 403 sem você escrever nada.
Como usar na prática
No Blade, pra esconder o botão de quem não pode clicar:
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Editar</a>
@endcan
Mesma regra, mesmo arquivo. Zero chance de a view discordar do controller.
Na rota, com o middleware can, quando o controller inteiro depende da permissão:
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update,post');
Em qualquer lugar do código, com o próprio usuário:
if ($user->can('delete', $post)) {
// ...
}
Funciona em Job, em Livewire, em comando de console. A regra é uma só, e todo mundo pergunta pra ela.
As pegadinhas (essas pegam todo mundo)
1. O $this->authorize() não existe mais por padrão. Do Laravel 11 pra frente, o controller base do skeleton não traz mais o trait AuthorizesRequests. Se você seguiu um tutorial antigo e tomou "Call to undefined method", é isso. Ou usa Gate::authorize('update', $post), ou adiciona o trait no seu controller base:
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
abstract class Controller
{
use AuthorizesRequests;
}
2. Métodos sem model precisam do nome da classe. create e viewAny não recebem instância — afinal, o post ainda não existe. Então você passa a classe:
Gate::authorize('create', Post::class);
Esquecer disso e passar $post num create é campeão de confusão.
3. O before() é poderoso demais. Dá pra liberar geral pro admin de uma vez:
public function before(User $user, string $ability): ?bool
{
return $user->isAdmin() ? true : null;
}
Só que atenção no retorno: true autoriza, false nega tudo e nem chama o resto da policy, e null deixa o fluxo continuar. Retornar false sem querer no before() derruba a policy inteira — e é um bug bem chato de achar, porque tudo simplesmente vira 403.
4. Visitante deslogado nem chega na policy. Por padrão o Laravel nega antes de executar o método. Se um post público pode ser visto por qualquer um, tipe o parâmetro como opcional:
public function view(?User $user, Post $post): bool
{
return $post->publicado_em !== null || $user?->id === $post->user_id;
}
Bônus: mensagem de erro que ajuda
Retornar false dá um 403 genérico. Às vezes você quer explicar o motivo:
use Illuminate\Auth\Access\Response;
public function update(User $user, Post $post): Response
{
return $post->user_id === $user->id
? Response::allow()
: Response::deny('Você só pode editar seus próprios posts.');
}
A mensagem chega no front, e o usuário para de abrir chamado perguntando por que o botão não funciona.
Antes de você fechar a aba
Dá um grep por abort(403) no seu projeto agora. Cada resultado é uma regra de negócio morando no lugar errado, esperando o dia em que vai divergir das outras cópias.
A boa notícia: migrar é rápido. Cria a policy, move o if pra lá, e vai trocando os controllers um por um. Dá pra fazer em partes, sem parar o projeto.
E aí, seu projeto tem policy ou tá no esquema if copiado? Conta nos comentários — e se você já caiu na do before() retornando false, quero muito saber quanto tempo levou pra descobrir. 😄
Top comments (0)