Using the builder/manager pattern in Laravel
Tags: laravel, php
Published: 2023-09-15 20:28:04

*This article was originally written by [Ashley Allen](https://www.honeybadger.io/blog/laravel-manager-pattern/#authorDetails) on the [Honeybadger Developer Blog](https://www.honeybadger.io/blog/laravel-manager-pattern/).*

At some point in your journey as a developer, you will likely come across the term "design pattern". However, what is it? How does it work? What is its intended use?

In this article, we'll briefly look at what design patterns are and why they're important. We'll then take a look at the "builder pattern" (or, specifically, a variation of it that's frequently used in Laravel, the "manager pattern"). We'll take a dive into the code and look at how the manager pattern is implemented in the Laravel framework itself. We'll then explore how we can implement the manager pattern in our own project's application code.

## What are design patterns and why are they important?

According to [Source Making](https://sourcemaking.com/design_patterns), a design pattern "is a general repeatable solution to a commonly occurring problem in software design". You can view design patterns as a toolbox of tried and tested solutions to common problems. They are not code that you can copy and paste into your project. Instead, it's best to think of them as principles that are predictable and consistent and can be used to resolve issues in your code.

A significant benefit of learning about using design patterns is that they aren't always tied to a specific programming language. Although you'd write the code differently depending on the language you're using, the underlying principle behind how and why a pattern works would be the same. This is a major benefit because it means that you can learn about design patterns once and then apply them to any language you're using.

Having a predictable and consistent way to solve problems also allows you to write high-quality code that is easier to understand and maintain by not only yourself but also other developers. This makes it easier for new developers on your team to start contributing meaningful code to your projects sooner because they may already be familiar with the patterns you're using.

However, it's important to note that design patterns are not a silver bullet. They aren't always the best solution to every problem and should be used within the context of the code on which you're working. This is especially true when you first learn about design patterns; it may seem logical to assume that all of your code must follow a given pattern. However, this isn't always the case; sometimes, it can add unnecessary complexity to your code when simpler code could have done the same job. Therefore, it's important to use your best judgement when deciding whether to use a design pattern.

In general, design patterns can be split into three distinct categories that describe the patterns' purpose:

- **Creational patterns** - These patterns are used to create objects that are flexible and easy to change, which encourages code reusability. Examples of these patterns are the factory pattern, builder pattern, and singleton pattern.
- **Structural patterns** - These patterns are used to assemble objects into larger structures that can then be used to achieve a common goal. Examples of these patterns are the adapter pattern, facade pattern, and decorator pattern.
- **Behavioral patterns** - These patterns are used to describe how objects interact with each other. Examples of these patterns are the observer pattern, strategy pattern, and command pattern.

For the remainder of this article, we'll be focusing on one of the creational patterns, the "builder pattern" (or, more specifically, the "manager pattern" in Laravel).

## What is the manager pattern?

Before we look at what the manager pattern is, we first need to understand the builder pattern.

The builder pattern is a creational design pattern that you can use to build complex (but similar) objects in small steps. It allows you to create objects that use the same construction code but represent different things.

In the Laravel world, a specific variation of the builder pattern is often referred to as the "manager pattern". This is due to the use of "manager classes" to handle the creation of the objects.

If you've used Laravel before, you'll likely have interacted with the manager pattern without realizing.

For example, when you use the `Storage` facade, you interact with the underlying `Illuminate\Filesystem\FilesystemManager` class. This class is responsible for creating the driver classes (for reading and writing to different file storage systems) that you interact with when you want to interact with file storage in your projects.

The framework also implements the manager pattern in other places and provides other manager classes, such as the following:

- `Illuminate\Auth\AuthManager`
- `Illuminate\Auth\Passwords\PasswordBrokerManager`
- `Illuminate\Broadcasting\BroadcastManager`
- `Illuminate\Cache\CacheManager`
- `Illuminate\Database\DatabaseManager`
- `Illuminate\Filesystem\FilesystemManager`
- `Illuminate\Hashing\HashManager`
- `Illuminate\Log\LogManager`
- `Illuminate\Mail\MailManager`
- `Illuminate\Notifications\ChannelManager`
- `Illuminate\Queue\QueueManager`
- `Illuminate\Redis\RedisManager`
- `Illuminate\Session\SessionManager`

To give this pattern some context, let's take a look at a small code example.

In your Laravel project, if you wanted to store a file in your "local" file storage, you might use the `local` driver:

```php
Storage::disk('local')->put(...);
```

However, if you wanted to store a file in an AWS S3 bucket, you might want to the use `s3` driver like so:

```php
Storage::disk('s3')->put(...);
```

Similarly, if you've defined a default storage disk by setting the `default` config key in your `config/filesystems.php`, then you might not want to manually specify the disk and would prefer to use something like this:

```php
Storage::put(...);
```

Although these examples may seem simple, they have a lot of complexity behind the scenes. The `Storage` facade allows us to switch between different drivers without having to worry about the underlying complexity of how each driver works.

This is especially useful in the case of the default driver because, theoretically, you could change the default driver in your `config/filesystems.php` config file and wouldn't need to change any of your application's code to start using the new file system storage. This is a result of decoupling our code from the underlying implementation details of the drivers.

## How does Laravel implement the manager pattern?

Now that you have a high-level understanding of how you interact with Laravel's implementations of the manager pattern, let's take a look at how it all works under the hood.

Some of the more complex manager classes, such as `Illuminate\Filesystem\FilesystemManager`, in the framework use a bespoke class for defining their behavior and functionality. However, some of the simpler classes, such as `Illuminate\Hashing\HashManager`, extend from an abstract class called `Illuminate\Support\Manager`. This abstract class provides a lot of the base functionality that the manager classes need to work.

The manager classes, regardless of whether they extend from the `Illuminate\Support\Manager` class, all have a similar structure and are responsible for creating the driver classes with which you interact. To understand how these classes work under the hood, let's take a look at the `Illuminate\Support\Manager` class:

```php
namespace Illuminate\Support;

use Closure;
use Illuminate\Contracts\Container\Container;
use InvalidArgumentException;

abstract class Manager
{
    /**
     * The container instance.
     *
     * @var \Illuminate\Contracts\Container\Container
     */
    protected $container;

    /**
     * The configuration repository instance.
     *
     * @var \Illuminate\Contracts\Config\Repository
     */
    protected $config;

    /**
     * The registered custom driver creators.
     *
     * @var array
     */
    protected $customCreators = [];

    /**
     * The array of created "drivers".
     *
     * @var array
     */
    protected $drivers = [];

    /**
     * Create a new manager instance.
     *
     * @param  \Illuminate\Contracts\Container\Container  $container
     * @return void
     */
    public function __construct(Container $container)
    {
        $this->container = $container;
        $this->config = $container->make('config');
    }

    /**
     * Get the default driver name.
     *
     * @return string
     */
    abstract public function getDefaultDriver();

    /**
     * Get a driver instance.
     *
     * @param  string|null  $driver
     * @return mixed
     *
     * @throws \InvalidArgumentException
     */
    public function driver($driver = null)
    {
        $driver = $driver ?: $this->getDefaultDriver();

        if (is_null($driver)) {
            throw new InvalidArgumentException(sprintf(
                'Unable to resolve NULL driver for [%s].', static::class
            ));
        }

        // If the given driver has not been created before, we will create the instances
        // here and cache it so we can return it next time very quickly. If there is
        // already a driver created by this name, we'll just return that instance.
        if (! isset($this->drivers[$driver])) {
            $this->drivers[$driver] = $this->createDriver($driver);
        }

        return $this->drivers[$driver];
    }

    /**
     * Create a new driver instance.
     *
     * @param  string  $driver
     * @return mixed
     *
     * @throws \InvalidArgumentException
     */
    protected function createDriver($driver)
    {
        // First, we will determine if a custom driver creator exists for the given driver and
        // if it does not we will check for a creator method for the driver. Custom creator
        // callbacks allow developers to build their own "drivers" easily using Closures.
        if (isset($this->customCreators[$driver])) {
            return $this->callCustomCreator($driver);
        } else {
            $method = 'create'.Str::studly($driver).'Driver';

            if (method_exists($this, $method)) {
                return $this->$method();
            }
        }

        throw new InvalidArgumentException("Driver [$driver] not supported.");
    }

    /**
     * Call a custom driver creator.
     *
     * @param  string  $driver
     * @return mixed
     */
    protected function callCustomCreator($driver)
    {
        return $this->customCreators[$driver]($this->container);
    }

    /**
     * Register a custom driver creator Closure.
     *
     * @param  string  $driver
     * @param  \Closure  $callback
     * @return $this
     */
    public function extend($driver, Closure $callback)
    {
        $this->customCreators[$driver] = $callback;

        return $this;
    }

    /**
     * Get all of the created "drivers".
     *
     * @return array
     */
    public function getDrivers()
    {
        return $this->drivers;
    }

    /**
     * Get the container instance used by the manager.
     *
     * @return \Illuminate\Contracts\Container\Container
     */
    public function getContainer()
    {
        return $this->container;
    }

    /**
     * Set the container instance used by the manager.
     *
     * @param  \Illuminate\Contracts\Container\Container  $container
     * @return $this
     */
    public function setContainer(Container $container)
    {
        $this->container = $container;

        return $this;
    }

    /**
     * Forget all of the resolved driver instances.
     *
     * @return $this
     */
    public function forgetDrivers()
    {
        $this->drivers = [];

        return $this;
    }

    /**
     * Dynamically call the default driver instance.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        return $this->driver()->$method(...$parameters);
    }
}
```

As previously mentioned, because the `Manager` class is an abstract class, it can't be instantiated on its own but must be extended by another class first. Therefore, for the purposes of this article, let's also take a look at the `HashManager` class, which extends the `Manager` class for hashing and verifying passwords:

```php
namespace Illuminate\Hashing;

use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Support\Manager;

class HashManager extends Manager implements Hasher
{
    /**
     * Create an instance of the Bcrypt hash Driver.
     *
     * @return \Illuminate\Hashing\BcryptHasher
     */
    public function createBcryptDriver()
    {
        return new BcryptHasher($this->config->get('hashing.bcrypt') ?? []);
    }

    /**
     * Create an instance of the Argon2i hash Driver.
     *
     * @return \Illuminate\Hashing\ArgonHasher
     */
    public function createArgonDriver()
    {
        return new ArgonHasher($this->config->get('hashing.argon') ?? []);
    }

    /**
     * Create an instance of the Argon2id hash Driver.
     *
     * @return \Illuminate\Hashing\Argon2IdHasher
     */
    public function createArgon2idDriver()
    {
        return new Argon2IdHasher($this->config->get('hashing.argon') ?? []);
    }

    /**
     * Get information about the given hashed value.
     *
     * @param  string  $hashedValue
     * @return array
     */
    public function info($hashedValue)
    {
        return $this->driver()->info($hashedValue);
    }

    /**
     * Hash the given value.
     *
     * @param  string  $value
     * @param  array  $options
     * @return string
     */
    public function make($value, array $options = [])
    {
        return $this->driver()->make($value, $options);
    }

    /**
     * Check the given plain value against a hash.
     *
     * @param  string  $value
     * @param  string  $hashedValue
     * @param  array  $options
     * @return bool
     */
    public function check($value, $hashedValue, array $options = [])
    {
        return $this->driver()->check($value, $hashedValue, $options);
    }

    /**
     * Check if the given hash has been hashed using the given options.
     *
     * @param  string  $hashedValue
     * @param  array  $options
     * @return bool
     */
    public function needsRehash($hashedValue, array $options = [])
    {
        return $this->driver()->needsRehash($hashedValue, $options);
    }

    /**
     * Get the default driver name.
     *
     * @return string
     */
    public function getDefaultDriver()
    {
        return $this->config->get('hashing.driver', 'bcrypt');
    }
}
```

Before we delve into what these classes are doing, it's worth noting that each of the three hashing driver classes (`BcryptHasher`, `ArgonHasher`, and `Argon2IdHasher`) implement the following `Illuminate\Contracts\Hashing\Hasher` interface. It's not necessary to understand the interface in detail, but it may help to give the example and descriptions some more context.

```php
namespace Illuminate\Contracts\Hashing;

interface Hasher
{
    /**
     * Get information about the given hashed value.
     *
     * @param  string  $hashedValue
     * @return array
     */
    public function info($hashedValue);

    /**
     * Hash the given value.
     *
     * @param  string  $value
     * @param  array  $options
     * @return string
     */
    public function make($value, array $options = []);

    /**
     * Check the given plain value against a hash.
     *
     * @param  string  $value
     * @param  string  $hashedValue
     * @param  array  $options
     * @return bool
     */
    public function check($value, $hashedValue, array $options = []);

    /**
     * Check if the given hash has been hashed using the given options.
     *
     * @param  string  $hashedValue
     * @param  array  $options
     * @return bool
     */
    public function needsRehash($hashedValue, array $options = []);
```

Now there are 5 parts of the parent `Manager` class that we're particularly interested in:

- The `getDefaultDriver` abstract method definition.
- The `driver` method.
- The `createDriver` method.
- The `__call` method.
- The `extend` method.

### The `getDefaultDriver` method

The `getDefaultDriver` abstract method specifies that in the child class (in this case, the `Illuminate\Hashing\HashManager`), there must be a method called `getDefaultDriver` that returns the default driver name. This is the driver that will be used if no driver is specified when calling the `driver` method.

For context, if you don't call the `driver` method and use something like `Hash::make('password')`, then the `getDefaultDriver` method will be called to determine which driver to use.

### The `driver` method

The `driver` method is the method that will be called whenever you interact with the manager class. It attempts to return the necessary driver class for using. However, if the driver hasn't been created yet (presumably if it's the first time you're calling a method in the driver), then it will create the driver and then store it as a class-level variable for later use.

It's also worth noting that you can explicitly call the `driver` method yourself rather than letting the `__call` method do it for you. For example, you could call `Hash::driver('argon')->make('password')` to use the Argon hashing driver.

### The `createDriver` method

The `createDriver` method is responsible for creating the driver class. It first checks to see if there is a custom driver creator for the driver that you're trying to create. If there is, then it will call the custom driver creator and return the result. However, if there isn't, then it will attempt to call a method on the manager class that is named `create{DriverName}Driver`. For example, if we wanted to create a new driver for our `bcrypt` hash driver, then the method that would be called is `createBcryptDriver`. Therefore, by default, each driver should have their own method in the manager class that determines how the driver should be created and returned. We can see this in the `HashManager` class with the `createBcryptDriver`, `createArgonDriver`, and `createArgon2idDriver` methods.

### The `__call` method

The `__call` method is called whenever you call a method on the manager class that doesn't exist. Instead, it will attempt to forward the method call to the driver class.

### The `extend` method

The `extend` method is used to register a custom driver creator. Therefore, in our example of the `HashManager`, if we wanted to define our own driver for creating hashes, we could use this method to define our own new driver so that we could call it in our application code.

For example, if we wanted to create our own hashing driver, we could create a class that implements the `Hasher` interface like so:

```php
namespace App\Hashing;

use Illuminate\Contracts\Hashing\Hasher;

class CustomHasher implements Hasher
{
    public function info($hashedValue)
    {
        // Custom implementation here...
    }

    public function make($value, array $options = [])
    {
        // Custom implementation here...
    }

    public function check($value, $hashedValue, array $options = [])
    {
        // Custom implementation here...
    }

    public function needsRehash($hashedValue, array $options = [])
    {
        // Custom implementation here...
    }
}
```

We can then make use of the `extend` method on the `Hash` facade to register our custom hashing driver. We can do this in the `boot` method of the `AppServiceProvider` like so:

```php
namespace App\Providers;

use App\Hashing\CustomHasher;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{

    // ...

    public function boot()
    {
        Hash::extend(
            driver: 'custom-hasher',
            callback: static fn (): Hasher => new CustomHasher()
        );
    }
}
```

Now that we've done this, we can use our custom hashing driver in our application code like so:

```php
Hash::driver('custom-hasher')->make('password');
```

By using the `extend` method, you can make use of the existing manager classes that are already integral to the Laravel framework and add your own custom drivers to suit your projects. For additional examples of how to use this type of approach, you can check out the [Custom filesystems](https://laravel.com/docs/9.x/filesystem#custom-filesystems) section of the Laravel documentation; it includes an example of how to register a custom file storage driver to interact with Dropbox.

**Note:** I've used the `HashManager` class as an example in this article purely due to its simplicity in comparison to some of the other manager classes so that the concepts of the pattern can be explained easier. It's worth noting that you're strongly advised not to create your own hashing algorithms. Instead, you should use one of the existing hashing algorithms provided by the PHP core. If you want to create your own hashing algorithm, be aware that you're responsible for ensuring that your algorithm is secure and that it's not vulnerable to any known attacks.

## Implementing the manager pattern yourself

Now that we've looked at how the underlying manager classes work, let's look at how we can implement the manager pattern ourselves in our own projects. For this example, we'll create a manager class that will be responsible for creating and managing our own simple drivers for communicating with some exchange-rate APIs.

### Creating the driver classes

To get started, we'll create a simple interface that the exchange-rate API drivers can implement. The interface will define a single `exchangeRate` method that gets the exchange rate between a currency pair on a given date. We'll place this interface in an `app/Interfaces` directory and call it `ExchangeRateApiDriver`:

```php
namespace App\Interfaces;

use Carbon\CarbonInterface;

interface ExchangeRateApiDriver
{
    public function exchangeRate(string $from, string $to, CarbonInterface $date): string;
}
```

We can then create our two drivers and place them in an `app/Services/ExchangeRates` directory. Both of the driver classes will implement the `ExchangeRateApiDriver` interface.

One driver (which we'll call `FixerIODriver`) will interact with the [Fixer.io](https://fixer.io/) API and the other (which we'll call `OpenExchangeRatesDriver`) will interact with the [Open Exchange Rates](https://openexchangerates.org/) API.

We'll create a `FixerIODriver` class:

```php
namespace App\Services\ExchangeRates;

use App\Interfaces\ExchangeRateApiDriver;

class FixerIODriver implements ExchangeRateApiDriver
{
    public function exchangeRate(string $from, string $to, CarbonInterface $date): string
    {
        // Implementation goes here...
    }
}
```

Likewise, we'll also create our `OpenExchangeRatesDriver` class:

```php
namespace App\Services\ExchangeRates;

use App\Interfaces\ExchangeRateApiDriver;

class OpenExchangeRatesDriver implements ExchangeRateApiDriver
{
    public function exchangeRate(string $from, string $to, CarbonInterface $date): string
    {
        // Implementation goes here...
    }
}
```

### Creating the manager class

Now that we have both of our drivers ready to go, we can create our manager class that will be used to create the drivers. We'll create an `ExchangeRatesManager` class in an `app/Services/ExchangeRates` directory:

```php
namespace App\Services\ExchangeRates;

use App\Interfaces\ExchangeRateApiDriver;
use Illuminate\Support\Manager;

class ExchangeRatesManager extends Manager
{
    public function createFixerIoDriver(): ExchangeRateApiDriver
    {
        return new FixerIoDriver();
    }

    public function createOpenExchangeRatesDriver(): ExchangeRateApiDriver
    {
        return new OpenExchangeRatesDriver();
    }

    public function getDefaultDriver()
    {
        return $this->config->get('exchange-rates.driver', 'fixer-io');
    }
}
```

As you can see, we've created two new methods (`createFixerIoDriver` and `createOpenExchangeRatesDriver`) that can be used to resolve the drivers. The methods’ names follow the structure that the underlying `Manager` class expects. In our example, we aren't doing anything special in the methods, but you could add additional logic to the methods if desired. For example, you may want to pass some driver-specific config to the drivers (such as API keys).

You may also have noticed that we've implemented a `getDefaultDriver` method. This method is used to determine which driver should be used by default if no driver is specified when resolving the driver. In our example, we're using the `exchange-rates.driver` config value (which would typically be set in the `driver` field of a `config/exchange-rates.php` config file) to determine which driver should be used by default. If the config value isn't set, then we'll default to the `fixer-io` driver.

For the purpose of this guide, I've not made the `ExchangeRatesManager` class implement the `ExchangeRatesApiDriver` interface. Instead, I'm relying on the `__call` method in the abstract parent `Manager` class to forward the method call to the resolved driver. However, if you'd prefer the manager class to implement the interface, you can, but you’ll need to manually forward the method call to the resolved driver yourself (similar to how the `HashManager` class does it). For example, you could add a method like this to your manager class:

```php
public function exchangeRate(string $from, string $to, CarbonInterface $date): string
{
    return $this->driver()->exchangeRate($from, $to, $date);
}
```

### Registering the manager class

Now that we have the manager class prepared, we can register it as a singleton in the service container. Thus, the manager class will only be instantiated once and will be available to be resolved from the container in our application code. It means that the driver classes will also only be instantiated once. We can do this by updating our `AppServiceProvider` like so:

```php
namespace App\Providers;

use App\Services\ExchangeRates\ExchangeRatesManager;
use Carbon\CarbonInterface;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(
            abstract: ExchangeRatesManager::class,
            concrete: fn (Application $app) => new ExchangeRatesManager($app),
        );
    }

    // ...

}
```

We've defined that whenever we try to resolve the `ExchangeRatesManager` class from the service container, we want to resolve the `ExchangeRatesManager` itself.

That's it! We should now be ready to interact with our drivers in our application code!

### Using the manager class

For example, if we wanted to use the default driver, we could do something like this:

```php
app(ExchangeRatesManager::class)->exchangeRate(
    from: 'GBP',
    to: 'USD',
    date: Carbon::parse('2021-01-01'),
);
```

Similarly, if we wanted to explicitly define the driver to use, we could do something like this:

```php
app(ExchangeRatesManager::class)
    ->driver('open-exchange-rates')
    ->exchangeRate(
        from: 'GBP',
        to: 'USD',
        date: Carbon::parse('2021-01-01'),
    );
```

Alternatively, if we wanted to use the `ExchangeRatesManager` class in a part of our application code that supports dependency injection (such as a controller method), we could do avoid using the `app` helper and pass it as an argument like so:

```php
namespace App\Http\Controllers;

use App\Services\ExchangeRates\ExchangeRatesManager;

class ExchangeRatesController extends Controller
{
    public function index(ExchangeRatesManager $exchangeRatesManager)
    {
        $rate = $exchangeRatesManager->exchangeRate(
            from: 'GBP',
            to: 'USD',
            date: Carbon::parse('2021-01-01'),
        );
        
        // ...
    });
}
```

### Using the manager class via a facade

Although the topic is contentious, if you wanted to make your code look more Laravel-y, you could potentially also introduce a "[facade](https://laravel.com/docs/9.x/facades)" into your code.

To do this, you could create an `ExchangeRates` facade class in an `app/Facades` directory:

```php
namespace App\Facades;

use App\Services\ExchangeRates\ExchangeRatesManager;
use Carbon\CarbonInterface;
use Illuminate\Support\Facades\Facade;

/**
 * @method static string driver(string $driver = null)
 * @method static string exchangeRate(string $from, string $to, CarbonInterface $date)
 *
 * @see ExchangeRatesManager
 */
class ExchangeRate extends Facade
{
    protected static function getFacadeAccessor()
    {
        return ExchangeRatesManager::class;
    }
}
```

As you can see above, we've created a new facade and used docblocks to document the available methods and the underlying class that will be resolved when using the facade.

By creating the facade, we would now be able to use the `ExchangeRate` facade in our application code like so:

```php
use App\Facades\ExchangeRate;

ExchangeRate::exchangeRate(
    from: 'GBP',
    to: 'USD',
    date: Carbon::parse('2021-01-01'),
);
```

If we wanted to specify which driver to use, we could also do the following:

```php
use App\Facades\ExchangeRate;

ExchangeRate::driver('open-exchange-rates')
    ->exchangeRate(
        from: 'GBP',
        to: 'USD',
        date: Carbon::parse('2021-01-01'),
    );
```

## Conclusion

Hopefully, this article has given you a good understanding of what the manager pattern is and the benefits of using it. It should have also shown you how it's used in Laravel and given some insight into how it's implemented under the hood. You should now be able to implement the manager pattern in your own projects so that you can write flexible and reusable code.