Design Patterns in Laravel: Using the Bridge Pattern

by Steve Oldham


Maybe you’ve been there. You’ve crafted a great web application that serves your client well. You did your due diligence, researched each feature and spent time carefully selecting a service to integrate or a great third party package to use, to help you deliver a certain feature.

It’s working beautifully!

Then, after a while, along comes a requirement, an addition to a feature maybe, that exposes a hole in said carefully selected package. Uh oh.

Now comes the sinking feeling that you may have to spend a lot of time re-doing work and testing with a completely different package…*sigh* 🤦

In some situations, especially in Laravel, there can be a straightforward solution to this kind of problem. A mail provider doesn’t support a certain need? No worries we can swap in another provider super easily. But sometimes it may simply boil down to the need to use more than one service or package to achieve the final goal.

The example problem

In our case, we’d built a Laravel application where the user can export various views as a PDF. We’d used the excellent DomPdf package which worked well, except when we wanted to render a PDF with a more complex data-table layout...it just wasn’t happening.

After identifying the best way to display the data that made it easy for the user to consume and reduced the amount of paper needed when printing (that’s right...🌲🌳 friendly), changing the layout again wasn’t an option.

We also knew we couldn’t justify the time to swap in a new PDF rendering engine and test all those other views again. Ringing any bells?

Enter the Bridge pattern!

The Bridge pattern is a structural design pattern in PHP that’s used to vary an implementation of an object or class.

If you’ve used Laravel even a little you’ll have seen evidence of the Bridge pattern and the sort of flexibility we’re aiming for in the central services of the framework. For example:

  • Databases/Queues can use different connections
  • The Filesystem can use different disks

So let’s take a look at how the Laravel Filesystem uses the Bridge pattern. Here’s a standard call for storing a file to an S3 bucket:

Storage::disk('s3')->put('avatars/1', $fileContents);

The default disk for storage is usually local, but here we’re specifying that we’d like to store our files on our S3 disk which is set up in our filesystems.php config. So how does this actually work?

Behind the scenes the class that manages our filesystem is, wait for it...the FilesystemManager (I know right 🤯). This class uses the __call() magic method to proxy method calls to a driver class that’s unique to each disk available in our filesystem. So when we call that put() method after specifying our S3 disk, we’re really asking our S3 disk driver class to use its own implementation of the put() method to store the files in our S3 bucket.

Laravel uses the concept of managers and drivers throughout the framework to separate some of the concerns and responsibilities of classes in your application. This is a great feature of the bridge pattern. We delegate all that responsibility for the interaction with the files on a disk to a class that knows how to talk to the specific disk we’re using. We don’t need to muddy the FilesystemManager with a load of methods for each implementation, there’s a clean separation of responsibilities.

Bridge all the things!

So how could you apply this to your own use case?

Let’s take our little PDF dilemma as an example and create a Pdf service class that can use different Engines to render our PDF. We’ll approach it just like disks in the Filesystem:

Pdf::view('pdf.example', $data)->engine('snappy')->download('example.pdf');

Pretty straight forward, right? We set the view for our PDF and any required data using the view() method. We also set which engine we want to use to render our PDF using the engine() method. Then we return a download response to serve our PDF.

Now we know our approach, let’s write some code...

The Pdf class

Our Pdf class will be the hub used for generating all PDFs in our application and will act a little like Laravel’s FileManager.

TIP: We’ve created our classes under an App\Export\Pdf namespace because it’s exclusively for exporting PDFs (also a good place for a CSV, Image or any other export class to live). It can be tempting to put all your utility classes in an App\Lib directory, but grouping them by purpose can help give just a bit more info about what a class is used for in your application 😉


Our Pdf class:

Let’s break down what’s going on here. How are we actually using our engine? Let’s look at the render() method called by our download() method.

public function render()
{
  $class = 'App\Export\Pdf\Engines\\' . ucfirst($this->engine) . 'Engine'; return (new $class($this))->render();
}

Our render method actually creates a new instance of a PDF engine class and calls a render method on that engine class. This means we can have as many PDF engine classes as we want and simply use our engine() method on the fly to choose which engine we’d like to generate our PDF. Nice!

Let’s take a look at a PDF engine class.

The PdfEngine class

Because we’ll have multiple engine classes we’ll start by extending an abstract class that will serve as the base for all our PDF engines.

The base class contains our Pdf instance (passed in via the constructor), a view() method to get our rendered blade view and enforces our engine class to implement a render() method which will be used to create a PDF using the engine’s corresponding package. Here’s our DompdfEngine class:

Our DompdfEngine has the sole responsibility of implementing the DomPdf specific way of generating a PDF...that’s it. Using the bridge pattern we’ve kept the rendering of the PDF and any specific settings and quirks of our renderers within our engine classes and away from our Pdf class. Tidy!

TIP: Hold up...how are we making a static call to our very much public view() method on the Pdf class? Good catch 😉 We’re using the magical voodoo of Laravel’s Real-Time Facades (available since v5.5). A Facade acts like a kind of proxy allowing us to make static calls to a class’ public methods. To treat any class in your application as if it were a facade on the fly simply prefix the namespace of the imported class with Facades:

use Facades\App\Export\Pdf\Pdf; 🎉

Not bad, but what can we improve?

It’s looking good, but as we’re working in Laravel there are a few things that we can do to make this implementation of the bridge pattern a little more solid and useable.

Config all the things!

Just like the filesystem disks or database connections we can define all our PDF engines and their defaults in a nice little config file. Not only will this tidy things up, it also affords us the opportunity to specify options in our engines that are environment specific. Let’s add a pdf.php to the config:

Now in our Pdf class we can fetch our default engine from the config in the constructor:

protected $engine;
 
public function __construct()
{
  $this->engine = config('pdf.default');
}

And our render method for the DompdfEngine class can take its options from the config:

$renderer = new Dompdf(config('pdf.engines.dompdf.options'));

Check our engine exists!

Now that we have our pdf engines defined in our config it’d definitely be worth checking that the engine we’re trying to use on the fly actually exists. So let’s update our engine() method on our Pdf class:

public function engine($engine)
{
  if (!in_array($engine, array_keys(config('pdf.engines')))) {
    throw new InvalidArgumentException("PDF engine '$engine' is not registered");
  }
  $this->engine = $engine;
 
  return $this;
}

Now if we try and use an engine that isn’t configured we’ll get a nice exception to help point us in the right direction.

Conclusion

There’s (always) more we could do to improve our little example of the Bridge pattern in Laravel. From implementing contracts like Renderable on our PDF and engine classes to creating a little pdf() helper method (think the Laravel view() helper).

The main takeaway here is what you can do with a PHP design pattern like the Bridge. Whether you’re implementing multiple ways to send a message, collect a payment, store a file, notify a user, this pattern can absolutely be used to better structure your application, decouple your code and separate some of those concerns. Cleaner, clearer and more usable code? Um...yes, please!

Further reading

More on the Bridge Pattern: https://www.jakowicz.com/bridge-pattern-in-php/

A deep dive into the driver based component approach in Laravel: https://itnext.io/building-driver-based-components-in-laravel-5b390dc25bd9

PHP Design Patterns Repo: https://github.com/domnikl/DesignPatternsPHP


Ready to get started on that new web project?
Let's get to work! →