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');
}
}| Relation | Use it for |
|---|---|
hasOne / belongsTo | The simple pair: a post belongs to one author |
hasMany | The many side: a post has many comments |
belongsToMany | A pivot table, for example posts and tags |
hasManyThrough | A distant relation reached through an intermediate table |
morphMany | Comments 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.Related
Installation and routing Migrations and validation
Last refreshed 2026-09-18.