Controllers, Blade views and Eloquent models

Thin controllers, templates that escape by default, and models whose relations do not issue a query per row.

Controllers

<?php
namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::with('author')->latest()->paginate(15);
        return view('posts.index', compact('posts'));
    }

    public function show(Post $post)          // route model binding
    {
        return view('posts.show', ['post' => $post]);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => ['required', 'string', 'max:200'],
            'body'  => ['required', 'string', 'min:20'],
        ]);

        $post = $request->user()->posts()->create($validated);

        return redirect()
            ->route('posts.show', $post)
            ->with('status', 'Post created.');
    }
}
  • Keep HTTP concerns in the controller and put domain rules in a service or a model method, so the same rule is reusable from a command or a job.
  • Return view(), redirect() or a JSON payload; Laravel negotiates the response type for you.
  • with('author') eager-loads the relation used by the view and prevents an N+1 query.
  • paginate(15) returns a paginator, and {{ $posts->links() }} renders the controls.

Blade views and Eloquent models

<!-- resources/views/posts/index.blade.php -->
@extends('layouts.app')

@section('content')
    <h1>Posts</h1>

    @forelse ($posts as $post)
        <article>
            <h2>
                <a href="{{ route('posts.show', $post) }}">{{ $post->title }}</a>
            </h2>
            <p>by {{ $post->author->name }} on {{ $post->created_at->diffForHumans() }}</p>
        </article>
    @empty
        <p>No posts yet.</p>
    @endforelse

    {{ $posts->links() }}
@endsection
<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Post extends Model
{
    protected $fillable = ['title', 'body'];   // mass-assignment allowlist

    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class, 'user_id');
    }

    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }

    public function scopePublished($query)
    {
        return $query->whereNotNull('published_at');
    }
}
RelationUse it for
hasOne / belongsToThe simple pair: a post belongs to one author
hasManyThe many side: a post has many comments
belongsToManyA pivot table, for example posts and tags
hasManyThroughA distant relation reached through an intermediate table
morphManyComments or reactions that attach to several parent types
⚠️
A loop that touches $post->author for every row issues one query per row. Eager-load with with('author'), and enable Model::preventLazyLoading() in development so the mistake fails loudly instead of quietly getting slower.

FAQ

What does $fillable protect against?
Mass assignment of columns the user must not set, such as is_admin. Attributes outside $fillable, or listed in $guarded, are ignored by create() and update().
Eloquent or the query builder?
Eloquent when you want models, relations, casts and events. The query builder (DB::table(...)) for bulk reports or large exports where hydrating thousands of models is the bottleneck.

Installation and routing Migrations and validation

Last refreshed 2026-09-18.