Laravel Advanced Tutorial - From MVC to Repository Pattern & Caching
🚀 Laravel Advanced: From MVC to Repository Pattern & Caching
📑 Table of Contents
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.
- 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');
});
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.
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',
];
}
$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
| Field | Rule |
|---|---|
| name | Required text, max 100 characters |
| message | Required text, max 1,000 characters |
4. Dynamic Navigation with View Composers
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);
});
}
* 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.
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);
// ...
}
}
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',
];
}
| Cast | DB Value | PHP Value |
|---|---|---|
date | '1995-08-15' | Carbon instance |
boolean | 1 / 0 | true / false |
array | JSON string | PHP array |
decimal:2 | 1250.50 | String 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');
}
- 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
- Start Simple: Closures in
web.phpare fine for prototypes. - Move to MVC: Controllers keep routes clean and testable.
- Use View Composers: Share data across all views without repetition.
- Decouple with Interfaces: The Repository Pattern lets you swap implementations easily.
- Leverage Eloquent Casting: Let Laravel handle type conversion for you.
- Cache Wisely: Always invalidate cache when data changes.
- Test at Every Level: Tinker for exploration, PHPUnit for automation, cURL for integration.