Skip to content
Laravel PHP Framework Guide — Modern MVC Development

Laravel PHP Framework Guide — Modern MVC Development

DodaTech Updated Jun 6, 2026 8 min read

Laravel is the most popular PHP framework — it provides expressive, elegant syntax with a robust ecosystem including Eloquent ORM, Blade templates, Artisan CLI, and extensive tooling for modern web development.

What You’ll Learn

By the end of this guide, you’ll understand Laravel’s architecture, work with Eloquent ORM and Blade, create routes and middleware, and build modern PHP applications.

Why Laravel Matters

Laravel dominates modern PHP development — it powers everything from startups to enterprise applications. At DodaTech, we build Durga Antivirus Pro’s customer portal and DodaZIP’s premium subscription service with Laravel. Its ecosystem includes Forge (server management), Vapor (serverless), Nova (admin panels), and Cashier (billing) — tools that accelerate development dramatically.

Laravel Architecture

    flowchart TD
  A[Request] --> B[Route]
  B --> C[Middleware]
  C --> D[Controller]
  D --> E[Eloquent ORM]
  D --> F[Blade View]
  E --> G[(Database)]
  F --> H[Response]
  
Prerequisites: Strong PHP OOP knowledge, MVC pattern understanding, Composer familiarity, and basic MySQL experience.

Key Concepts

Eloquent ORM — Beautiful ActiveRecord

class Product extends Model
{
    protected $fillable = ['name', 'price', 'description'];

    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    public function scopeInStock($query)
    {
        return $query->where('stock', '>', 0);
    }
}

// Usage — reads like English
Product::inStock()->where('price', '<', 100)->get();
$product = Product::find(1);
$product->update(['price' => 19.99]);

Eloquent makes database queries read like natural language. Product::inStock()->where('price', '<', 100)->get() — you can almost read this out loud.

Blade Templating

@extends('layouts.app')

@section('content')
    <h1>Products</h1>
    @foreach ($products as $product)
        <div class="card">
            <h2>{{ $product->name }}</h2>
            <p>Price: {{ $product->price }}</p>
        </div>
    @endforeach
@endsection

Blade is lightweight but powerful — with template inheritance, components, and sections, all compiled to plain PHP for performance.

Artisan CLI

php artisan make:model Product -m      # Model + migration
php artisan make:controller ProductController --resource
php artisan migrate                     # Run migrations
php artisan tinker                      # Interactive shell
php artisan route:list                  # Show all routes

Artisan is Laravel’s command center — over 100+ commands for generating code, managing databases, running tasks, and more.

Queues and Jobs

Laravel’s queue system offloads slow tasks (email, file processing, API calls) to background workers:

namespace App\Jobs;

use App\Models\Product;
use App\Services\ThumbnailService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Bus\Queueable;

class GenerateProductThumbnails implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable;

    public function __construct(
        public Product $product
    ) {}

    public function handle(ThumbnailService $thumbnails): void
    {
        $paths = $thumbnails->generate(
            $this->product->image_path,
            [200, 400, 800]
        );

        $this->product->update([
            'thumb_small'  => $paths[200],
            'thumb_medium' => $paths[400],
            'thumb_large'  => $paths[800],
        ]);

        logger("Thumbnails generated for product #{$this->product->id}");
    }
}

Dispatch the job from anywhere:

// In a controller
GenerateProductThumbnails::dispatch($product);

// Delayed dispatch
GenerateProductThumbnails::dispatch($product)->delay(now()->addMinutes(5));

// After a database transaction commits
dispatch(new GenerateProductThumbnails($product))->afterCommit();

Run the queue worker:

php artisan queue:work

Expected output in storage/logs/laravel.log:

[2026-06-06 10:23:45] local.DEBUG: Thumbnails generated for product #42

The queue system supports Redis, Amazon SQS, Beanstalkd, and database drivers — you can switch backends by changing QUEUE_CONNECTION in .env. DodaTech uses Redis queues in DodaZIP to process file conversions asynchronously, keeping the API responsive even under heavy load.

Events and Listeners

Laravel’s event system lets you decouple logic across your application:

namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderShipped
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public Order $order
    ) {}
}

Create a listener:

namespace App\Listeners;

use App\Events\OrderShipped;
use App\Notifications\ShipmentConfirmation;
use Illuminate\Support\Facades\Log;

class SendShipmentNotification
{
    public function handle(OrderShipped $event): void
    {
        $event->order->user->notify(
            new ShipmentConfirmation($event->order)
        );

        Log::info('Shipment notification sent', [
            'order_id' => $event->order->id,
            'user_id'  => $event->order->user_id,
        ]);
    }
}

Register the mapping in App\Providers\EventServiceProvider:

protected $listen = [
    OrderShipped::class => [
        SendShipmentNotification::class,
        UpdateInventory::class,
        SendAnalytics::class,
    ],
];

Multiple listeners can respond to a single event without the event emitter knowing about them — a textbook observer pattern.

How Laravel Works Under the Hood

Laravel’s request lifecycle involves several key components:

    flowchart TD
    A[public/index.php] --> B[Service Container Bootstrap]
    B --> C[Kernel: handle Request]
    C --> D[Middleware Pipeline]
    D --> E[Router: match]
    E --> F[Controller Dispatch]
    F --> G[Service Container Resolution]
    G --> H[Response]
    H --> I[Terminable Middleware]
  
  1. public/index.php is the single entry point. It autoloads Composer dependencies, creates the Laravel application instance, and binds the kernel into the service container.

  2. Service Container — Laravel’s DI container is the most powerful in PHP. It resolves classes by their type-hints, manages singleton bindings, and stores contextual bindings. When you type-hint ProductRepository $repo in a controller, the container recursively resolves all of $repo’s dependencies.

  3. Kernel — the HTTP kernel (App\Http\Kernel) defines middleware stacks (global and route-specific) and sends the request through them in a pipeline.

  4. Middleware pipeline — the request passes through each middleware layer (EncryptCookies, StartSession, VerifyCsrfToken, etc.). Each middleware can modify the request or return a response early.

  5. Router — matches the HTTP method and URI against defined routes. It resolves the controller and method from the route definition.

  6. Controller dispatch — the matched controller method is called. Laravel’s container resolves the controller and injects any type-hinted dependencies.

  7. Service providers — all service providers listed in config/app.php are registered and booted during the bootstrap phase. This is how facades, database, queue, mail, and every other Laravel feature hooks into the application.

  8. Facades — classes like Cache::, DB::, Queue:: are static proxies to underlying container instances. Cache::get('key') is syntactic sugar for $app->make('cache')->get('key'). The facade’s getFacadeAccessor() returns the container binding name.

Common Mistakes

1. Not using Eloquent’s lazy loading detection — Enable Model::preventLazyLoading() in development to catch N+1 query issues before they reach production.

2. Putting business logic in controllers — Controllers should be thin. Move logic to dedicated classes (services, actions, or repositories).

3. Forgetting to use mass-assignment protection — Always define $fillable or $guarded in models. Without it, users could modify any column.

4. Not caching config in production — Run php artisan config:cache in production. Without it, config files are loaded on every request.

5. Overusing Facades in tests — Facades make testing harder. Use dependency injection for better testability.

Practice Questions

1. What’s the difference between php artisan make:model Product -m and without -m?

The -m flag also generates a migration file for the model’s database table.

2. How does Eloquent prevent N+1 queries?

Eager loading with Product::with('category')->get() loads all related data in one query instead of one per product.

3. What’s the purpose of $fillable in a model?

It defines which attributes can be mass-assigned (e.g., Product::create($request->all())), preventing unexpected column modification.

4. Challenge: Create a Laravel route, controller, and Blade view for a product listing page that filters by category.

// Route
Route::get('/products/{category?}', [ProductController::class, 'byCategory']);

// Controller
public function byCategory(?string $category = null)
{
    $products = $category
        ? Product::whereHas('category', fn($q) => $q->where('slug', $category))->get()
        : Product::all();
    return view('products.index', compact('products'));
}

FAQ

What makes Laravel different from other PHP frameworks?
Laravel’s expressive syntax, extensive ecosystem (Forge, Vapor, Nova, Cashier, Sail), Eloquent ORM, Blade templating, and strong community make it the most popular PHP framework.
How does Eloquent compare to Doctrine?
Eloquent is simpler (ActiveRecord pattern). Doctrine (used by Symfony) is more powerful with a Data Mapper pattern. Eloquent is easier for most projects.
What is Laravel Sail?
A light-weight command-line interface for interacting with Laravel’s default Docker configuration — makes local development easy without installing PHP/MySQL directly.

Try It Yourself

composer create-project laravel/laravel myapp
cd myapp
php artisan make:model Product -m
# Edit migration, then:
php artisan migrate
php artisan tinker
# Try: Product::create(['name' => 'Test', 'price' => 9.99])

Real-World Task: Order Processing Pipeline

Build a complete order processing flow using events, queues, and notifications. This mirrors the subscription billing system in DodaZIP’s premium service.

Step 1: Create the event:

php artisan make:event OrderPlaced
namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderPlaced
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public Order $order
    ) {}
}

Step 2: Create listeners for different concerns:

php artisan make:listener ProcessPayment --event=OrderPlaced
php artisan make:listener UpdateInventory --event=OrderPlaced
php artisan make:listener SendOrderConfirmation --event=OrderPlaced

Each listener handles one responsibility:

namespace App\Listeners;

use App\Events\OrderPlaced;
use App\Services\PaymentGateway;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;

class ProcessPayment implements ShouldQueue
{
    public function __construct(
        private PaymentGateway $gateway
    ) {}

    public function handle(OrderPlaced $event): void
    {
        $charge = $this->gateway->charge(
            $event->order->total,
            $event->order->payment_token
        );

        $event->order->update([
            'paid_at'  => now(),
            'charge_id' => $charge->id,
        ]);

        Log::info("Payment processed for order #{$event->order->id}");
    }
}

Step 3: In your controller, fire the event:

public function store(Request $request): RedirectResponse
{
    $order = Order::create($request->validated());

    event(new OrderPlaced($order));

    return redirect()->route('orders.show', $order)
        ->with('success', 'Order placed!');
}

Step 4: Run the queue worker (since listeners implement ShouldQueue):

php artisan queue:work

Expected output — the database shows payment processed, inventory decremented, and the customer receives an email — all happening asynchronously in the background.

Challenge: Add a PaymentFailed listener that retries failed payments up to 3 times, then sends an admin alert if the payment can’t be processed. Use Laravel’s $tries property on the job class.

What’s Next

LessonDescription
https://tutorials.dodatech.com/backend/php/symfony/Enterprise PHP framework
https://tutorials.dodatech.com/backend/php/yii/High-performance framework
https://tutorials.dodatech.com/backend/php/cakephp/Rapid development framework
PHPCore PHP advanced OOP
MySQLDatabase for Laravel apps

What’s Next

Congratulations on completing this Laravel tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro