9 min read

Mastering Design Patterns in Laravel

Unraveling the Secrets Behind Laravel’s Architectural Mastery

Laravel PHP Design Patterns Oop Concepts Backend
Mastering Design Patterns in Laravel

Introduction

Laravel, a PHP framework known for its elegance and simplicity, is a treasure trove of software design patterns. These patterns are not just academic concepts but practical solutions that address common problems in web application development. Whether you’re a beginner eager to explore Laravel’s capabilities, a mid-level developer aiming to refine your skills, or an expert seeking to deepen your understanding, this post is your compass to navigating the world of design patterns in Laravel.

Design patterns in Laravel provide a structured approach to solving software design problems, making code more maintainable, scalable, and testable. By the end of this guide, you’ll have a solid grasp of how Laravel employs these patterns, complete with real-code examples to illuminate the path.

1. Model-View-Controller (MVC)

A software architectural pattern that separates application functionality into three interconnected components: the model, the view, and the controller.

Purpose: To separate the internal representations of information from the ways information is presented to and accepted from the user.

Benefits: Simplifies the development process by segregating the database logic, user interface, and the user input. This separation helps manage complex applications, reduces dependencies between the system components, and makes scaling and maintaining the application easier.

Example: Creating a Basic User Management System

  • Model (App\Models\User.php): Handles data and business logic.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    // User-specific logic
}
  • View (resources/views/users/index.blade.php): Displays data.
<ul>
    @foreach ($users as $user)
        <li>{{ $user->name }}</li>
    @endforeach
</ul>
  • Controller (App\Http\Controllers\UserController.php): Processes requests.
namespace App\Http\Controllers;
use App\Models\User;

class UserController extends Controller
{
    public function index()
    {
        $users = User::all();
        return view('users.index', ['users' => $users]);
    }
}

2. Service Container and Dependency Injection

A design pattern used to implement dependency injection, allowing classes to outsource the creation and management of their dependencies to an external object.

Purpose: To manage class dependencies and provide a central place for obtaining objects and managing their life cycles.

Benefits: Enhances modularity and testability by decoupling the creation of objects from their usage. It simplifies the management of dependencies and promotes more maintainable and flexible code.

Example: Injecting a Custom Service into a Controller

namespace App\Http\Controllers;
use App\Services\CustomService;

class MyController extends Controller
{
    private $service;

    public function __construct(CustomService $service)
    {
        $this->service = $service;
    }

    public function show()
    {
        return $this->service->performAction();
    }
}

3. Facades

A structural design pattern that provides a simplified interface to a complex subsystem, library, or framework.

Purpose: To offer a single, unified, and easy-to-use interface over a set of interfaces or systems, simplifying the complexities behind them.

Benefits: Allows easier access to complex functionalities with a straightforward approach, minimizing the learning curve for new developers and enhancing code readability.

Example: Using the Cache Facade

use Illuminate\Support\Facades\Cache;

class EventController extends Controller
{
    public function store()
    {
        Cache::put('key', 'value', $minutes);
    }
}

4. Repository Pattern

A design pattern that abstracts the data layer, creating a boundary between the application’s business logic and the data access logic.

Purpose: To organize the data access logic and business logic, making the system more readable, maintainable, and easier to update or change data sources.

Benefits: Promotes a cleaner separation of concerns and enhances the ability to unit test components in isolation. It simplifies data access logic by abstracting it into reusable interfaces.

Example: Implementing a User Repository

  • Interface (App\Repositories\UserRepositoryInterface.php):
namespace App\Repositories;

interface UserRepositoryInterface
{
    public function all();
}

Implementation (App\Repositories\EloquentUserRepository.php):

namespace App\Repositories;
use App\Models\User;

class EloquentUserRepository implements UserRepositoryInterface
{
    public function all()
    {
        return User::all();
    }
}

5. Observer Pattern

A behavioral design pattern where an object, known as the subject, maintains a list of its dependents, called observers, and notifies them of any state changes.

Purpose: To allow for a subscription mechanism to notify multiple objects about any events that happen to the object they’re observing.

Benefits: Enables a publish/subscribe relationship that reduces coupling between the components of an application and allows for a dynamic addition or removal of observers.

Example: Creating a User Observer

namespace App\Observers;
use App\Models\User;

class UserObserver
{
    public function created(User $user)
    {
        // Actions to take after a user is created
    }
}

6. Strategy Pattern

A behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable.

Purpose: To enable the algorithm’s behavior to be selected at runtime based on the context.

Benefits: Provides flexibility to choose the appropriate algorithm at runtime. It simplifies unit testing and promotes a cleaner and more modular code structure.

Example: Implementing Payment Strategies

  • Strategy Interface (App\Contracts\PaymentGatewayInterface.php):
namespace App\Contracts;

interface PaymentGatewayInterface
{
    public function charge($amount);
}

  • Concrete Strategies for different payment gateways (App\Services\StripePaymentGateway.php):
namespace App\Services;
use App\Contracts\PaymentGatewayInterface;

class StripePaymentGateway implements PaymentGatewayInterface
{
    public function charge($amount)
    {
        // Stripe charging logic
    }
}
  • Usage within a service or controller, dynamically choosing the strategy:
use App\Contracts\PaymentGatewayInterface;

class PaymentService
{
    protected $paymentGateway;

    public function __construct(PaymentGatewayInterface $paymentGateway)
    {
        $this->paymentGateway = $paymentGateway;
    }

    public function charge($amount)
    {
        $this->paymentGateway->charge($amount);
    }
}

7. Service Provider

In Laravel, a service provider is a way to bind services, register them, and perform bootstrapping tasks within the framework.

Purpose: To define and register services and configurations that the application will use.

Benefits: Acts as the central place for application configuration and service bootstrapping, making it easier to manage and scale the application’s backend logic.

Example: Registering a Custom Service

  • Service Provider (App\Providers\CustomServiceProvider.php):
namespace App\Providers;
use Illuminate\Support\ServiceProvider;

class CustomServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind('customService', function ($app) {
            return new \App\Services\CustomService();
        });
    }
}
  • Bootstrapping the provider in config/app.php:
'providers' => [
    // Other Service Providers

    App\Providers\CustomServiceProvider::class,
],
  • Accessing CustomService in a Controller:
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class MyCustomController extends Controller
{
    protected $customService;

    // Laravel's service container will automatically resolve the customService
    // when MyCustomController is instantiated.
    public function __construct(\App\Services\CustomService $customService)
    {
        $this->customService = $customService;
    }

    public function index()
    {
        // Use the customService for something
        $result = $this->customService->performSomeAction();

        return response()->json($result);
    }
}

8. Singleton Pattern

A creational design pattern that ensures a class has only one instance and provides a global point of access to that instance.

Purpose: To control access to a shared resource, such as a database or file system, by ensuring only one instance of the class is created.

Benefits: Reduces unnecessary memory usage by limiting the number of instances and provides a controlled access point to the resource.

Example: Using Singleton for a Helper Service

  • Defining a service as a singleton
$this->app->singleton('HelperService', function ($app) {
    return new \App\Services\HelperService();
});
  • Using HelperService Singleton in a Controller:
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class HelperController extends Controller
{
    public function show()
    {
        // Access the singleton instance of HelperService directly via the app() helper
        $helperService = app('HelperService');

        // Now you can use the HelperService for something
        $data = $helperService->getSomeData();

        return response()->json($data);
    }
}

9. Builder (Manager) Pattern

A creational design pattern that allows for the construction of complex objects step by step.

Purpose: To separate the construction of a complex object from its representation, allowing the same construction process to create different representations.

Benefits: Enhances control over the construction process, allows for flexibility in the object’s construction, and encapsulates the construction logic.

Example: Fluent Query Builder Usage

use Illuminate\Support\Facades\DB;

$users = DB::table('users')
            ->where('active', 1)
            ->orderBy('name', 'asc')
            ->get();

// Building Complex Queries

// Begin building the query
$query = DB::table('users')
    ->select('users.id', 'users.name', 'profiles.photo')
    ->join('profiles', 'users.id', '=', 'profiles.user_id')
    ->where('users.active', 1);


// Conditionally add more constraints to the query in a step-by-step manner
if (request()->has('age')) {
    $query->where('profiles.age', '>=', request()->input('age'));
}


// Finalize the query with ordering and retrieval
$users = $query->orderBy('users.name', 'asc')->get();

10. Factory Pattern

A creational design pattern that uses factory methods to deal with the problem of creating objects without specifying the exact class of object that will be created.

Purpose: To create instances of classes without having to specify the exact class type, making the system more modular and extensible.

Benefits: Promotes loose coupling by reducing the dependency of the application on concrete classes. It simplifies the addition of new classes to the application by only requiring that they implement an interface.

Example: Dynamic Notification Sender

  • Notification Interface
namespace App\Contracts;

interface NotificationInterface
{
    public function send($message);
}
  • Concrete Classes
namespace App\Notifications;

use App\Contracts\NotificationInterface;

class EmailNotification implements NotificationInterface
{
    public function send($message)
    {
        // Send email logic
    }
}

class SMSNotification implements NotificationInterface
{
    public function send($message)
    {
        // Send SMS logic
    }
}
  • Notification Factory
namespace App\Factories;

use App\Contracts\NotificationInterface;
use App\Notifications\EmailNotification;
use App\Notifications\SMSNotification;

class NotificationFactory
{
    public static function create($type): NotificationInterface
    {
        switch ($type) {
            case 'email':
                return new EmailNotification();
            case 'sms':
                return new SMSNotification();
            default:
                throw new \Exception("Notification type {$type} not supported");
        }
    }
}
  • Usage
use App\Factories\NotificationFactory;

class NotificationService
{
    public function sendNotification($type, $message)
    {
        $notification = NotificationFactory::create($type);
        $notification->send($message);
    }
}

Conclusion

Understanding and leveraging design patterns in Laravel can significantly enhance your application’s architecture, making it more efficient, scalable, and maintainable. Each pattern serves a distinct purpose, from simplifying development with MVC and Facades to enhancing flexibility with Strategy and Builder patterns. By mastering these patterns, you’ll be able to tap into the full potential of Laravel, crafting elegant solutions to complex problems with ease and confidence.

As you continue to explore Laravel, keep these patterns in mind. They’re not just tools but keys to unlocking the framework’s true power, allowing you to write cleaner, more efficient code that stands the test of time. Whether you’re just starting out or looking to deepen your Laravel expertise, these design patterns will be invaluable companions on your development journey.


Need Help With Your Laravel Project?

I specialize in building custom Laravel applications, process automation, and SaaS development. Whether you need to eliminate repetitive tasks or build something from scratch, let's discuss your project.

⚡ Currently available for 2-3 new projects

Hafiz Riaz

About Hafiz Riaz

Full Stack Developer from Turin, Italy. I build web applications with Laravel and Vue.js, and automate business processes. Creator of ReplyGenius, StudyLab, and other SaaS products.

View Portfolio →