Skip to main content

Service Providers

Overview

Service Providers are a convenient way to organize and register services within DomainFlow Core. They let you encapsulate logic for binding services, bootstrapping functionality, and optionally deferring expensive or optional features until needed. By grouping related services in a single provider, you keep your application tidy and make it simpler to manage multiple services at once.


Key Concepts

  • Registration: A provider’s main job is to bind services into the application so they can be resolved when needed.
  • Boot: Once registered, a provider can perform additional setup in its boot() method—especially useful if the provider needs to react to other services now being available.
  • Deferred Loading: If you mark a provider as deferred, it only registers its services on demand, improving startup performance for applications with many optional services.

Method Descriptions (Application-Level)

Below are the main methods on your Application instance to register, manage, and load service providers.

registerProvider($provider)

Signature

public function registerProvider(ServiceProviderInterface $provider): void

Purpose Registers a service provider with the application. If the provider is not deferred, it immediately calls the provider’s register() method. If the application has already booted, it then calls boot() on the provider.

ParameterTypeRequired?Description
$providerServiceProviderInterfaceYesAn object that implements the methods needed to register and optionally boot services.

Usage Example

$app->registerProvider(new MyCustomServiceProvider());

unregisterProvider($providerClass)

Signature

public function unregisterProvider(string $providerClass): void

Purpose Removes a provider from the provider registry and releases its outstanding deferred-service claims. It does not undo bindings or instances that the provider's register() method has already placed in the container. It also releases any deferred service-identifier claims the provider class held (see Deferred Service Identifier Collisions below), so a different provider class may claim the same identifier afterward.

ParameterTypeRequired?Description
$providerClassstringYesThe fully qualified class name of the provider to remove.

Usage Example

$app->unregisterProvider(\Acme\Example\ExampleServiceProvider::class);

getProviders()

Signature

public function getProviders(): array

Purpose Returns an array of all currently registered providers. You might use this for debugging or when you need to iterate over each provider for specialized logic.

ParameterTypeRequired?Description
NoneN/AN/AN/A

Usage Example

foreach ($app->getProviders() as $provider) {
// Inspect or interact with the provider
}

loadDeferredProviders()

Signature

public function loadDeferredProviders(): void

Purpose Pre-warms every still-deferred provider by registering and booting it immediately, for any provided service identifier that has not already been bound. Useful for a CLI warm-up step, or any situation where you want everything loaded up front rather than waiting for on-demand resolution.

ParameterTypeRequired?Description
NoneN/AN/AN/A
Not called automatically

This is an explicit opt-in — boot() and get() never call it for you. The documented default lifecycle only loads a deferred provider the first time one of its provides() identifiers is requested via get(). Calling loadDeferredProviders() defeats that laziness for whatever providers are still deferred at the time it runs.

Usage Example

// If you want no lazy loading:
$app->loadDeferredProviders();

hasProvider($providerClass)

Signature

public function hasProvider(string $providerClass): bool

Purpose Returns true only after a provider has actually been registered and is present in the active provider map. A deferred provider that has merely been queued or claimed but not loaded yet returns false.

ParameterTypeRequired?Description
$providerClassstringYesThe fully qualified class name of the provider to check for.

Usage Example

if (!$app->hasProvider(\Vendor\Logging\LoggingProvider::class)) {
$app->registerProvider(new LoggingProvider());
}

Method Descriptions (Inside a Service Provider)

To create your own provider, you can implement the ServiceProviderInterface or extend a base provider class. The interface typically expects you to define:

register($app)

Signature

public function register(Application $app): void

Purpose Where you bind services into the application, e.g., $app->bind('SomeKey', ...). This method is called when the provider is first registered—either immediately or on-demand if deferred.

ParameterTypeRequired?Description
$appApplicationYesReference to the main application object

boot($app)

Signature

public function boot(Application $app): void

Purpose Runs after register()—useful for tasks that rely on services being bound already. If the application is already booted when you register a provider, boot() is called immediately after register() completes.

ParameterTypeRequired?Description
$appApplicationYesReference to the main application object

provides()

Signature

public function provides(): array

Purpose Returns an array of “service keys” that this provider is responsible for. When marked as deferred, the application uses this list to see if your provider should be loaded.

ParameterTypeRequired?Description
NoneN/AN/AMust return a list of string keys.

isDeferred()

Signature

public function isDeferred(): bool

Purpose Indicates whether your provider is eligible for lazy-loading (i.e., only loaded when one of its provides() services is requested).

ParameterTypeRequired?Description
NoneN/AN/AReturn true for lazy-loading.

AbstractServiceProvider

Rather than implementing ServiceProviderInterface from scratch, most providers should extend DomainFlow\Service\AbstractServiceProvider, which supplies a usable default for every method except register():

MethodDefault behavior
boot()No-op.
provides()Returns the protected $providedServices array.
isDeferred()Returns the public $defer property (false by default).

A concrete provider therefore usually only needs to implement register() and, if it defers, set $defer = true and populate $providedServices:

class LoggingServiceProvider extends AbstractServiceProvider
{
public bool $defer = true;

protected array $providedServices = ['Logger'];

public function register(Application $app): void
{
$app->bind('Logger', fn ($app) => new Logger());
}
}

If a provider's deferred status must be computed rather than stored as a simple flag, override isDeferred() directly instead of relying on $defer.


Deferred Service Identifier Collisions

A provides() identifier (a class-string, interface-string, or plain string alias — all treated as opaque by Core) may be claimed by exactly one deferred provider class at a time.

  • registerProvider() throws BootstrappingException::forDeferredServiceIdentifierCollision() if a second, different provider class tries to claim an identifier another provider class already owns.
  • Re-registering the same provider class for the same identifier is not a collision — the most recently supplied instance is the one used on resolution, so constructor-dependent providers remain safely re-registerable.
  • unregisterProvider() releases a provider class's claims, so a different provider class may claim the same identifier(s) afterward.
$app->registerProvider(new MailServiceProvider()); // claims 'Mailer'

// A different provider class claiming the same identifier throws:
$app->registerProvider(new OtherMailServiceProvider()); // throws BootstrappingException

// Freeing the claim first allows the swap:
$app->unregisterProvider(MailServiceProvider::class);
$app->registerProvider(new OtherMailServiceProvider()); // now succeeds

Declarative Provider Ordering

A provider may optionally implement DomainFlow\Service\OrderedServiceProviderInterface — an opt-in extension of ServiceProviderInterface — to declare which other providers must be registered and booted before it, within the same boot() cycle.

Signature

interface OrderedServiceProviderInterface extends ServiceProviderInterface
{
/**
* @return list<class-string<ServiceProviderInterface>>
*/
public function dependsOn(): array;
}

How ordering is resolved

Before running the register/boot passes, boot() resolves a stable topological order across every currently registered provider's declared dependsOn() classes:

  • A provider that does not implement the interface (or declares no dependencies) keeps its plain insertion-order position relative to every other undeclared provider — existing code with no ordering declarations behaves exactly as before.
  • A dependency cycle makes boot() throw a BootstrappingException.
  • A dependsOn() entry naming a provider class that was never registered also makes boot() throw a BootstrappingException.
Scope: this orders the boot pass

registerProvider()'s eager path already calls register() synchronously the moment it is called — before or independent of boot(). So by the time boot()'s ordering pass runs, every eagerly-registered provider's register() has already executed in plain call order; dependsOn() therefore reliably reorders only the boot() calls. In practice this doesn't weaken the guarantee: a provider's register()-time bindings are already available to every other provider's register()/boot() regardless of declared order, so dependsOn() still guarantees what matters most — a provider's boot() observing another provider's fully-registered state.

Usage Example

class DatabaseServiceProvider extends AbstractServiceProvider
{
public function register(Application $app): void
{
$app->bind('db.connection', fn () => new PdoConnection(/* ... */));
}
}

class ReportingServiceProvider extends AbstractServiceProvider implements OrderedServiceProviderInterface
{
public function dependsOn(): array
{
return [DatabaseServiceProvider::class];
}

public function register(Application $app): void
{
$app->bind('reporting', fn ($app) => new ReportingService($app->get('db.connection')));
}

public function boot(Application $app): void
{
// Guaranteed to run after DatabaseServiceProvider::boot().
}
}

$app->registerProvider(new ReportingServiceProvider()); // registered first, but...
$app->registerProvider(new DatabaseServiceProvider());
$app->boot(); // ...DatabaseServiceProvider still boots before ReportingServiceProvider.

Comparative Descriptions

  • Immediate vs. Deferred Registration
  • If isDeferred() returns false, register() happens right away.
  • If true, the provider’s services get bound only when their keys are first requested or if you explicitly call loadDeferredProviders().
  • register() vs. boot()
  • register() is for binding or configuring services.
  • boot() is for tasks that rely on the services being fully registered, or for hooking into the application after it’s been initialized.

Practical Examples

class LoggingServiceProvider extends AbstractServiceProvider
{
public bool $defer = true; // This can be set to false for immediate loading

protected array $providedServices = [
'Logger'
];

public function register(Application $app): void
{
// Bind the service
$app->bind('Logger', function ($app) {
return new Logger(/* ... */);
});
}

public function boot(Application $app): void
{
// Possibly attach the logger to events or load additional config
}
}

// Usage in your app:
$app->registerProvider(new LoggingServiceProvider());

// If you want to load it immediately:
$app->loadDeferredProviders();

// Retrieve the logger:
$logger = $app->get('Logger'); // triggers deferred registration if not yet loaded

Extensibility

  • Multiple Services per Provider: You can return many service keys in provides(). This makes it simple to group related services (e.g., “Mailer” and “MailQueue”).
  • Conditional Providers: Some teams use environment checks to decide whether to register certain providers. For instance, only register a debug provider if you’re in development mode.
  • Plugin Systems: If your application supports add-ons, you can treat each add-on as a “service provider” that integrates new functionality without cluttering your core code.

Service providers keep your code organized and encourage modularity, making it easier to share or reuse common functionality across projects.

Related

A provider that owns an external resource (a database connection, a queue client) can also report its own health/readiness — see Provider Health & Readiness.