Na Parte 5A, definimos a estrutura do banco de dados e os arquivos essenciais de modelagem (Model, Migration e Factory) para a entidade Task. Agora, na Parte 5B, vamos finalizar o CRUD Laravel 12: criando as rotas, o TaskController, as views com Blade e Tailwind CSS, e conectando tudo com os métodos de armazenamento, edição e exclusão.
Este é o momento onde o sistema começa a ganhar forma visual e funcional.

1. Criando o Controller de Tarefas
Use o Artisan para gerar um controller com os métodos básicos para o CRUD:
php artisan make:controller TaskController --resource
Este comando cria o TaskController com os métodos:
index()create()store()show()edit()update()destroy()
Abra o arquivo app/Http/Controllers/TaskController.php e edite conforme abaixo:
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class TaskController extends Controller
{
use AuthorizesRequests;
public function index()
{
$tasks = Task::where('user_id', Auth::id())->latest()->get();
return view('tasks.index', compact('tasks'));
}
public function create()
{
return view('tasks.create');
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
]);
Task::create([
'user_id' => Auth::id(),
'title' => $request->title,
'description' => $request->description,
'is_completed' => false,
]);
return redirect()->route('tasks.index')->with('success', 'Task created successfully!');
}
public function edit(Task $task)
{
$this->authorize('update', $task);
return view('tasks.edit', compact('task'));
}
public function update(Request $request, Task $task)
{
$this->authorize('update', $task);
$request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'is_completed' => 'nullable|boolean',
]);
$task->update([
'title' => $request->title,
'description' => $request->description,
'is_completed' => $request->has('is_completed'),
]);
return redirect()->route('tasks.index')->with('success', 'Task updated successfully!');
}
public function destroy(Task $task)
{
$this->authorize('delete', $task);
$task->delete();
return redirect()->route('tasks.index')->with('success', 'Task deleted!');
}
}
2. Protegendo com Policies
Nós precisamos criar de uma política para a Task, pois queremos garantir a segurança e a integridade dos dados. O objetivo é que apenas o usuário que criou uma tarefa possa editá-la ou excluí-la. A política da Task será o local centralizado onde definiremos essa regra: ela receberá o usuário logado e a tarefa em questão, e então decidirá se o ID do usuário logado corresponde ao user_id da tarefa. Se não corresponder, a política dirá “não”, e o Laravel automaticamente dispara o erro 403, protegendo sua aplicação de acessos indevidos.
a) Criar a TaskPolicy
Primeiro, você precisa criar um arquivo de política para a sua tarefa. Abra o terminal na raiz do seu projeto Laravel e execute o seguinte comando:
php artisan make:policy TaskPolicy --model=Task
Este comando irá criar um novo arquivo em app/Policies/TaskPolicy.php.
b) Registrar a TaskPolicy no AuthServiceProvider
Em seguida, você precisa registrar essa nova política para que o Laravel saiba que ela existe e a associe ao seu modelo Task.
- Abra o arquivo:
app/Providers/AuthServiceProvider.php - Dentro da classe
AuthServiceProvider, você verá uma propriedade protegida$policies(caso não exista, crie). Adicione a suaTaskeTaskPolicya este array. Certifique-se de adicionar osusestatements no topo do arquivo também. - Seu
AuthServiceProvider.phpdeve ficar parecido com isto:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Models\Task;
use App\Policies\TaskPolicy;
class AppServiceProvider extends ServiceProvider
{
/**
* The model to policy mappings for the application.
*
* @var array<class-string, class-string>
*/
protected $policies = [
Task::class => TaskPolicy::class,
];
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}
Passo 3: Implementar a Lógica na TaskPolicy
Agora, você precisa adicionar a lógica de autorização dentro do arquivo app/Policies/TaskPolicy.php. Este arquivo já deve ter métodos “esqueletos” criados pelo comando make:policy.
A lógica principal para update e delete será verificar se o id do usuário logado ($user->id) é o mesmo que o user_id da tarefa ($task->user_id).
Substitua o conteúdo do seu app/Policies/TaskPolicy.php por este:
<?php
namespace App\Policies;
use App\Models\User;
use App\Models\Task; // Importe o modelo Task
use Illuminate\Auth\Access\Response;
class TaskPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
// Qualquer usuário autenticado pode ver a lista de tarefas
return $user !== null;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Task $task): bool
{
// O usuário pode ver a tarefa se for o proprietário dela
return $user->id === $task->user_id;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
// Qualquer usuário autenticado pode criar tarefas
return $user !== null;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Task $task): bool
{
// O usuário pode atualizar a tarefa se for o proprietário dela
return $user->id === $task->user_id;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Task $task): bool
{
// O usuário pode deletar a tarefa se for o proprietário dela
return $user->id === $task->user_id;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Task $task): bool
{
// O usuário pode restaurar a tarefa se for o proprietário dela
return $user->id === $task->user_id;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Task $task): bool
{
// O usuário pode forçar a exclusão da tarefa se for o proprietário dela
return $user->id === $task->user_id;
}
}
3. Definindo as Rotas no web.php
Agora, registre o recurso de rotas no seu routes/web.php, dentro do grupo protegido:
use App\Http\Controllers\TaskController;
Route::middleware(['auth', 'verified'])->group(function () {
Route::resource('tasks', TaskController::class)->except(['show']);
});
Isso cria automaticamente as rotas RESTful padrão para o CRUD de tarefas.
4. Criando as Views com Blade e Tailwind
Crie a pasta resources/views/tasks e dentro dela os arquivos:
a) index.blade.php
<x-app-layout>
<x-slot name="header">
<div class="flex justify-between items-center">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('My Tasks') }}
</h2>
<a href="{{ route('tasks.create') }}" class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">
{{ __('New Task') }}
</a>
</div>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
{{-- Success Message (Toast) --}}
@if (session('success'))
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-6 rounded-md shadow-sm" role="alert">
<p class="font-bold">{{ __('Success!') }}</p>
<p>{{ session('success') }}</p>
</div>
@endif
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
@forelse ($tasks as $task)
<div class="flex items-center justify-between border-b border-gray-200 py-4 last:border-b-0">
<div class="flex items-center">
<span class="text-lg {{ $task->is_completed ? 'line-through text-gray-500' : 'text-gray-900' }}">
{{ $task->title }}
</span>
@if ($task->is_completed)
<span class="ml-2 px-2 py-0.5 bg-green-100 text-green-800 text-xs font-semibold rounded-full">{{ __('Completed') }}</span>
<span class="ml-2 text-sm text-gray-500">({{ __('Finished on:') }} {{ $task->updated_at->format('d/m/Y H:i') }})</span>
@else
<span class="ml-2 px-2 py-0.5 bg-yellow-100 text-yellow-800 text-xs font-semibold rounded-full">{{ __('Open') }}</span>
@endif
</div>
<div class="flex items-center space-x-3">
<a href="{{ route('tasks.edit', $task) }}" class="text-sm text-indigo-600 hover:text-indigo-900 font-medium">
{{ __('Edit') }}
</a>
<form action="{{ route('tasks.destroy', $task) }}" method="POST" onsubmit="return confirm('{{ __('Are you sure you want to delete this task?') }}');">
@csrf
@method('DELETE')
<button type="submit" class="text-sm text-red-600 hover:text-red-900 font-medium">
{{ __('Delete') }}
</button>
</form>
</div>
</div>
@empty
<p class="text-gray-600 text-center py-4">{{ __('No tasks found. How about creating one?') }}</p>
@endforelse
</div>
</div>
</div>
</div>
</x-app-layout>
b) create.blade.php e edit.blade.php
Os dois arquivos são muito semelhantes. Crie o create.blade.php:
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Create New Task') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<form method="POST" action="{{ route('tasks.store') }}">
@csrf
<div>
<x-input-label for="title" :value="__('Task Title')" />
<x-text-input id="title" class="block mt-1 w-full" type="text" name="title" :value="old('title')" required autofocus />
<x-input-error :messages="$errors->get('title')" class="mt-2" />
</div>
<div class="mt-4">
<x-input-label for="description" :value="__('Description (Optional)')" />
<textarea id="description" name="description" rows="3" class="block mt-1 w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm">{{ old('description') }}</textarea>
<x-input-error :messages="$errors->get('description')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-6 space-x-8">
<a href="{{ route('tasks.index') }}" class="text-gray-600 hover:text-gray-900">
{{ __('Cancel') }}
</a>
<x-primary-button>
{{ __('Save Task') }}
</x-primary-button>
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>
E o edit.blade.php:
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Edit Task') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<form method="POST" action="{{ route('tasks.update', $task) }}">
@csrf
@method('PATCH') {{-- Or @method('PUT') --}}
<div>
<x-input-label for="title" :value="__('Task Title')" />
<x-text-input id="title" class="block mt-1 w-full" type="text" name="title" :value="old('title', $task->title)" required autofocus />
<x-input-error :messages="$errors->get('title')" class="mt-2" />
</div>
<div class="mt-4">
<x-input-label for="description" :value="__('Description (Optional)')" />
<textarea id="description" name="description" rows="3" class="block mt-1 w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm">{{ old('description', $task->description) }}</textarea>
<x-input-error :messages="$errors->get('description')" class="mt-2" />
</div>
<div class="mt-4">
<label for="is_completed" class="inline-flex items-center">
<input id="is_completed" type="checkbox" name="is_completed" value="1" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500" {{ old('is_completed', $task->is_completed) ? 'checked' : '' }}>
<span class="ml-2 text-sm text-gray-600">{{ __('Completed?') }}</span>
</label>
<x-input-error :messages="$errors->get('is_completed')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-6">
<a href="{{ route('tasks.index') }}" class="text-gray-600 hover:text-gray-900 mr-4">
{{ __('Cancel') }}
</a>
<x-primary-button>
{{ __('Update Task') }}
</x-primary-button>
</div>
</form>
</div>
</div>
</div>
</x-app-layout>
Conclusão
Com isso, finalizamos o CRUD completo da entidade Task. Agora você pode listar, criar, editar e excluir tarefas com autenticação, controle de acesso e uma interface moderna com Tailwind CSS. O projeto já se comporta como um sistema real de gerenciamento de tarefas.
Na Parte 6, vamos fazer algumas melhorias no nosso CRUD.
🔗 Leitura recomendada: CRUD com Laravel 12, Breeze e Fortify – Parte 5A.