Leverage Dependency Inversion In Laravel Package Development

profile picture

Leverage Dependency Inversion In Laravel Package Development

Last week end I started working on an external package for my startup. Even though it's still in Work In Progress, the goal of the package is to be able to create and manage Printable Document on which you precisely position texts and images.

Once the HTML of the document is ready, the next step is to convert the HTML to PDF. And there are several PHP packages to achieve that.

But which one should I choose??

Answer: Why not all of them? And let the user choose the one that works best for them!

This is something that Laravel already does everywhere. See the Mail component for exemple. In the config file in your laravel app, you can choose the driver you wish to use (log, mailgun, mandrill, smtp, ...)

Let's go step by step

First step in your package development

I assume that you've already setup the base of your Laravel Package. The most important piece of code starts in the PackageServiceProvider. It should look like this. I took the package service provider from my print package

<?php

namespace ABCreche\Printer;

use Illuminate\Support\ServiceProvider;

class PrintServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any package services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
  
    /**
     * Register any package services.
     *
     * @return void
     */
    public function register()
    {
        $this->mergeConfigFrom(
            __DIR__ . '/../config/printer.php',
            'printer'
        );

        $this->app->bind('printer', function ($app) {
            return new Printer;
        });
    }

Now, I didn't inject anything in the Printer class. The goal is to achieve something like this in the code:

<?php

namespace ABCreche\Printer;

use ABCreche\Printer\Interfaces\PDFConverter;

class Printer
{
    public function __construct(PDFConverter $converter)
    {
        $this->converter = $converter;
    }

    public function print(string $html, string $fileName)
    {
        return $this->converter->convert($html, $fileName);
    }
}

In the constructor you see that I inject an Interface. If you are familiar with Dependency Inversion, you immedialtely understand.

For the others, I already explained on this blog what is Dependency Inversion and how it works.

We will make a Dompdf Converter class that will handle the conversion. But Dompdf is going to be one of the available drivers that the package will support, so we cannot inject the DompdfConvert class in the Printer class.

You get it, we will code the interface and inject that interface into the Printer class

Let's quickly review the Interface

In my Printer class I inject a PDFConverter Interface in the constructor. Let's create that:

<?php

namespace ABCreche\Printer\Interfaces;

use ABCreche\Printer\PrintTemplate;

interface PDFConverter
{
    public function convert(string $html, $filename): PDFConverter;
}

Then we make the first driver:

<?php

namespace ABCreche\Printer\Converters;

use ABCreche\Printer\Interfaces\PDFConverter;

class DompdfConverter implements PDFConverter
{
    protected $path;

    public function convert(string $html, $path): PDFConverter
    {
        // Code to generate the pdf with Dompdf code

        return $this;
    }

    public function getLocalPath()
    {
        return $this->path;
    }
}

How do I dynamically inject a dependency following the config file ?

The Config File

// config/printer.php

return [
    // 'dompdf' or anyother drive,
    'converter' => 'dompdf'
];

Using Laravel Resolver

Laravel provides a class allowing you to guess the class to instanciate based on the config file. This class is called Manager. I don't know why it's called like that.

You'll have to create a new class that extends this Manager class.

<?php

namespace ABCreche\Printer;

use Illuminate\Support\Manager;
use ABCreche\Printer\Converters\DompdfConverter;

class ConverterManager extends Manager
{
    /**
     * Create an instance of the Dompdf Converter driver
     *
     * @return DompdfConverter
     */
    protected function createDompdfDriver()
    {
        return new DompdfConverter;
    }

    /**
     * Get the default driver
     *
     * @return string
     */
    public function getDefaultDriver()
    {
        return $this->app['config']['printer.converter'];
    }
}

And finally, back in the PackageServiceProvider:

$this->app->bind('printer', function ($app) {
  $manager = new ConverterManager($app);

  return new Printer($manager->driver());
});

And there you go!

As this Manager class was surprisingly missing from the documentation, I thought this tutorial might help a few people.

Hope it helps

package
laravel
dependency inversion
manager
development

2019 My Dynamic Production SPRL All rights Reserved.