Skip to content

Register Service Providers

Service providers are the composition boundary between application features and the container. A provider describes how to construct a feature and where it connects to WordPress, leaving the feature class focused on its own behavior.

For example, an admin notice can live in one feature namespace:

src/
  Admin_Notice/
    Notice.php
    Provider.php

Name the feature provider Provider inside its namespace. This keeps references such as Admin_Notice\Provider immediately recognizable in the application’s provider list.

Add feature configuration to the root config.php:

<?php declare(strict_types=1);

return [
	'foundation' => [
		'prefix' => $_ENV['FOUNDATION_PREFIX'] ?? 'your-plugin',
	],
	'admin_notice' => [
		'capability' => $_ENV['ADMIN_NOTICE_CAPABILITY'] ?? 'manage_options',
	],
];

Application classes should receive resolved values through constructor injection rather than reading $_ENV or the configuration object directly.

Create src/Admin_Notice/Notice.php. The notice receives the capability supplied by its provider, while user-facing text remains in the feature so WordPress can translate it:

<?php declare(strict_types=1);

namespace YourPlugin\Admin_Notice;

use InvalidArgumentException;

/**
 * Displays the configured plugin notice in WordPress administration screens.
 */
final readonly class Notice {

	/**
	 * @throws InvalidArgumentException When the capability is empty.
	 */
	public function __construct(
		private string $capability
	) {
		if ( trim( $this->capability ) === '' ) {
			throw new InvalidArgumentException( 'The admin notice capability cannot be empty.' );
		}
	}

	/**
	 * Display the configured notice.
	 *
	 * @action admin_notices
	 */
	public function display(): void {
		if ( ! current_user_can( $this->capability ) ) {
			return;
		}

		printf(
			'<div class="notice notice-info"><p>%s</p></div>',
			esc_html__( 'Your plugin is ready.', 'your-plugin' )
		);
	}
}

The @action admin_notices annotation records why WordPress calls display(). The class itself does not register global hooks or resolve dependencies from the container.

In src/Admin_Notice/Provider.php, alias the Foundation base provider because it shares the Provider short name. Keep the feature’s definitions and hooks together in one focused registration method:

<?php declare(strict_types=1);

namespace YourPlugin\Admin_Notice;

use StellarWP\Foundation\Container\Contracts\Provider as Service_Provider;

/**
 * Configures the admin notice and connects it to WordPress.
 */
final class Provider extends Service_Provider {

	public function register(): void {
		$this->register_admin_notice();
	}

	private function register_admin_notice(): void {
		$this->container->when( Notice::class )
			->needs( '$capability' )
			->give( (string) $this->config->get( 'admin_notice.capability' ) );

		$this->container->singleton( Notice::class );

		add_action(
			'admin_notices',
			$this->container->callback( Notice::class, 'display' )
		);
	}
}

The provider registers the hook before resolving Notice. WordPress creates the notice through the container only when admin_notices runs.

As a provider grows, add methods named for the feature or capability they configure, such as register_admin_notice() or register_report_export(). Avoid grouping unrelated work under methods such as register_bindings() or register_hooks() merely because it uses the same API.

Register the feature in the ordered provider list in src/App.php:

use StellarWP\Foundation\Container\Contracts\Providable;
use YourPlugin\Admin_Notice;

/** @var list<class-string<Providable>> */
private const array PROVIDERS = [
	Admin_Notice\Provider::class,
];

Register infrastructure providers before feature providers that consume them. Avoid resolving application services while providers are still registering; complete the container graph before WordPress invokes its feature entrypoints.

For a larger feature, its top-level Provider may register internal providers so App only needs to know the feature entrypoint. Keep that provider as a pure composition boundary: if it registers other providers, it should not also contain bindings, configuration, hooks, or feature behavior. Providers shared across features still belong in the application’s ordered provider list.

Scope Foundation to the application before using shared WordPress resources.