Labs ICT
Pro Login

Practical Example: Blog App

Building a complete blog with models, views, and controllers.

Practical Example: Blog Application

Let us tie everything together by building a simple blog application. This project will use models, migrations, controllers, views, routes, and the features you have learned throughout this tutorial.

We will create a blog where users can write, edit, and delete posts. We will include authentication, validation, and pagination.

Models and Migrations

Start by creating the Post model and its migration. A post needs a title, body, and a reference to the user who created it.


php artisan make:model Post -m

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->string('title');
    $table->text('body');
    $table->timestamps();
});
    

Run php artisan migrate to create the table. Then define the relationships in the User and Post models.


namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasFactory;

    protected $fillable = ['title', 'body'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
    

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use HasFactory;

    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}
    

The $fillable attribute protects against mass assignment. The relationships let you access posts through a user and vice versa.

Routes and Controllers

Define your routes in routes/web.php. Use resource routes for standard CRUD operations.


use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::get('/', [PostController::class, 'index'])->name('posts.index');
Route::resource('posts', PostController::class)->except(['index']);
    

The controller handles the logic for each action. Use Form Request classes for validation.


namespace App\Http\Controllers;

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

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

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

        auth()->user()->posts()->create($validated);

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

    public function show(Post $post)
    {
        return view('posts.show', compact('post'));
    }

    public function edit(Post $post)
    {
        return view('posts.edit', compact('post'));
    }

    public function update(Request $request, Post $post)
    {
        $validated = $request->validate([
            'title' => 'required|max:255',
            'body' => 'required',
        ]);

        $post->update($validated);

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

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

Laravel route model binding automatically injects the Post instance. Validation ensures only clean data enters your database.

Views and Partials

Create Blade views for each page. Use layouts to avoid repeating the same HTML structure across views.


// resources/views/layouts/app.blade.php
<html>
<head>
    <title>My Blog</title>
</head>
<body>
    @yield('content')
</body>
</html>

// resources/views/posts/index.blade.php
@extends('layouts.app')

@section('content')
<h1>Latest Posts</h1>
@foreach ($posts as $post)
    <article>
        <h2>{{ $post->title }}</h2>
        <p>{{ Str::limit($post->body, 200) }}</p>
        <a href="{{ route('posts.show', $post) }}">Read More</a>
    </article>
@endforeach
{{ $posts->links() }}
@endsection
    

The @extends and @section directives let you compose views from reusable parts. The pagination links() method generates navigation automatically.

Testing the Blog

Write a feature test to verify that your blog works correctly. Test creating, viewing, and deleting posts.


namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;

class BlogTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_create_post()
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/posts', [
            'title' => 'My Blog Post',
            'body' => 'This is the body of my blog post.',
        ]);

        $response->assertRedirect();
        $this->assertDatabaseHas('posts', [
            'title' => 'My Blog Post',
        ]);
    }

    public function test_guest_cannot_create_post()
    {
        $response = $this->post('/posts', [
            'title' => 'Test',
            'body' => 'Content',
        ]);

        $response->assertRedirect('/login');
    }

    public function test_post_index_displays_posts()
    {
        Post::factory()->count(5)->create();

        $response = $this->get('/');
        $response->assertOk();
    }
}
    

Run your tests with php artisan test. Every assertion gives you confidence that your blog works as expected.