DEV Community

Simeon
Simeon

Posted on

Understanding Design Patterns With Laravel

Introduction

​​In object-oriented languages, software developers do what's needed to achieve their desired outcome. There are numerous kinds of objects, situations and problems in an application.
​​As a result, multiple strategies are required to tackle various problems. Implementing design patterns is crucial for improving the quality of online apps.
​​When a developer starts to consider issues like how to make the code more legible, maintainable, and upgradeable, he will run into design patterns.
​​There are particular design patterns for each programming language. We'll learn about the design patterns Laravel uses, and how and why are they used, with examples.

Design Patterns

Design patterns are established solutions to recurring problems in programming. They offer a reusable, optimized, and cost-effective approach to problem-solving and achieving desired outcomes. Designed by Eric Gamma and his colleagues in 1994, design patterns provide developers with a standardized and readable way of coding. Implementing design patterns in a project ensures clean and efficient code, as they have already been tested and used successfully by many other developers in similar situations. Design patterns not only provide robust systems but also offer friendly and efficient architectural solutions to common problems.

Benefits of using Design Patterns

  • Implements the code reusability concept

  • Easier Maintenance and Documentation

  • Improved readability

  • Ease in finding appropriate objects

  • Better specification of object interfaces

  • Ease of implementation even for complex software projects.

Factory Pattern

As a hypothetical scenario, consider making cake. It requires several components, like eggs and milk. Obtaining milk involves sourcing it from a dairy animal and undergoing pasteurization, which might be tasking. However, instead of going through this procedure, milk can be easily obtained from a grocery store. Therefore, in this instance, the grocery store is deemed the creator of the milk, and it is of no concern how the milk is produced, as long as it can be purchased, a similar logic applies to the other components required.

The factory pattern is a method for object creation that solves the problem of creating complex objects in a way that is maintainable, reusable, and testable. It acts as a central location for object creation and abstracts the process of object creation by providing an interface for it, without specifying the exact concrete class to be used.
Let’s go with an example;
The primary function of the cake factory is to produce cake and the details of the process are not important (abstract class), all that matters is the end product (instance). To be considered a valid factory, certain methods must be in place. For example, the cake factory produces the same type of cake, but with varying specifications.

interface Cakefactorycontract
{  
  public function make(array $toppings  =[ ]): Cake;
 }
Class Cakefactory implements Cakefactorycontract
{
     public function make(array $toppings = [ ]: Cake
       {
          return new Cake($toppings);
         }
}
$cake = (new Cakefactory)->make([ ‘fruits’ , ‘Oreos’ , ‘candy’ ]);


Enter fullscreen mode Exit fullscreen mode

So, if we look at this simple cake factory example, we have a Cake factory which is responsible for making a cake, and in the end, we get an instance of a cake.

Laravel implements the factory pattern, not only for creating complex objects, but also for generating views.
Let’s see an example:

<?php
Class Postcontroller
{
  public function index( ): \Illuminate\view\view
    {
         $posts = Post: : all( );
           return view (’posts.index’ , [’posts’ => $posts]); 
     }
}

Enter fullscreen mode Exit fullscreen mode

In this scenario, we utilize the view helper and call the view method. The view method acts as the helper method and eventually calls the factory. We provide the view with the necessary data for it to construct.
When you return a view in Laravel, it utilizes the factory pattern in the background to generate the view. This pattern is also utilized for creating notifications and validators.

/ / Illuminate\foundation\helpers.php

/**
*@return \Illuminate\view\view|\Illuminate\contracts\view\factory
*/
function view ($view = null,  $data  = [ ],  $mergedata = [ ])
{
  $factory =  app(viewFactory: :class): 
   if (func_num_args( ) === 0)  {  
        return $factory:
   }
   return $factory->make($view, data, $merge data): 
 }


Enter fullscreen mode Exit fullscreen mode

When we examine the helper method, we can see that a factory called the view factory is formed. The make() function, which is a factory method that ultimately generates a view and returns it back to us, is called by this factory. The views we utilize in our controller are produced by this factory.
This makes it easier for us to reuse and maintain clean code. Now we have a designated location for creating the view, which is easily maintainable, reusable, and testable.

Builder Pattern

Having learned how to make the cake ingredients from the previous abstract example, we now have the capability to assemble a cake using the factory. Each cake is different. To accommodate this, you can create a new builder for your cake. The cake baker acts as the builder, constructing various cakes to suit your personal taste.

The builder pattern is tasked with constructing complicated objects and utilizes factories for this purpose. In Laravel, it is commonly referred to as the manager pattern. The objective of this design pattern is to create simpler and reusable objects by separating complex object construction layers from the rest of the application, allowing for the separated layers to be utilized in various parts of the program.
The primary objective of all design patterns is to create objects. However, some objects can be inherently complex, and in such situations, it can be useful to have a director and builder to manage the construction process.
Using our previous example, let’s say we have a builder class called "CakeBuilder".
.

class Cakebuilder
 {
      public function make(CakebuilderInterface $cake): Cake
      {
          //returns object of type cake
       }
}
Enter fullscreen mode Exit fullscreen mode

All objects of the "Builder" type should conform to the same interface, which is defined by the CakebuilderInterface. The "make" method, specified by the interface, accepts an object that implements the CakebuilderInterfaceand returns a Cake object.
Therefore, when we call the "make method," our goal is to perform these steps: prepare the cake, add toppings, bake it, and ultimately obtain a Cakeobject.

class Cakebuilder
{ 
   public function make(CakeBuilderInterface $cake): Cake
    {
           return $cake
                   ->prepare()
                   ->applyToppings()
                   ->bake()
        }
 }

Enter fullscreen mode Exit fullscreen mode

The Cakebuilderuses these methods which every Cakebuilderobject must adhere to;

interface Cakebuilder interface
  {
      public function prepare(): Cake;
      public function applyToppings(): Cake;
      public function bake(): Cake;
  }
Enter fullscreen mode Exit fullscreen mode

In another instance let’s say we want to build a chocolate cake, we make a chocolate builder which implements the cake builder interface and from there we can implement the methods:

class Chocolatebuilder implements Cakebuilder interface
   { 
       protected cake;

           public function prepare(): Pizza
      {
          $this->cake = new Cake:
           return this->cake;
      }
         public function applyToppings(): Cake
      {
           $this->cake setToppings([‘fruits’, ‘candy’ ]):
            return this->cake;
      }
            public function bake(): Cake;
      {
          $this->pizza setBakingTemperature (180):
           $this->pizza setBakingMinutes (45):

       return $this->cake:
       }
}


Enter fullscreen mode Exit fullscreen mode

This is the blueprint of each cake object. And our Cakebuilderunderstands the chocolate builder because it’s adhering to the Cakebuilder interface.
This is what we get by putting it all together;

class Cakebuilder
{ 
    public function make(CakeBuilderInterface $cake): Cake
    { 
            return $cake
                   ->prepare()
                   ->applyToppings()
                   ->bake()
        }
 }
  // Create a Cakebuilder
  $cakebuilder = new Cakebuilder 

 // Create cake using Builderwhich returns Cake instance
$cake = $cakeBuilder->make(new ChocolateBuilder()):

Enter fullscreen mode Exit fullscreen mode

Using this example, we can create more builders by applying the same Interface.

Laravel uses this pattern in a bit of a different way. Laravel uses an abstract manager class which is adhered to by a lot of other classes that defines how the builder should work. And this is useful throughout the codebase a lot.
Example:

// Illuminate\Mail\TransportManager
Class TransportsManager extends Manager
{
    protected function createSmtpDriver()
    {
          //Code for building a SmtpTransport class
    }
protected function createMailgunDriver()
   {
        //Code for building a MailgunTransport class
    }
    protected function createSparkpostDriver()
    {
        //Code for building a SparkpostTransport class
    }
    protected function createLogDriver()
    {
        //Code for building a LogTransport class
    }
}

Enter fullscreen mode Exit fullscreen mode

Laravel uses this technique in a lot of places and the most apparent one is the mail class also called the transport manager, which is used for sending emails. Based on the drive name it will try to call a correct transport class and all classes extend the same transport class so they always return the same object.
Let's look at the default driver method:

class TransportManager extends Manager
{
    public function getDefaultDriver()
    {
        return $this->app['config']['mail.driver'];
    }
}

Enter fullscreen mode Exit fullscreen mode

In Laravel, the default driver method utilizes the configuration, making it simple to switch between different email services. For example, if you are currently using SMTP and want to switch to Mailgun or another service, this can easily be done through the default driver. This is made possible by the manager pattern, which is responsible for creating and returning objects. The SMTP driver is specified in the configuration file.

//config/mail.php
'driver' => env('MAIL_DRIVER', 'smtp'),
Enter fullscreen mode Exit fullscreen mode

When the Transport manager calls the getDefaultDriver() method and determines that Smtp is configured, it will create the SmtpDriver. This allows for sending an email.

Laravel manager pattern example:

Illuminate\Auth\AuthManager
Illuminate\Broadcasting\BroadcastManager
Illuminate\Cache\CacheManager
Illuminate\Filesystem\FilesystemManager
Illuminate\Mail\TransportManager
Illuminate\Notifications\ChannelManager
Illuminate\Queue\QueueManager
Illuminate\Session\SessionManager

Enter fullscreen mode Exit fullscreen mode

Repository Pattern

The repository is the interface between the data source and the part of the code that wants to interact with the data.
In simple terms, the Repository pattern in a Laravel application serves as a link between models and controllers. The model should not be in charge of connecting to or retrieving data from the database during this process. As a result, using the repository is required to keep our code clean and secure. It cuts down on code duplication and errors.

For example, we have a client table in our database, where we can add, store, edit and delete client data. We also have a Client model. The idea is, we’re going to create an interface and define some methods, using this:

UserInterface.php

<?php
//(Here the App\Repositories is the folder name)
namespace App\Repositories;

interface UserInterface
{
   public function all();

   public function get($id);

   public function store(array $data);

   public function update($id, array $data); 

   public function delete($id);
}
?>

Enter fullscreen mode Exit fullscreen mode

Our UserInterfaceinterface has five methods called; all(), get(), store(), update(), and delete (). We now need to establish a repository after creating our interface. We'll implement these functions in the repository by calling them from the interface. Let's setup a repository:

EloquentuserRepository.php

<?php
//(Here the App\Repositories is the folder name)
namespace App\Repositories;
use App\Models\Client;

class EloquentuserRepository implements UserInterface
{
    //To view all the data
    public function all()
    {
        return Client::get();
    }
    //Get an individual record
    public function get($id)
    {
        return Client::find($id);
    }
    //Store the data
    public function store(array $data)
    {
        return Client::create($data);
    }
    //Update the data
    public function update($id, array $data)
    {
        return Client::find($id)->update($data);
    }
    //Delete the data
    public function delete($id)
    {
        return Client::destroy($id);
    }
}
?>



Enter fullscreen mode Exit fullscreen mode

EloquentuserRepository is the name of the repository in this case, and it implements the UserInterface. Now that we can access all of those interface methods in this repository, we can use them. For instance, the get() method is used by the all() function to return all the data from the Client model. The other functions work in the same way.
We now have a repository and an interface. To make use of it, we'll need a bridge that connects the interface to the repository. We can use a ServiceProvider class as a bridge. Let's build a service provider.

Repositoryserviceprovider:
<?php
//(Here the App\Repositories is the folder name)
namespace App\Repositories;
use Illuminate\Support\ServiceProvider;

class RepositoriesServiceProvider extends ServiceProvider
{
    public function register()
    {
    $this->app->bind(
      'App\Repositories\UserInterface',
      'App\Repositories\EloquentuserRepository'
    );
    }
}
?>
Enter fullscreen mode Exit fullscreen mode

Our Service provider name is RepositoriesServiceProvider. We use a register function in this ServiceProvider class to bind the interface and repository. They will now be included in the Config.php file.

Config.php:
App\Repositories\RepositoriesServiceProvider::class,

We can now employ the UserInterfaceclass with a construct function in the UserController.

UserController:
use App\Repositories\UserInterface;

class UserController extends Controller
{
    protected $user;
    public function __construct(UserInterface $user){
        $this->user = $user;
    }
    //Get the data
    public function index(){
        $data= $this->user->all();
        return view('index', compact(data));
    }
    //Store the data
    public function store(Request $request){
    $this->user->store($request->all());
        return redirect()->route('client.index');
    }
    //Edit the data
    public function edit($id){
        $data= $this->user->get($id);
        return view('edit', compact(data));
    }
    //Update the data
    public function update($id, Request $request){
        $this->user->update($id, $request->all());
        return redirect()->route('client.index');
    }
    //Delete the data
    public function delete($id){
        $this->user->delete($id);
        return redirect()->route('client.index');
    }
}


Enter fullscreen mode Exit fullscreen mode

Typically, when using a CRUD system, the functions would be implemented in the controller. However, if there is another controller for the product table that requires the same CRUD system, it would not be efficient to write the same code repeatedly. Instead, both controllers can implement the same interface, UserInterface, to avoid duplicating the functions. This approach becomes even more important if the application has complex issues, as it makes maintenance and testing easier by keeping the controllers clean and organized through the use of different repositories.

Conclusion

Design patterns are a set of recipes for building maintainable code that makes our code look clean, is reusable and solves complex problems.
In this article we've learned about three of the Laravel design patterns and how we can apply them in real-world examples. To gain a better understanding of them, you must dive deeper and practice more. I hope you found this article useful.

Top comments (0)