Laravel Advanced Tutorial - From MVC to Repository Pattern & Caching

Laravel Advanced Tutorial - From MVC to Repository Pattern & Caching

🚀 Laravel Advanced: From MVC to Repository Pattern & Caching

.

1. Introduction

This tutorial takes you on a journey from the very basics of Laravel routing to advanced architectural patterns. By the end, you'll have built two complete applications — a Guestbook and a Contactbook — while learning industry-standard practices like the Repository Pattern, Dependency Injection, Eloquent Casting, and Caching.

🎯 What You'll Learn:
  • Laravel's Request Flow (Routes → Controllers → Views)
  • MVC Architecture & Resource Controllers
  • Eloquent Models, Migrations & Seeders
  • View Composers for Dynamic Navigation
  • Repository Pattern with Interfaces & Dependency Injection
  • Eloquent Casting (Date, Boolean, Array, Decimal)
  • Laravel Cache with Automatic Invalidation
  • Testing with Tinker, cURL & Feature Tests

2. Getting Started with Laravel Blade Breeze

Laravel follows a convention-over-configuration philosophy. The entry point for all web requests is routes/web.php.

2.1 Your First Route

// routes/web.php
Route::get('/guestbooktest', function () {
    return view('guestbooktest.index');
});
💡 Dot Notation Magic: Laravel automatically maps guestbooktest.index to resources/views/guestbooktest/index.blade.php — dots become slashes, and .blade.php is appended automatically.

2.2 A Simple Blade Template

<!-- resources/views/guestbooktest/index.blade.php -->
<!DOCTYPE html>
<html>
<head><title>Guestbook Test</title></head>
<body>
    <h1>Hello Guest</h1>
</body>
</html>

3. MVC Architecture – Building the Guestbook App

As applications grow, closures in web.php become unmanageable. Laravel uses the Model–View–Controller pattern to separate concerns.

Browser → /guestbook → routes/web.php → GuestbookController@index → guestbook.index → Browser

3.1 Generate Model & Migration

php artisan make:model Guestbook -m
// database/migrations/xxxx_xx_xx_create_guestbooks_table.php
public function up(): void
{
    Schema::create('guestbooks', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->text('message');
        $table->timestamps();
    });
}
php artisan migrate

3.2 Configure the Model

// app/Models/Guestbook.php
class Guestbook extends Model
{
    protected $fillable = [
        'name',
        'message',
    ];
}
🔒 Mass Assignment Protection: The $fillable array is a security safeguard — it tells Eloquent exactly which fields can be set via Guestbook::create([...]).

3.3 The Resource Controller (Trimmed Down)

php artisan make:controller GuestbookController --resource

Since our guestbook only needs view, create, and submit (no edit/delete), we trim it down:

// app/Http/Controllers/GuestbookController.php
class GuestbookController extends Controller
{
    public function index()
    {
        $guestbooks = Guestbook::latest()->get();
        return view('guestbook.index', compact('guestbooks'));
    }

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

    public function store(Request $request)
    {
        $validated = $request->validate([
            'name'    => ['required', 'string', 'max:100'],
            'message' => ['required', 'string', 'max:1000'],
        ]);
        Guestbook::create($validated);

        return redirect()->route('guestbook.index')
            ->with('success', 'Your message has been added.');
    }
}

3.4 Register Limited Routes

// routes/web.php
Route::resource('guestbook', GuestbookController::class)
    ->only(['index', 'create', 'store']);

3.5 Validation Rules Summary

FieldRule
nameRequired text, max 100 characters
messageRequired text, max 1,000 characters

Instead of hardcoding navigation links, we'll store them in the database and inject them into every view automatically using a View Composer.

4.1 Navbar Model & Seeder

php artisan make:model Navbar -m
php artisan make:seeder NavbarSeeder
// database/seeders/NavbarSeeder.php
public function run(): void
{
    Navbar::create(['name'=>'Guestbook',   'route'=>'guestbook.index', 'ordering'=>1]);
    Navbar::create(['name'=>'Add Message', 'route'=>'guestbook.create', 'ordering'=>2]);
    Navbar::create(['name'=>'About Us',    'route'=>'about', 'ordering'=>3]);
    Navbar::create(['name'=>'Contact Us',  'route'=>'contact', 'ordering'=>4]);
}
php artisan db:seed --class=NavbarSeeder

4.2 The View Composer

// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\View;
use App\Models\Navbar;

public function boot(): void
{
    View::composer('*', function ($view) {
        $navbars = Navbar::orderBy('ordering')->get();
        $view->with('navbars', $navbars);
    });
}
✨ Why View Composers? The * wildcard means every Blade view automatically receives a $navbars variable. No need to fetch navigation data in every controller!

4.3 Dynamic Navbar in Layout

<!-- resources/views/layouts/guestbookapp.blade.php -->
<nav>
    @foreach ($navbars as $navbar)
        <a href="{{ route($navbar->route) }}">
            {{ $navbar->name }}
        </a>
    @endforeach
</nav>

5. Refactoring with Repository Pattern & Interfaces

To decouple our controller from Eloquent, we introduce a Repository Interface. This is the foundation of clean, testable architecture.

Browser ↓ GuestbookController ↓ GuestbookRepositoryInterface ↓ EloquentGuestbookRepository ↓ Guestbook Model → Database

5.1 Define the Interface

// app/Contracts/GuestbookRepositoryInterface.php
interface GuestbookRepositoryInterface
{
    public function getAll(): Collection;
    public function create(array $data);
}

5.2 Implement the Repository

// app/Repositories/EloquentGuestbookRepository.php
class EloquentGuestbookRepository implements GuestbookRepositoryInterface
{
    public function getAll(): Collection
    {
        return Guestbook::latest()->get();
    }

    public function create(array $data)
    {
        return Guestbook::create($data);
    }
}

5.3 Bind in the Service Container

// app/Providers/AppServiceProvider.php
public function register(): void
{
    $this->app->bind(
        GuestbookRepositoryInterface::class,
        EloquentGuestbookRepository::class
    );
}

5.4 Inject into the Controller

class GuestbookController extends Controller
{
    private GuestbookRepositoryInterface $guestbooks;

    public function __construct(GuestbookRepositoryInterface $guestbooks)
    {
        $this->guestbooks = $guestbooks;
    }

    public function index()
    {
        $guestbooks = $this->guestbooks->getAll();
        return view('guestbook.index', compact('guestbooks'));
    }

    public function store(Request $request)
    {
        $validated = $request->validate([...]);
        $this->guestbooks->create($validated);
        // ...
    }
}
🧠 Why This Matters: The controller no longer knows about Eloquent. You can swap EloquentGuestbookRepository for a different implementation (e.g., an API-based one) without touching the controller. This is the essence of Dependency Inversion.

6. Contactbook App – Eloquent Casting & Caching

The Contactbook demonstrates advanced Eloquent features: casting and caching.

6.1 Migration with Rich Data Types

Schema::create('contacts', function (Blueprint $table) {
    $table->id();
    $table->string('name', 100);
    $table->string('email')->unique();
    $table->string('phone', 30)->nullable();
    $table->date('birthday')->nullable();
    $table->boolean('is_active')->default(true);
    $table->json('interests')->nullable();
    $table->decimal('account_balance', 10, 2)->default(0);
    $table->timestamps();
});

6.2 Eloquent Casting

// app/Models/Contact.php
protected function casts(): array
{
    return [
        'birthday'        => 'date',
        'is_active'       => 'boolean',
        'interests'       => 'array',
        'account_balance' => 'decimal:2',
    ];
}
CastDB ValuePHP Value
date'1995-08-15'Carbon instance
boolean1 / 0true / false
arrayJSON stringPHP array
decimal:21250.50String with 2 decimals

6.3 Smart Caching with Auto-Invalidation

// app/Http/Controllers/ContactController.php
public function index()
{
    $contacts = Cache::remember(
        'contacts.all',
        now()->addMinutes(10),
        function () {
            return Contact::latest()->get();
        }
    );
    return view('contactbook.index', compact('contacts'));
}

public function store(StoreContactRequest $request)
{
    // ... validation and creation ...
    Contact::create($validated);
    Cache::forget('contacts.all');  // ← invalidate stale cache
    return redirect()->route('contacts.index');
}
⚡ Cache Flow:
  • First visit → cache miss → query DB → store result in cache
  • Subsequent visits → cache hit → return instantly
  • New contact added → Cache::forget() clears stale data

7. Testing with Tinker, cURL & PHPUnit

7.1 Tinker – Manual Exploration

php artisan tinker

# Resolve the repository through the container
$repo = app(App\Contracts\GuestbookRepositoryInterface::class);
get_class($repo);  // → App\Repositories\EloquentGuestbookRepository

# Test methods
$repo->getAll()->count();
$repo->create(['name'=>'Ali', 'message'=>'Hello!']);

7.2 Automated Feature Test

php artisan make:test EloquentGuestbookRepositoryTest
class EloquentGuestbookRepositoryTest extends TestCase
{
    use RefreshDatabase;

    public function test_repository_can_create_a_guestbook_record(): void
    {
        $repository = app(GuestbookRepositoryInterface::class);
        $guestbook = $repository->create([
            'name'    => 'Ali',
            'message' => 'Testing repository directly.',
        ]);

        $this->assertInstanceOf(Guestbook::class, $guestbook);
        $this->assertDatabaseHas('guestbooks', [
            'name'    => 'Ali',
            'message' => 'Testing repository directly.',
        ]);
    }
}
php artisan test --filter=EloquentGuestbookRepositoryTest

7.3 cURL – API-Style Testing

# Linux / macOS
curl -H "Accept: application/json" http://127.0.0.1:8000/contacts

# Grab CSRF token from form
TOKEN=$(grep -oP 'name="_token"\s+value="\K[^"]+' contact-form.html)

# Submit a new contact
curl -X POST -b cookies.txt -c cookies.txt \
  -H "X-CSRF-TOKEN: $TOKEN" \
  --data-urlencode "name=Ahmad Rahman" \
  --data-urlencode "email=ahmad@example.com" \
  --data-urlencode "account_balance=500.75" \
  http://127.0.0.1:8000/contacts

🎯 Key Takeaways

  1. Start Simple: Closures in web.php are fine for prototypes.
  2. Move to MVC: Controllers keep routes clean and testable.
  3. Use View Composers: Share data across all views without repetition.
  4. Decouple with Interfaces: The Repository Pattern lets you swap implementations easily.
  5. Leverage Eloquent Casting: Let Laravel handle type conversion for you.
  6. Cache Wisely: Always invalidate cache when data changes.
  7. Test at Every Level: Tinker for exploration, PHPUnit for automation, cURL for integration.

📘 Laravel Advanced Workshop • • Built with ❤️ and Laravel 11

Resources: Laravel Docs | PHP Sandbox