PHP & Laravel — Zero to Hero Episode 19: Eloquent ORM — Connecting Your Application to a Real Database

What Are We Doing in This Post?

For the last three episodes our posts have been hardcoded arrays inside the controller. Every time you restart the server, the data resets. Nothing is saved permanently.

In this episode we connect everything to a real MySQL database using Eloquent — Laravel's ORM.

Eloquent lets you interact with your database using clean PHP instead of raw SQL. Instead of writing SELECT * FROM posts WHERE id = 5, you write Post::find(5). Instead of INSERT INTO posts ..., you write Post::create([...]). It reads like English and it is far safer than raw queries.

By the end of this episode your blog will be fully database-driven — creating, reading, updating, and deleting real posts stored in MySQL.


What is Eloquent?

Eloquent is an ORM — Object Relational Mapper. It maps database tables to PHP classes and database rows to PHP objects.

Real world analogy: Think of a database table as a spreadsheet. Eloquent is like a smart assistant who sits between you and that spreadsheet. Instead of you writing complex formulas to find, add, or update rows yourself, you tell the assistant in plain language — "find me the post with ID 5", "create a new post with this title", "delete all draft posts". The assistant handles all the spreadsheet operations for you.

Each database table has one corresponding Eloquent Model class. The posts table has a Post model. The users table has a User model. The model is your interface to that table.


Step 1 — Create the Post Model

Run this Artisan command:


    php artisan make:model Post

Laravel creates app/Models/Post.php. Open it:


    <?php

    namespace App\Models;

    use Illuminate\Database\Eloquent\Model;

    class Post extends Model
    {

    }

That is it. An empty class that extends Eloquent's Model. By extending Model, your Post class automatically inherits hundreds of methods for database interaction — create, find, update, delete, query, paginate, and much more.

Eloquent uses conventions to figure out the table name automatically. The Post model maps to the posts table. The User model maps to users. Eloquent pluralizes the model name and lowercases it. You never have to specify the table name unless you break the convention.


Step 2 — Configure Mass Assignment Protection

Before we can create records using Eloquent's create() method, we need to tell it which columns are safe to fill from user input. This is called the $fillable property.


    <?php

    namespace App\Models;

    use Illuminate\Database\Eloquent\Model;

    class Post extends Model
    {
        protected $fillable = [
            'user_id',
            'title',
            'slug',
            'body',
            'status',
            'views',
            'published_at',
        ];
    }

$fillable is a whitelist. Only the columns listed here can be mass-assigned — meaning filled in bulk using an array. This prevents a security vulnerability called mass assignment — where a malicious user could inject extra fields into a form submission to overwrite columns they should not have access to.

For example, without $fillable, a clever user could add is_admin=1 to a form submission and potentially overwrite their own role in the database. With $fillable, only the explicitly listed columns are accepted.


Step 3 — Update the Migration

Our posts migration from Episode 16 already has the right structure. But let us verify by checking database/migrations/ for the create_posts_table file. The migration should look like this — if it does not match, update it:


    <?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')->nullable()->constrained()->onDelete('cascade');
                $table->string('title');
                $table->string('slug')->unique();
                $table->text('body');
                $table->string('status')->default('draft');
                $table->unsignedInteger('views')->default(0);
                $table->timestamp('published_at')->nullable();
                $table->timestamps();
            });
        }

        public function down(): void
        {
            Schema::dropIfExists('posts');
        }
    };

We made user_id nullable for now — since we do not have authentication yet, posts will not be tied to a specific user until Episode 21.

Run a fresh migration to rebuild the database cleanly:


    php artisan migrate:fresh

This drops all existing tables and reruns every migration. Your laravel_myapp database now has clean empty tables.


Step 4 — Seed the Database With Fake Data

Instead of manually adding test posts through a form, let us use Laravel's seeder system to populate the database automatically.

Run:


    php artisan make:seeder PostSeeder

Open database/seeders/PostSeeder.php and update it:


    <?php

    namespace Database\Seeders;

    use Illuminate\Database\Seeder;
    use App\Models\Post;
    use Illuminate\Support\Str;

    class PostSeeder extends Seeder
    {
        public function run(): void
        {
            $posts = [
                [
                    'title'        => 'Getting Started With Laravel',
                    'slug'         => 'getting-started-with-laravel',
                    'body'         => 'Laravel is a powerful PHP framework that makes web development enjoyable. It provides tools for routing, database management, authentication, and much more out of the box. Whether you are building a simple blog or a complex enterprise application, Laravel has the tools you need.',
                    'status'       => 'published',
                    'published_at' => now(),
                ],
                [
                    'title'        => 'Understanding MVC Architecture',
                    'slug'         => 'understanding-mvc-architecture',
                    'body'         => 'MVC separates your application into Models, Views, and Controllers. This separation of concerns keeps your code organized, maintainable, and easy to scale. The Model handles data, the View handles display, and the Controller handles the logic that connects them.',
                    'status'       => 'published',
                    'published_at' => now(),
                ],
                [
                    'title'        => 'Working With Eloquent ORM',
                    'slug'         => 'working-with-eloquent-orm',
                    'body'         => 'Eloquent makes database queries feel like writing plain English. Each database table has a corresponding Model class that you use to interact with that table. You can fetch, create, update, and delete records without writing a single line of raw SQL.',
                    'status'       => 'published',
                    'published_at' => now(),
                ],
                [
                    'title'        => 'Blade Templating Engine Deep Dive',
                    'slug'         => 'blade-templating-engine-deep-dive',
                    'body'         => 'Blade is Laravel\'s powerful templating engine. It gives you clean syntax for displaying variables, loops, conditionals, and template inheritance. Every Blade file is compiled to plain PHP and cached for maximum performance.',
                    'status'       => 'draft',
                    'published_at' => null,
                ],
                [
                    'title'        => 'Laravel Routing Complete Guide',
                    'slug'         => 'laravel-routing-complete-guide',
                    'body'         => 'Laravel\'s routing system is clean and expressive. You can define routes with parameters, constraints, names, and groups. Route::resource generates all seven RESTful routes in a single line.',
                    'status'       => 'published',
                    'published_at' => now(),
                ],
            ];

            foreach ($posts as $post) {
                Post::create($post);
            }
        }
    }

Now register this seeder in database/seeders/DatabaseSeeder.php:


    <?php

    namespace Database\Seeders;

    use Illuminate\Database\Seeder;

    class DatabaseSeeder extends Seeder
    {
        public function run(): void
        {
            $this->call([
                PostSeeder::class,
            ]);
        }
    }

Run the seeder:


    php artisan db:seed

You will see:
INFO  Seeding database.
PostSeeder ............... DONE

Check phpMyAdmin at http://localhost:8080/phpmyadmin — open laravel_myapp, click posts table, click Browse. You will see all five posts inserted.


Step 5 — Update the Controller to Use Eloquent

Now replace all the hardcoded arrays in PostController with real Eloquent queries:


    <?php

    namespace App\Http\Controllers;

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

    class PostController extends Controller
    {
        public function index()
        {
            $posts = Post::latest()->get();
            return view('posts.index', ['posts' => $posts]);
        }

        public function create()
        {
            return view('posts.create');
        }

        public function store(Request $request)
        {
            $request->validate([
                'title'  => 'required|min:3|max:255',
                'body'   => 'required|min:10',
                'author' => 'required|max:100',
            ]);

            Post::create([
                'title'        => $request->input('title'),
                'slug'         => Str::slug($request->input('title')),
                'body'         => $request->input('body'),
                'status'       => 'published',
                'published_at' => now(),
            ]);

            return redirect()->route('posts.index')->with('success', 'Post created successfully!');
        }

        public function show($id)
        {
            $post = Post::findOrFail($id);
            $post->increment('views');
            return view('posts.show', ['post' => $post]);
        }

        public function edit($id)
        {
            $post = Post::findOrFail($id);
            return view('posts.edit', ['post' => $post]);
        }

        public function update(Request $request, $id)
        {
            $post = Post::findOrFail($id);

            $request->validate([
                'title' => 'required|min:3|max:255',
                'body'  => 'required|min:10',
            ]);

            $post->update([
                'title' => $request->input('title'),
                'slug'  => Str::slug($request->input('title')),
                'body'  => $request->input('body'),
            ]);

            return redirect()->route('posts.show', $post->id)->with('success', 'Post updated successfully!');
        }

        public function destroy($id)
        {
            $post = Post::findOrFail($id);
            $post->delete();
            return redirect()->route('posts.index')->with('success', 'Post deleted successfully!');
        }
    }

Let us go through every Eloquent method used here.


Understanding Eloquent Methods

Post::latest()->get()


    <?php

    $posts = Post::latest()->get();

latest() orders results by created_at descending — newest first. get() executes the query and returns a Collection — Laravel's powerful array-like object containing all matching Post models.

Post::create()


    <?php

    Post::create([
        'title' => 'My Post',
        'slug'  => 'my-post',
        'body'  => 'Post content here.',
    ]);

Creates a new row in the posts table with the provided data. Returns the newly created Post model with its auto-assigned id already set.

Post::findOrFail($id)


    <?php

    $post = Post::findOrFail($id);

Finds a post by its primary key. If found, returns the Post model. If not found, automatically throws a 404 error — no manual abort(404) needed. This is much cleaner than Post::find($id) which returns null on failure and requires a manual check.

$post->increment('views')


    <?php

    $post->increment('views');

Increments the views column by 1 directly in the database. Safer than fetching the value, adding 1 in PHP, and saving back — because the increment happens atomically in MySQL.

$post->update()


    <?php

    $post->update([
        'title' => 'Updated Title',
        'body'  => 'Updated content.',
    ]);

Updates only the specified columns on the existing record. updated_at is set automatically by Eloquent.

$post->delete()


    <?php

    $post->delete();

Deletes the record from the database permanently.


Eloquent Query Builder — Filtering and Querying

Eloquent has a full query builder for more complex queries:


    <?php

    Post::all();

    Post::find(5);

    Post::findOrFail(5);

    Post::where('status', 'published')->get();

    Post::where('status', 'published')->orderBy('created_at', 'desc')->get();

    Post::where('status', 'published')->latest()->take(3)->get();

    Post::where('views', '>', 100)->get();

    Post::where('status', 'published')->where('views', '>', 50)->get();

    Post::where('status', 'published')->orWhere('views', '>', 1000)->get();

    Post::count();

    Post::where('status', 'published')->count();

    Post::latest()->first();

    Post::where('slug', 'my-post')->first();

all() — fetch every row. Use carefully on large tables.

find($id) — fetch by primary key, returns null if not found.

where() — add conditions. Chainable.

orderBy('column', 'direction') — sort results.

take($n) — limit results to n rows.

count() — count matching rows without fetching them.

first() — fetch only the first matching row.


Accessing Model Properties

When Eloquent returns a model, each database column becomes a property:


    <?php

    $post = Post::findOrFail(1);

    echo $post->id;
    echo $post->title;
    echo $post->body;
    echo $post->status;
    echo $post->created_at;
    echo $post->updated_at;

In Blade templates you access them the same way:


    <h1>{{ $post->title }}</h1>
    <p>{{ $post->body }}</p>
    <small>{{ $post->created_at->diffForHumans() }}</small>

$post->created_at is not just a plain string — Eloquent automatically casts timestamp columns to Carbon objects. Carbon is a PHP date library that gives you clean methods like diffForHumans() which outputs "3 hours ago", "2 days ago", "1 month ago" — exactly what you see on real blog posts and social media.


Update the Blade Views for Eloquent Data

Update resources/views/posts/index.blade.php — the data is now Eloquent model objects, not arrays, so we use -> instead of []:


    @extends('layouts.app')

    @section('title', 'All Posts — MyBlog')

    @push('styles')
    <style>
        .post-card { background:#fff; border:1px solid #e2e8f0; border-radius:12px; padding:24px; margin-bottom:20px; }
        .post-card h2 { font-size:20px; font-weight:700; margin-bottom:8px; color:#1e293b; }
        .post-card p { font-size:14px; color:#64748b; line-height:1.6; margin-bottom:12px; }
        .post-meta { font-size:12px; color:#94a3b8; display:flex; gap:16px; align-items:center; }
        .badge { display:inline-block; padding:3px 10px; border-radius:20px; font-size:11px; font-weight:600; }
        .badge-published { background:#f0fdf4; color:#166534; }
        .badge-draft { background:#fef9c3; color:#854d0e; }
        .read-more { color:#6366f1; font-weight:600; text-decoration:none; font-size:13px; }
    </style>
    @endpush

    @section('content')

        <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:32px;">
            <h1 style="font-size:28px; font-weight:800;">All Posts</h1>
            <a href="{{ route('posts.create') }}" style="background:#6366f1; color:#fff; padding:10px 20px; border-radius:8px; text-decoration:none; font-size:14px; font-weight:600;">+ New Post</a>
        </div>

        @forelse($posts as $post)
            <div class="post-card">
                <div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:10px;">
                    <h2>{{ $post->title }}</h2>
                    <span class="badge badge-{{ $post->status }}">{{ ucfirst($post->status) }}</span>
                </div>
                <p>{{ Str::limit($post->body, 120) }}</p>
                <div class="post-meta">
                    <span>{{ $post->created_at->diffForHumans() }}</span>
                    <span>{{ $post->views }} views</span>
                    <a href="{{ route('posts.show', $post->id) }}" class="read-more">Read More →</a>
                </div>
            </div>
        @empty
            <div style="text-align:center; padding:60px; color:#64748b;">
                <p style="font-size:18px; margin-bottom:16px;">No posts yet.</p>
                <a href="{{ route('posts.create') }}" style="color:#6366f1; font-weight:600;">Create your first post</a>
            </div>
        @endforelse

    @endsection

Update resources/views/posts/show.blade.php:


    @extends('layouts.app')

    @section('title', $post->title . ' — MyBlog')

    @section('content')

        <article style="background:#fff; border:1px solid #e2e8f0; border-radius:12px; padding:40px; max-width:700px; margin:0 auto;">

            <span class="badge badge-{{ $post->status }}" style="display:inline-block; padding:3px 10px; border-radius:20px; font-size:11px; font-weight:600; background:#f0fdf4; color:#166534; margin-bottom:16px;">
                {{ ucfirst($post->status) }}
            </span>

            <h1 style="font-size:30px; font-weight:800; margin-bottom:8px; color:#1e293b;">{{ $post->title }}</h1>

            <div style="display:flex; gap:16px; color:#94a3b8; font-size:13px; margin-bottom:32px; padding-bottom:24px; border-bottom:1px solid #e2e8f0;">
                <span>{{ $post->created_at->format('d M Y') }}</span>
                <span>{{ $post->views }} views</span>
            </div>

            <div style="font-size:16px; line-height:1.85; color:#334155;">
                {{ $post->body }}
            </div>

            <div style="margin-top:40px; padding-top:24px; border-top:1px solid #e2e8f0; display:flex; gap:16px;">
                <a href="{{ route('posts.index') }}" style="color:#6366f1; font-weight:600; text-decoration:none;">← All Posts</a>
                <a href="{{ route('posts.edit', $post->id) }}" style="color:#f59e0b; font-weight:600; text-decoration:none;">Edit Post</a>

                <form method="POST" action="{{ route('posts.destroy', $post->id) }}" style="display:inline;" onsubmit="return confirm('Delete this post?')">
                    @csrf
                    @method('DELETE')
                    <button type="submit" style="background:none; border:none; color:#ef4444; font-weight:600; cursor:pointer; font-size:16px; padding:0;">Delete Post</button>
                </form>
            </div>

        </article>

    @endsection

Notice @method('DELETE') — HTML forms only support GET and POST natively. For PUT, PATCH, and DELETE requests from a form, Blade provides @method() which adds a hidden _method field that Laravel reads to determine the actual HTTP method.

Create resources/views/posts/edit.blade.php:


    @extends('layouts.app')

    @section('title', 'Edit Post — MyBlog')

    @section('content')

        <div style="background:#fff; border:1px solid #e2e8f0; border-radius:12px; padding:40px; max-width:600px; margin:0 auto;">

            <h1 style="font-size:24px; font-weight:800; margin-bottom:28px;">Edit Post</h1>

            <form method="POST" action="{{ route('posts.update', $post->id) }}">
                @csrf
                @method('PATCH')

                <div style="margin-bottom:20px;">
                    <label style="display:block; font-size:14px; font-weight:600; margin-bottom:6px;">Title</label>
                    <input type="text" name="title" value="{{ old('title', $post->title) }}"
                        style="width:100%; padding:10px 14px; border:1px solid #e2e8f0; border-radius:8px; font-size:15px;" required>
                </div>

                <div style="margin-bottom:28px;">
                    <label style="display:block; font-size:14px; font-weight:600; margin-bottom:6px;">Body</label>
                    <textarea name="body" rows="8"
                        style="width:100%; padding:10px 14px; border:1px solid #e2e8f0; border-radius:8px; font-size:15px; resize:vertical;" required>{{ old('body', $post->body) }}</textarea>
                </div>

                <div style="display:flex; gap:12px;">
                    <button type="submit"
                        style="background:#6366f1; color:#fff; padding:12px 28px; border:none; border-radius:8px; font-size:15px; font-weight:600; cursor:pointer;">
                        Update Post
                    </button>
                    <a href="{{ route('posts.show', $post->id) }}"
                        style="background:#f1f5f9; color:#475569; padding:12px 28px; border-radius:8px; font-size:15px; font-weight:600; text-decoration:none;">
                        Cancel
                    </a>
                </div>
            </form>

        </div>

    @endsection

old('title', $post->title) — the second argument to old() is a default value. If the form was freshly loaded (not a failed re-submission), it shows the existing post title. If validation failed and the form was re-shown, it shows what the user last typed.


Testing the Full Application

Start the server if it is not running:


    php artisan serve

Visit http://127.0.0.1:8000/posts — you see all five seeded posts with real data from MySQL, view counts, and relative timestamps.

Click any post — the view count increments on every visit. Refresh and watch it go up.

Click Edit Post — update the title and body, submit. You are redirected back to the post with a success message.

Click Delete Post — confirm the dialog. Post is gone from the database.

Go to Create Post — fill in the form and submit. A new post appears in the list, stored permanently in MySQL.

This is a fully working database-driven Laravel application. Create, Read, Update, Delete — all working with real persistent data.


What Did We Learn in This Post?

Eloquent ORM maps database tables to PHP model classes. The Post model maps to the posts table automatically by convention.

$fillable whitelists columns for mass assignment — a critical security protection.

Key Eloquent methods: all(), find(), findOrFail(), create(), update(), delete(), where(), latest(), first(), count(), get(), increment().

findOrFail() automatically returns a 404 response when a record is not found — cleaner than manual null checks.

Timestamp columns like created_at are Carbon objects — diffForHumans() gives you "3 hours ago" style output automatically.

@method('DELETE') and @method('PATCH') in Blade forms let HTML forms send non-GET/POST HTTP methods that Laravel understands.

old('field', $default) in edit forms shows existing data on first load and preserves user input on failed validation.

Seeders populate the database with test data using php artisan db:seed — far faster than manual data entry during development.


What is Coming in Episode 20?

Our application works but it has no input validation error messages shown to the user. Submit a form with invalid data right now — Laravel rejects it silently and redirects back.

Episode 20 covers Laravel Form Validation in depth — validation rules, displaying error messages in Blade, custom error messages, and Form Request classes that keep validation logic out of controllers completely.

See you in the next one.

No comments:

Post a Comment

PHP & Laravel — Zero to Hero Episode 19: Eloquent ORM — Connecting Your Application to a Real Database

What Are We Doing in This Post? For the last three episodes our posts have been hardcoded arrays inside the controller. Every time you resta...