Migrations and validation

Versioned schema changes you can roll back, and rules that keep unvalidated input away from the database.

Migrations

php artisan make:migration create_posts_table --create=posts
php artisan migrate
php artisan migrate:rollback --step=1
php artisan migrate:status
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->string('title', 200);
            $table->text('body');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();
            $table->index(['published_at', 'user_id']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};
  • One migration per schema change, and never edit a migration that has already run in production — add a new one instead.
  • foreignId(...)->constrained() creates the column, the index and the foreign key in a single call.
  • migrate:rollback runs down() in reverse order, so keep it correct and a bad deploy stays reversible.
  • migrate:fresh drops every table and reapplies everything; it is for local development only, never for a shared environment.
  • Seeders belong in database/seeders and should be idempotent so they can run repeatedly.

Validation and form requests

php artisan make:request StorePostRequest
<?php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', Post::class);
    }

    public function rules(): array
    {
        return [
            'title'   => ['required', 'string', 'max:200'],
            'body'    => ['required', 'string', 'min:20'],
            'slug'    => ['required', Rule::unique('posts', 'slug')->ignore($this->post)],
            'tags'    => ['array'],
            'tags.*'  => ['exists:tags,id'],
        ];
    }
}
public function store(StorePostRequest $request)
{
    // runs only if authorize() passed and the rules passed
    return Post::create($request->validated());
}
RuleWhat it checks
requiredThe key is present and not empty
sometimesValidate only when the key is present
unique:table,columnNo duplicate, with ignore() for updates
exists:table,columnThe value references an existing row
array and field.*Validate each element of a list
Rule::in([...])Restrict the value to an allowlist
⚠️
$request->all() passes every submitted field into create(), including fields the form never showed. Use $request->validated() so only rule-approved data reaches the database, and keep $fillable as a second line of defence.

FAQ

Where do validation failures go?
For a web request, back to the form with the errors flashed to the session and available as $errors. For a JSON request, a 422 response with an errors object. Neither needs code in the action.
Form request or inline validate()?
Inline $request->validate() for a small endpoint with two or three rules. Move to a FormRequest once the rules grow, need an authorisation check, or are reused by several actions.

Controllers, Blade views and Eloquent models Installation and routing

Last refreshed 2026-09-18.