Laravel
CRUD & Validation
Build full Create, Read, Update, Delete functionality using Resource Controllers, Form Requests, and Blade forms with proper validation.
Generate Resource Controller: Use Artisan to scaffold a resource controller with all CRUD methods and optionally bind it to a model via
--model.
terminal
BASH
# Generate resource controller with model binding
php artisan make:controller PostController --resource --model=Post
# Generate Form Request classes for store and update
php artisan make:request StorePostRequest
php artisan make:request UpdatePostRequest
# Generate model with migration, factory, and seeder
php artisan make:model Post -mfs
Route Resource Declaration: Register all 7 RESTful routes (index, create, store, show, edit, update, destroy) with a single
Route::resource() call. Use only or except to limit routes.
routes/web.php
PHP
<?php
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;
// Full resource – generates index, create, store, show, edit, update, destroy
Route::resource('posts', PostController::class);
// Limit to specific actions
Route::resource('posts', PostController::class)->only(['index', 'show']);
Route::resource('posts', PostController::class)->except(['destroy']);
// Nested resource (e.g. post comments)
Route::resource('posts.comments', CommentController::class)->shallow();
// Named middleware group
Route::middleware(['auth'])->group(function () {
Route::resource('posts', PostController::class)->except(['index', 'show']);
});
Full Resource Controller: Complete implementation of all CRUD methods. Uses Form Request classes for validation, redirects with flash messages, and model route binding.
app/Http/Controllers/PostController.php
PHP
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StorePostRequest;
use App\Http\Requests\UpdatePostRequest;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class PostController extends Controller
{
public function index(): View
{
= Post::latest()->paginate(10);
return view('posts.index', compact('posts'));
}
public function create(): View
{
return view('posts.create');
}
public function store(StorePostRequest $request): RedirectResponse
{
Post::create($request->validated());
return redirect()->route('posts.index')
->with('success', 'Post created successfully.');
}
public function show(Post $post): View
{
return view('posts.show', compact('post'));
}
public function edit(Post $post): View
{
return view('posts.edit', compact('post'));
}
public function update(UpdatePostRequest $request, Post $post): RedirectResponse
{
$post->update($request->validated());
return redirect()->route('posts.show', $post)
->with('success', 'Post updated successfully.');
}
public function destroy(Post $post): RedirectResponse
{
$post->delete();
return redirect()->route('posts.index')
->with('success', 'Post deleted successfully.');
}
}
Form Request Validation: Encapsulate authorization and validation logic in dedicated
FormRequest classes. The authorize() method controls access; rules() defines validation constraints; messages() customizes error text.
app/Http/Requests/StorePostRequest.php
PHP
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
// Allow all authenticated users
return true;
// Or check policy: return $this->user()->can('create', Post::class);
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'min:5', 'max:255'],
'slug' => ['required', 'string', 'unique:posts,slug'],
'body' => ['required', 'string', 'min:20'],
'status' => ['required', 'in:draft,published'],
'image' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
'tags' => ['nullable', 'array'],
'tags.*' => ['exists:tags,id'],
'published_at' => ['nullable', 'date', 'after_or_equal:today'],
];
}
public function messages(): array
{
return [
'title.required' => 'A post title is required.',
'title.min' => 'The title must be at least 5 characters.',
'slug.unique' => 'This slug is already taken.',
'body.min' => 'Post content must be at least 20 characters.',
'image.max' => 'Image size must not exceed 2 MB.',
];
}
// Optionally transform input before validation
protected function prepareForValidation(): void
{
$this->merge([
'slug' => \Illuminate\Support\Str::slug($this->slug ?? $this->title),
]);
}
}
Common Validation Rules Reference: A quick-reference list of frequently used Laravel validation rules for strings, numbers, dates, files, and database checks.
validation-rules-reference.php
PHP
// String rules
'name' => ['required', 'string', 'max:100', 'alpha_dash'],
'email' => ['required', 'email:rfc,dns', 'unique:users,email'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'url' => ['nullable', 'url', 'active_url'],
// Numeric rules
'age' => ['required', 'integer', 'between:18,100'],
'price' => ['required', 'numeric', 'min:0', 'max:9999.99'],
'quantity' => ['required', 'digits_between:1,5'],
// Date rules
'start_date' => ['required', 'date', 'date_format:Y-m-d'],
'end_date' => ['required', 'date', 'after:start_date'],
// File rules
'avatar' => ['required', 'image', 'mimes:jpg,png,gif,webp', 'max:1024'],
'document' => ['required', 'file', 'mimes:pdf,doc,docx', 'max:5120'],
// Database rules
'category_id' => ['required', 'exists:categories,id'],
'username' => ['required', 'unique:users,username,' . $userId], // ignore own record
// Array rules
'tags' => ['required', 'array', 'min:1', 'max:5'],
'tags.*' => ['integer', 'exists:tags,id'],
// Conditional rules
'company' => ['required_if:account_type,business'],
'vat' => ['required_unless:country,US'],
'phone' => ['required_with:country_code'],
// Custom Rule object
'code' => ['required', new \App\Rules\ValidPromoCode],
Blade CRUD Form: A complete create/edit Blade form using
@csrf, @method('PUT') for updates, @error directives for inline validation errors, and old() to repopulate fields.
resources/views/posts/create.blade.php
HTML
@extends('layouts.app')
@section('content')
<h1>{{ isset($post) ? 'Edit Post' : 'Create Post' }}</h1>
<form action="{{ isset($post) ? route('posts.update', $post) : route('posts.store') }}"
method="POST" enctype="multipart/form-data">
@csrf
@isset($post)
@method('PUT')
@endisset
{{-- Title --}}
<div class="mb-3">
<label for="title">Title</label>
<input type="text" id="title" name="title"
value="{{ old('title', $post->title ?? '') }}"
class="form-control @error('title') is-invalid @enderror">
@error('title')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
{{-- Body --}}
<div class="mb-3">
<label for="body">Content</label>
<textarea id="body" name="body" rows="8"
class="form-control @error('body') is-invalid @enderror">{{ old('body', $post->body ?? '') }}</textarea>
@error('body')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
{{-- Status Select --}}
<div class="mb-3">
<label for="status">Status</label>
<select id="status" name="status"
class="form-select @error('status') is-invalid @enderror">
<option value="draft" {{ old('status', $post->status ?? '') === 'draft' ? 'selected' : '' }}>Draft</option>
<option value="published" {{ old('status', $post->status ?? '') === 'published' ? 'selected' : '' }}>Published</option>
</select>
@error('status')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
{{-- Image --}}
<div class="mb-3">
<label for="image">Cover Image</label>
<input type="file" id="image" name="image"
class="form-control @error('image') is-invalid @enderror">
@error('image')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<button type="submit" class="btn btn-primary">
{{ isset($post) ? 'Update Post' : 'Create Post' }}
</button>
</form>
@endsection
Delete with Soft Deletes: Implement safe deletion using
SoftDeletes trait so records are hidden but kept in the database. Include a restore mechanism.
app/Models/Post.php
PHP
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Post extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = ['title', 'slug', 'body', 'status', 'image', 'published_at'];
protected $casts = [
'published_at' => 'datetime',
];
// Scope: only published posts
public function scopePublished($query)
{
return $query->where('status', 'published')->whereNotNull('published_at');
}
}
// Migration: add soft-deletes column
// Schema::table('posts', function (Blueprint $table) {
// $table->softDeletes(); // adds deleted_at column
// });
// In controller – soft delete (sets deleted_at)
$post->delete();
// Query including trashed records
Post::withTrashed()->get();
// Restore a soft-deleted record
Post::withTrashed()->find($id)->restore();
// Permanently delete
Post::withTrashed()->find($id)->forceDelete();
// Blade: delete form (POST tunnel for DELETE method)
// <form action="{{ route('posts.destroy', $post) }}" method="POST">
// @csrf @method('DELETE')
// <button>Delete</button>
// </form>