← Back to Dashboard

Factory Method

Laravel: Cache::driver(), Storage::disk(), Manager pattern

Factory Method Pattern

Intent (GoF)

"Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses."

In simpler terms: instead of having client code decide which concrete class to create with new, you delegate that decision to a dedicated factory. The client asks for an object through a common interface and gets back the right implementation without knowing -- or caring -- which one it is.

Real-World Analogy

Picture a kiosk in a mall that accepts multiple payment methods. When a customer taps "Pay Now", the system needs to create the correct payment gateway connection -- GCash, Maya, or cash processing. The cashier (your application code) should not need to know the technical details of connecting to each gateway. Instead, the cashier tells a payment terminal (the factory): "I need a gateway for GCash." The terminal hands back a ready-to-use gateway object. If a new payment method is added next quarter (say, a crypto wallet), only the terminal needs updating -- not every cashier in the mall.

The Anti-Pattern: Scattered new Statements

In codebases without a factory, you see the same creation logic duplicated everywhere:

class CheckoutController
{
    public function processPayment(string $method, float $amount): string
    {
        if ($method === 'gcash') {
            $gateway = new GCashGateway();
            $gateway->setMerchantId('GC-12345');
            $gateway->setApiKey('secret_gc');
        } elseif ($method === 'maya') {
            $gateway = new MayaGateway();
            $gateway->setPublicKey('pk_maya');
            $gateway->setSecretKey('sk_maya');
        } elseif ($method === 'cash') {
            $gateway = new CashGateway();
        } else {
            throw new \InvalidArgumentException("Unknown method: {$method}");
        }

        return $gateway->charge($amount);
    }
}
class RefundController
{
    public function processRefund(string $method, float $amount): string
    {
        // Exact same if/elseif chain copied here...
        if ($method === 'gcash') {
            $gateway = new GCashGateway();
            $gateway->setMerchantId('GC-12345');
            // ...
        }
        // ...
    }
}

Problems:

  1. Duplication. The creation logic is copy-pasted into every controller, service, or command that needs a gateway. Change how GCash is configured and you must update every copy.
  2. Open/Closed Principle violation. Adding a new payment method means modifying every file that creates gateways. You are editing existing code instead of extending it.
  3. Tight coupling. The controller knows about every concrete gateway class. It cannot be tested in isolation without dragging in all gateway implementations.
  4. Inconsistent initialisation. One developer sets the API key; another forgets. There is no single place that guarantees correct setup.

The Fix: Factory Method

Extract all creation logic into a single factory:

interface PaymentGatewayInterface
{
    public function charge(float $amount): string;
    public function getName(): string;
}

class PaymentGatewayFactory
{
    public static function create(string $type): PaymentGatewayInterface
    {
        return match (strtolower($type)) {
            'gcash' => new GCashGateway(),
            'maya'  => new MayaGateway(),
            'cash'  => new CashGateway(),
            default => throw new \InvalidArgumentException(
                "Unknown payment gateway type: {$type}"
            ),
        };
    }
}

Now client code is clean and decoupled:

class CheckoutController
{
    public function processPayment(string $method, float $amount): string
    {
        $gateway = PaymentGatewayFactory::create($method);
        return $gateway->charge($amount);
    }
}

What you gain:

  1. Single point of creation. All gateway instantiation lives in one place. Change the configuration of GCash? Update one factory method.
  2. Open for extension, closed for modification. Adding a new gateway means adding a new class and one line in the factory -- no existing client code changes.
  3. Loose coupling. Controllers depend only on PaymentGatewayInterface, not on any concrete class. This makes testing trivial -- inject a mock gateway in tests.
  4. Consistent initialisation. Every gateway is created the same way, with the same configuration, every time.

Static Factory vs. Abstract Factory

The example above uses a static factory method -- the simplest and most common form. The GoF pattern also describes an abstract factory where the factory itself is an interface with multiple implementations. This is useful when you have families of related objects (e.g., a PaymentGateway + ReceiptPrinter + RefundHandler that must all match the same provider).

For most Laravel applications, the static factory or Laravel's Manager pattern (covered in the Laravel Context tab) is the right choice.

When to Use the Factory Method

  • When client code should not know which concrete class it gets
  • When creation logic involves configuration, validation, or setup that should not leak into consumers
  • When you expect new implementations to be added over time
  • When the same creation logic appears in more than one place

When NOT to Use It

  • When there is only one implementation and no foreseeable second one -- a factory for a single class is over-engineering
  • When the constructor is trivial and there is no setup logic worth encapsulating
  • When dependency injection already handles the wiring (Laravel's container is itself a factory)

Key Takeaway

If you find the same if/elseif or match block for creating objects in more than one place, you need a factory. Centralise the decision, return an interface, and let client code stay blissfully ignorant of concrete classes.

What You Learned

  • The Factory Method pattern encapsulates object creation logic behind a single method call
  • Callers request objects by type without knowing which concrete class is instantiated
  • Adding a new product type means adding a new class and one line to the factory — no changes to calling code
  • Factories pair naturally with interfaces: the factory returns the interface type, hiding implementation details

Factory Method in Laravel

Laravel is built on the Factory Method pattern. The framework's "Manager" classes are textbook implementations of this pattern, and you interact with them every day -- possibly without realising it.

The Manager Pattern

Laravel's Illuminate\Support\Manager is an abstract class that provides a structured way to create and manage driver-based components. At its core, it is a factory:

// Simplified version of how Laravel's Manager works internally
abstract class Manager
{
    protected array $drivers = [];

    abstract public function getDefaultDriver(): string;

    public function driver(?string $driver = null): mixed
    {
        $driver = $driver ?: $this->getDefaultDriver();

        if (!isset($this->drivers[$driver])) {
            $this->drivers[$driver] = $this->createDriver($driver);
        }

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

    protected function createDriver(string $driver): mixed
    {
        $method = 'create' . ucfirst($driver) . 'Driver';

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

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

Each concrete Manager implements createXxxDriver() methods for each supported driver.

CacheManager

// Using the Cache factory
$redis = Cache::store('redis');     // Returns a RedisStore instance
$file  = Cache::store('file');      // Returns a FileStore instance
$array = Cache::store('array');     // Returns an ArrayStore instance

// All share the same interface -- your code doesn't care which store it gets
$redis->put('key', 'value', 3600);
$file->put('key', 'value', 3600);

Internally, CacheManager has methods like:

protected function createFileDriver(): Repository
{
    return $this->repository(new FileStore(...));
}

protected function createRedisDriver(): Repository
{
    return $this->repository(new RedisStore(...));
}

Each createXxxDriver method is a factory method. The Manager selects the right one based on the driver name.

Filesystem / Storage

// config/filesystems.php defines disks
'disks' => [
    'local' => ['driver' => 'local', 'root' => storage_path('app')],
    's3'    => ['driver' => 's3', 'key' => '...', 'secret' => '...'],
],

// Client code uses the factory
$local = Storage::disk('local');   // Returns a LocalFilesystemAdapter
$s3    = Storage::disk('s3');      // Returns an S3Adapter

// Same interface, different implementations
$local->put('file.txt', 'content');
$s3->put('file.txt', 'content');

Adding a custom disk driver is as simple as registering a new creator:

// In a service provider
Storage::extend('dropbox', function ($app, $config) {
    return new DropboxAdapter(
        new DropboxClient($config['token'])
    );
});

This is the Open/Closed Principle at work: you extend the factory without modifying it.

Session and Auth Managers

The same pattern appears in session management and authentication:

// SessionManager creates drivers based on config
// config/session.php: 'driver' => 'file'
// Internally: createFileDriver(), createDatabaseDriver(), createRedisDriver(), etc.

// AuthManager creates guards based on config
// config/auth.php: 'defaults' => ['guard' => 'web']
$user = Auth::guard('web')->user();    // SessionGuard
$user = Auth::guard('api')->user();    // TokenGuard

Building Your Own Manager

You can create your own Manager-based factory in Laravel for your domain:

use Illuminate\Support\Manager;

class PaymentGatewayManager extends Manager
{
    public function getDefaultDriver(): string
    {
        return $this->config->get('payments.default', 'cash');
    }

    protected function createGcashDriver(): PaymentGatewayInterface
    {
        return new GCashGateway(
            merchantId: $this->config->get('payments.gcash.merchant_id'),
        );
    }

    protected function createMayaDriver(): PaymentGatewayInterface
    {
        return new MayaGateway(
            publicKey: $this->config->get('payments.maya.public_key'),
        );
    }

    protected function createCashDriver(): PaymentGatewayInterface
    {
        return new CashGateway();
    }
}

Register it in a service provider:

$this->app->singleton('payment.gateway', function ($app) {
    return new PaymentGatewayManager($app);
});

// Usage anywhere in the app:
$gateway = app('payment.gateway')->driver('gcash');
$gateway->charge(1500.00);

The Pattern Summarised

Every time you call Cache::store(), Storage::disk(), Auth::guard(), or Session::driver(), you are using a Factory Method. The client code names the variant it wants via a string key, and the Manager returns the correctly configured concrete implementation behind a common interface. This is why adding new cache backends, filesystem drivers, or auth guards in Laravel is so painless -- the factory pattern makes it a matter of registering one new method, not rewriting consumer code.

This example is pre-filled and runnable. Click "Run" to see the output. Feel free to modify the code and experiment.

Output

                

            

Complete the code below to pass all test cases. Use the hints if you get stuck.