Optional Laravel service providers

Sometimes in development, you want to use some Composer packages that you don't want to have installed in live environments, such as staging or production. Usually, you would just install this as dev dependencies, with something like:

composer require --dev vendor/package

Such dependencies are then declared as "development-only", and, in general, you don't install those in any environments except locally.

And sometimes, these packages are Laravel-specific and include their own service providers so they can register themselves within the application and function properly. Laravel has a nifty feature since version 5.5 (please don't install dependencies with a dev-master version specified, as that link suggests, instead using stable versions only) called Package Auto-Discovery, enabling package developers to declare their service providers within their composer.json files, which Laravel will later automatically register when they are installed.

Laravel is, however, slightly opinionated about the order in which service providers load, and this logic can be found in Illuminate\Foundation\Application::registerConfiguredProviders():

public function registerConfiguredProviders()
{
    $providers = Collection::make($this->config['app.providers'])
                    ->partition(function ($provider) {
                        return Str::startsWith($provider, 'Illuminate\\');
                    });

    $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);

    (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
                ->load($providers->collapse()->toArray());
}

What this does, basically, is read all of the providers declared in the providers section of the primary config/app.php configuration file, and extract all providers starting with Illuminate\ (ie. all core framework providers) as the first group of providers to load, leaving the others after that, followed by injecting all auto-discovered packages in-between, resulting in the following order in which providers get registered and booted:

  1. Illuminate\ service providers declared in app.providers
  2. Auto-discovered packages' service providers
  3. Other service providers declared in app.providers

This might not be ideal, because it somewhat relies on magic behavior, and makes it less obvious about which providers will ultimately get registered in your application. I've found that it's best to just disable auto-discovery completely, which you can achieve by adding a section like the following in your composer.json file:

"extra": {
    "laravel": {
        "dont-discover": [
            "*"
        ]
    }
},

After that, you'll just explicitly register the service providers you need in app.providers, and make it less confusing for yourself and other developers working on the same codebase - otherwise, the only obvious ways to figure out which providers are registered are to manually inspect your dependencies and see which ones are developed with auto-discover in mind, inspecting the cached packages file usually located in bootstrap/cache/packages.php, or doing something silly like editing the previously mentioned registerConfiguredProviders() method to temporarily dump the compiled list of providers to load - and none of those options are as easy as simply reading the list in app.providers.

This introduces a new problem - you need to figure out how to load some service providers only in your development environment, but not in others. One option I've found recommended online is to use a package such as percymamedy/laravel-dev-booter, which allows you to define additional service provider groups, and configure them to be loaded only in certain environments. Such an approach, however, suffers from the same issue we were trying to resolve in the first place, which is explicitly declaring the order in which service provider get loaded, as it will still load providers in groups.

The best solution I was able to find that solves the issue to my satisfaction is to create a kind of a "proxy" provider, which checks whether the real service provider class is available, and registers it if so.

It's an easy solution, so here's an example featuring barryvdh/laravel-debugbar, a package commonly used in development to help out with debugging:

<?php

declare(strict_types=1);

namespace App\Debugbar;

use Barryvdh\Debugbar\ServiceProvider as DebugbarServiceProvider;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;

class ServiceProvider extends BaseServiceProvider
{
    /**
     * @inheritDoc
     */
    public function register(): void
    {
        if (class_exists(DebugbarServiceProvider::class)) {
            $this->app->register(DebugbarServiceProvider::class);
        }
    }
}

Then, just add App\Debugbar\ServiceProvider to the app.providers list, and it will load the actual package's service provider only if it's installed, while still enabling us to always be explicit about all of the service providers we register, and the order in which they will be loaded.

You could also just add this same logic into the App\Providers\AppServiceProvider, which is provided by default in Laravel, but then once again you lose the clarity of having the service provider explicitly listed in app.providers, which was one of the original goals for me.

If you would like to generalize the above solution, one option is to do something like this:

<?php

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Support\ServiceProvider as BaseServiceProvider;

abstract class OptionalServiceProvider extends BaseServiceProvider
{
    /**
     * The name of the optional service provider.
     *
     * @var string
     */
    protected $optionalProviderClassName;

    /**
     * @inheritDoc
     */
    public function register(): void
    {
        if (class_exists($this->optionalProviderClassName)) {
            $this->app->register($this->optionalProviderClassName);
        }
    }
}

And then, for actual service providers (such as the one previously shown), you could do:

<?php

declare(strict_types=1);

namespace App\Debugbar;

use App\Providers\OptionalServiceProvider;
use Barryvdh\Debugbar\ServiceProvider as DebugbarServiceProvider;

class ServiceProvider extends OptionalServiceProvider
{
    /**
     * @inheritDoc
     */
    protected $optionalProviderClassName = DebugbarServiceProvider::class;
}

I also tinkered with the idea of being able to add something like optional(SomePackage\ServiceProvider::class) directly into app.providers, where the optional() function would be some helper that dynamically creates a service provider as the one above, though that one seems like a lot more work.

Ultimately, the overhead of figuring out a generalized solution to this problem just wasn't worth the effort for me, as I'll usually only have no more than a few such "optional" providers in an application, where the non-generalized solution is just good enough as it is.