From fcde7d5c0a5370055da3d8a6dc14b8611ad5c274 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 16:52:46 +0300 Subject: [PATCH 01/35] feat: introduce WP aware container contract and adapter --- README.md | 1 + composer.json | 2 + src/Container/Contracts/Container.php | 4 +- src/ContainerWordPress/.gitattributes | 7 + .../.github/workflows/close-pull-request.yml | 13 ++ src/ContainerWordPress/.gitignore | 2 + src/ContainerWordPress/ContainerAdapter.php | 135 ++++++++++++++++++ .../Contracts/Container.php | 66 +++++++++ src/ContainerWordPress/composer.json | 24 ++++ 9 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 src/ContainerWordPress/.gitattributes create mode 100644 src/ContainerWordPress/.github/workflows/close-pull-request.yml create mode 100644 src/ContainerWordPress/.gitignore create mode 100644 src/ContainerWordPress/ContainerAdapter.php create mode 100644 src/ContainerWordPress/Contracts/Container.php create mode 100644 src/ContainerWordPress/composer.json diff --git a/README.md b/README.md index 11db6f1..198ad1c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Foundation is a StellarWP Composer monorepo for reusable PHP packages intended f ## Repositories - [stellarwp/foundation-container](https://github.com/stellarwp/foundation-container) +- [stellarwp/foundation-container-wordpress](https://github.com/stellarwp/foundation-container-wordpress) - [stellarwp/foundation-pipeline](https://github.com/stellarwp/foundation-pipeline) - [stellarwp/foundation-log](https://github.com/stellarwp/foundation-log) - [stellarwp/foundation-wpcli](https://github.com/stellarwp/foundation-wpcli) diff --git a/composer.json b/composer.json index 8a27e39..ed974f9 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "replace": { "stellarwp/foundation-cli": "self.version", "stellarwp/foundation-container": "self.version", + "stellarwp/foundation-container-wordpress": "self.version", "stellarwp/foundation-log": "self.version", "stellarwp/foundation-pipeline": "self.version", "stellarwp/foundation-wpcli": "self.version" @@ -44,6 +45,7 @@ "psr-4": { "StellarWP\\Foundation\\Cli\\": "src/Cli/", "StellarWP\\Foundation\\Container\\": "src/Container/", + "StellarWP\\Foundation\\ContainerWordPress\\": "src/ContainerWordPress/", "StellarWP\\Foundation\\Log\\": "src/Log/", "StellarWP\\Foundation\\Pipeline\\": "src/Pipeline/", "StellarWP\\Foundation\\WPCli\\": "src/WPCli/" diff --git a/src/Container/Contracts/Container.php b/src/Container/Contracts/Container.php index 5502a94..2e05d5a 100644 --- a/src/Container/Contracts/Container.php +++ b/src/Container/Contracts/Container.php @@ -14,8 +14,8 @@ interface Container extends ContainerInterface /** * Register a service provider. * - * @param class-string $serviceProviderClass - * @param string ...$alias + * @param class-string $serviceProviderClass + * @param string ...$alias * * @throws \lucatume\DI52\ContainerException */ diff --git a/src/ContainerWordPress/.gitattributes b/src/ContainerWordPress/.gitattributes new file mode 100644 index 0000000..e82014a --- /dev/null +++ b/src/ContainerWordPress/.gitattributes @@ -0,0 +1,7 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore paths when git creates an archive of this package +.gitattributes export-ignore +.gitignore export-ignore +.github export-ignore diff --git a/src/ContainerWordPress/.github/workflows/close-pull-request.yml b/src/ContainerWordPress/.github/workflows/close-pull-request.yml new file mode 100644 index 0000000..6bfbabe --- /dev/null +++ b/src/ContainerWordPress/.github/workflows/close-pull-request.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "This is a read-only repository. Please submit your PR on the https://github.com/stellarwp/foundation repository.

Thanks!" diff --git a/src/ContainerWordPress/.gitignore b/src/ContainerWordPress/.gitignore new file mode 100644 index 0000000..d1502b0 --- /dev/null +++ b/src/ContainerWordPress/.gitignore @@ -0,0 +1,2 @@ +vendor/ +composer.lock diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php new file mode 100644 index 0000000..0f96c00 --- /dev/null +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -0,0 +1,135 @@ + $decorators, ?array $afterBuildMethods = null) + * @method void bindDecorators($id, array $decorators, ?array $afterBuildMethods = null) + * @method void bind(string $id, mixed $implementation = null, ?array $afterBuildMethods = null) + * @method mixed get(string $id) + * @method DI52Container get_container() + * @method bool has(string $id) + * @method void singleton(string $id, mixed $implementation = null, ?array $afterBuildMethods = null) + * @method void give(mixed $implementation) + * @method Closure instance(mixed $id, array $buildArgs = [], ?array $afterBuildMethods = null) + * @method callable callback(object|string $id, string $method) + */ +final class ContainerAdapter implements Container +{ + private readonly FoundationContainerAdapter $container; + + public function __construct(DI52Container $container) { + $this->container = new FoundationContainerAdapter($container); + } + + /** + * {@inheritDoc} + */ + public function register(string $serviceProviderClass, ...$alias): void { + $this->container->register($serviceProviderClass, ...$alias); + + do_action("{$serviceProviderClass}_registered"); + + foreach ($alias as $slug) { + do_action("{$slug}_registered"); + } + } + + /** + * {@inheritDoc} + */ + public function register_after_all_actions(array $actions, string $serviceProviderClass, ...$alias): void { + $not_done_actions = array_filter(array_map(static fn($action) => did_action( $action ) ? false : $action)); + if (empty($not_done_actions)) { + // All the actions are done already, we can register immediately. + $this->register($serviceProviderClass, ...$alias); + return; + } + + foreach ($not_done_actions as $not_done_action) { + $closure = function() use ($not_done_actions, $serviceProviderClass, $alias, &$closure) { + foreach ($not_done_actions as $nda) { + remove_action($nda, $closure); + } + + $this->register_after_all_actions($not_done_actions, $serviceProviderClass, ...$alias); + }; + + add_action($not_done_action, $closure); + } + } + + /** + * {@inhertiDoc} + */ + public function register_on_action(string $action, string $serviceProviderClass, ...$alias): void { + if (did_action($action)) { + // If the action has already fired, register the provider immediately. + $this->register($serviceProviderClass, ...$alias); + + return; + } + + // If the action has not fired yet, register the provider when/if it does. + $registration_closure = function() use ($action, $serviceProviderClass, $alias, &$registration_closure) { + // Remove the closure from the action to avoid calling it again. + remove_action($action, $registration_closure); + $this->register($serviceProviderClass, ...$alias); + }; + + add_action($action, $registration_closure); + } + + /** + * {@inhertiDoc} + */ + public function register_after_provider( + string $baseProviderClass, + string $dependantProviderClass, + ...$alias + ): void { + $this->register_on_action("{$baseProviderClass}_registered", $dependantProviderClass, ...$alias); + } + + /** + * {@inheritDoc} + */ + public function when(string $class): Container { + $this->container->when($class); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function needs(string $id): Container { + $this->container->needs($id); + + return $this; + } + + /** + * Defer all other calls to the wrapped Foundation container adapter. + * + * @param string $name The method name. + * @param mixed[] $args Method arguments. + */ + public function __call(string $name, array $args): mixed { + return $this->container->{$name}(...$args); + } +} diff --git a/src/ContainerWordPress/Contracts/Container.php b/src/ContainerWordPress/Contracts/Container.php new file mode 100644 index 0000000..33ef5ed --- /dev/null +++ b/src/ContainerWordPress/Contracts/Container.php @@ -0,0 +1,66 @@ + $serviceProviderClass The Service Provider to register on $action. + * @param string $alias Alias(es) for the $serviceProviderClass. + * + * @return void + * + * @throws \lucatume\DI52\ContainerException + */ + public function register_on_action(string $action, string $serviceProviderClass, ...$alias): void; + + /** + * @param class-string|string $baseProviderClass The provider class or id that the $dependantProviderClass + * depends on. + * @param class-string $dependantProviderClass The Service Provider to register after + * $baseProviderClass has been registered. + * @param string ...$alias Alias(es) for the $dependantProviderClass. + * + * @return void + * + * @throws \lucatume\DI52\ContainerException + */ + public function register_after_provider(string $baseProviderClass, string $dependantProviderClass, ...$alias): void; + + /** + * @param list $actions A list of actions that all need to be fired for the Provider to be + * registered. + * @param class-string $serviceProviderClass The Service Provider to register when the last action from $actions + * is fired. + * @param ...$alias Alias(es) for the $serviceProviderClass. + * + * @return void + * + * @throws \lucatume\DI52\ContainerException + */ + public function register_after_all_actions(array $actions, string $serviceProviderClass, ...$alias): void; +} diff --git a/src/ContainerWordPress/composer.json b/src/ContainerWordPress/composer.json new file mode 100644 index 0000000..a71c108 --- /dev/null +++ b/src/ContainerWordPress/composer.json @@ -0,0 +1,24 @@ +{ + "name": "stellarwp/foundation-container-wordpress", + "type": "library", + "description": "The Foundation DI container wrapper for WordPress projects.", + "license": "GPL-2.0-or-later", + "config": { + "vendor-dir": "vendor", + "preferred-install": "dist" + }, + "require": { + "php": ">=8.3", + "stellarwp/foundation-container": "^1.2" + }, + "autoload": { + "psr-4": { + "StellarWP\\Foundation\\ContainerWordPress\\": "" + } + }, + "extra": { + "branch-alias": { + "dev-main": "1.2.x-dev" + } + } +} From a7afb102ff33cb7538098b7be43dbc8783dff4a1 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 17:04:06 +0300 Subject: [PATCH 02/35] fix: pass $actions to array_map --- src/ContainerWordPress/ContainerAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index 0f96c00..60b0b5c 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -53,7 +53,7 @@ public function register(string $serviceProviderClass, ...$alias): void { * {@inheritDoc} */ public function register_after_all_actions(array $actions, string $serviceProviderClass, ...$alias): void { - $not_done_actions = array_filter(array_map(static fn($action) => did_action( $action ) ? false : $action)); + $not_done_actions = array_filter(array_map(static fn ($action) => did_action($action) ? false : $action, $actions)); if (empty($not_done_actions)) { // All the actions are done already, we can register immediately. $this->register($serviceProviderClass, ...$alias); From 53d37fc4ce41ee1b732933ae73e4937ff9e21321 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 17:07:29 +0300 Subject: [PATCH 03/35] fix: use same closure across all actions so we can reliably remove it --- src/ContainerWordPress/ContainerAdapter.php | 35 ++++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index 60b0b5c..04e3c07 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -53,28 +53,39 @@ public function register(string $serviceProviderClass, ...$alias): void { * {@inheritDoc} */ public function register_after_all_actions(array $actions, string $serviceProviderClass, ...$alias): void { - $not_done_actions = array_filter(array_map(static fn ($action) => did_action($action) ? false : $action, $actions)); - if (empty($not_done_actions)) { - // All the actions are done already, we can register immediately. + $pending = array_values(array_filter($actions, static fn (string $action): bool => ! did_action($action))); + + if ($pending === []) { + // All the actions have already fired, register the provider immediately. $this->register($serviceProviderClass, ...$alias); + return; } - foreach ($not_done_actions as $not_done_action) { - $closure = function() use ($not_done_actions, $serviceProviderClass, $alias, &$closure) { - foreach ($not_done_actions as $nda) { - remove_action($nda, $closure); + // A single closure is hooked onto every pending action. Whichever action fires + // last finds all actions done, registers once, and detaches from the rest. + $register_when_ready = function () use ($actions, $pending, $serviceProviderClass, $alias, &$register_when_ready): void { + foreach ($actions as $action) { + if (! did_action($action)) { + return; } + } - $this->register_after_all_actions($not_done_actions, $serviceProviderClass, ...$alias); - }; + // Detach from every pending action so the provider is only registered once. + foreach ($pending as $action) { + remove_action($action, $register_when_ready); + } - add_action($not_done_action, $closure); + $this->register($serviceProviderClass, ...$alias); + }; + + foreach ($pending as $action) { + add_action($action, $register_when_ready); } } /** - * {@inhertiDoc} + * {@inheritDoc} */ public function register_on_action(string $action, string $serviceProviderClass, ...$alias): void { if (did_action($action)) { @@ -95,7 +106,7 @@ public function register_on_action(string $action, string $serviceProviderClass, } /** - * {@inhertiDoc} + * {@inheritDoc} */ public function register_after_provider( string $baseProviderClass, From 09f3e498fffc2c4a2b8f4ed1b60673b28392b644 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 17:11:08 +0300 Subject: [PATCH 04/35] chore: pint issues --- src/ContainerWordPress/ContainerAdapter.php | 25 +++++++++---------- .../Contracts/Container.php | 8 +----- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index 04e3c07..d64f54d 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -4,7 +4,6 @@ use Closure; use lucatume\DI52\Container as DI52Container; -use lucatume\DI52\ContainerException; use StellarWP\Foundation\Container\ContainerAdapter as FoundationContainerAdapter; use StellarWP\Foundation\ContainerWordPress\Contracts\Container; @@ -15,18 +14,18 @@ * container API and gain WordPress-specific helpers. Add WordPress-specific * methods here alongside the matching signatures on {@see Container}. * - * @method mixed make(string $id) - * @method mixed getVar(string $key, mixed|null $default = null) - * @method void singletonDecorators($id, array $decorators, ?array $afterBuildMethods = null) - * @method void bindDecorators($id, array $decorators, ?array $afterBuildMethods = null) - * @method void bind(string $id, mixed $implementation = null, ?array $afterBuildMethods = null) - * @method mixed get(string $id) + * @method mixed make(string $id) + * @method mixed getVar(string $key, mixed|null $default = null) + * @method void singletonDecorators($id, array $decorators, ?array $afterBuildMethods = null) + * @method void bindDecorators($id, array $decorators, ?array $afterBuildMethods = null) + * @method void bind(string $id, mixed $implementation = null, ?array $afterBuildMethods = null) + * @method mixed get(string $id) * @method DI52Container get_container() - * @method bool has(string $id) - * @method void singleton(string $id, mixed $implementation = null, ?array $afterBuildMethods = null) - * @method void give(mixed $implementation) - * @method Closure instance(mixed $id, array $buildArgs = [], ?array $afterBuildMethods = null) - * @method callable callback(object|string $id, string $method) + * @method bool has(string $id) + * @method void singleton(string $id, mixed $implementation = null, ?array $afterBuildMethods = null) + * @method void give(mixed $implementation) + * @method Closure instance(mixed $id, array $buildArgs = [], ?array $afterBuildMethods = null) + * @method callable callback(object|string $id, string $method) */ final class ContainerAdapter implements Container { @@ -96,7 +95,7 @@ public function register_on_action(string $action, string $serviceProviderClass, } // If the action has not fired yet, register the provider when/if it does. - $registration_closure = function() use ($action, $serviceProviderClass, $alias, &$registration_closure) { + $registration_closure = function () use ($action, $serviceProviderClass, $alias, &$registration_closure) { // Remove the closure from the action to avoid calling it again. remove_action($action, $registration_closure); $this->register($serviceProviderClass, ...$alias); diff --git a/src/ContainerWordPress/Contracts/Container.php b/src/ContainerWordPress/Contracts/Container.php index 33ef5ed..65c9feb 100644 --- a/src/ContainerWordPress/Contracts/Container.php +++ b/src/ContainerWordPress/Contracts/Container.php @@ -28,12 +28,10 @@ public function when(string $class): Container; public function needs(string $id): Container; /** - * @param string $action. The WordPress action the $serviceProviderClass should be registered on. + * @param string $action The WordPress action the $serviceProviderClass should be registered on. * @param class-string $serviceProviderClass The Service Provider to register on $action. * @param string $alias Alias(es) for the $serviceProviderClass. * - * @return void - * * @throws \lucatume\DI52\ContainerException */ public function register_on_action(string $action, string $serviceProviderClass, ...$alias): void; @@ -45,8 +43,6 @@ public function register_on_action(string $action, string $serviceProviderClass, * $baseProviderClass has been registered. * @param string ...$alias Alias(es) for the $dependantProviderClass. * - * @return void - * * @throws \lucatume\DI52\ContainerException */ public function register_after_provider(string $baseProviderClass, string $dependantProviderClass, ...$alias): void; @@ -58,8 +54,6 @@ public function register_after_provider(string $baseProviderClass, string $depen * is fired. * @param ...$alias Alias(es) for the $serviceProviderClass. * - * @return void - * * @throws \lucatume\DI52\ContainerException */ public function register_after_all_actions(array $actions, string $serviceProviderClass, ...$alias): void; From d209ff8ab496f28a29e5a70d47c47bc79928883b Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 17:17:35 +0300 Subject: [PATCH 05/35] docs: add actions docblocks --- src/ContainerWordPress/ContainerAdapter.php | 38 +++++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index d64f54d..03f3a76 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -29,22 +29,54 @@ */ final class ContainerAdapter implements Container { + /** + * Prefix for the WordPress actions fired when a service provider is registered. + */ + private const string REGISTERED_ACTION_PREFIX = 'stellarwp/foundation/container/wp/'; + private readonly FoundationContainerAdapter $container; public function __construct(DI52Container $container) { $this->container = new FoundationContainerAdapter($container); } + /** + * Build the "registered" WordPress action name for a service provider or alias. + * + * @param string $identifier The service provider class or alias slug. + */ + private function registered_action(string $identifier): string { + return self::REGISTERED_ACTION_PREFIX . $identifier . '/registered'; + } + /** * {@inheritDoc} */ public function register(string $serviceProviderClass, ...$alias): void { $this->container->register($serviceProviderClass, ...$alias); - do_action("{$serviceProviderClass}_registered"); + /** + * Fires after a service provider has been registered in the container. + * + * The dynamic portion of the hook name, `$serviceProviderClass`, refers to the + * fully-qualified class name of the registered service provider. + * + * @param class-string $serviceProviderClass The registered service provider class. + * @param string[] $alias The aliases the provider was registered under. + */ + do_action($this->registered_action($serviceProviderClass), $serviceProviderClass, $alias); foreach ($alias as $slug) { - do_action("{$slug}_registered"); + /** + * Fires after a service provider has been registered, once per alias. + * + * The dynamic portion of the hook name, `$slug`, refers to an alias the + * service provider was registered under. + * + * @param class-string $serviceProviderClass The registered service provider class. + * @param string[] $alias The aliases the provider was registered under. + */ + do_action($this->registered_action($slug), $serviceProviderClass, $alias); } } @@ -112,7 +144,7 @@ public function register_after_provider( string $dependantProviderClass, ...$alias ): void { - $this->register_on_action("{$baseProviderClass}_registered", $dependantProviderClass, ...$alias); + $this->register_on_action($this->registered_action($baseProviderClass), $dependantProviderClass, ...$alias); } /** From 98042ae20cfd7f051db9d7f3818c8cda982f0af9 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 17:28:34 +0300 Subject: [PATCH 06/35] docs: add package readme file --- src/ContainerWordPress/README.md | 106 +++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/ContainerWordPress/README.md diff --git a/src/ContainerWordPress/README.md b/src/ContainerWordPress/README.md new file mode 100644 index 0000000..ec50c95 --- /dev/null +++ b/src/ContainerWordPress/README.md @@ -0,0 +1,106 @@ +# Foundation Container WordPress + +> [!WARNING] +> **This is a read-only repository!** For pull requests or issues, see [stellarwp/foundation](https://github.com/stellarwp/foundation). + +A WordPress-focused wrapper around [stellarwp/foundation-container](https://github.com/stellarwp/foundation-container). +It exposes the full Foundation DI container API and adds WordPress-specific helpers. + +## Installation + +```shell +composer require stellarwp/foundation-container-wordpress +``` + +## Usage + +Create a new `ContainerAdapter`, passing in an instance of di52. It implements the +[`Contracts/Container.php`](./Contracts/Container.php) interface, which extends the base +Foundation container contract: + +```php +bind(Container::class, $container); +``` + +Everything the [Foundation Container](https://github.com/stellarwp/foundation-container) can do, +this wrapper can do too — binding, singletons, service providers, contextual bindings, and so on. + +## WordPress helpers + +On top of the base container API, this wrapper adds hook-aware service provider +registration. These methods are declared on +[`Contracts/Container.php`](./Contracts/Container.php) and implemented in +[`ContainerAdapter.php`](./ContainerAdapter.php). + +All of them accept the same optional trailing `...$alias` arguments as the base +`register()` method. + +### Registration actions + +`register()` is overridden so that, once a provider has been registered, it fires +WordPress actions other code can hook onto: + +| Action | Fired | +| --- | --- | +| `stellarwp/foundation/container/wp/{$serviceProviderClass}/registered` | Once, for the registered provider class. | +| `stellarwp/foundation/container/wp/{$alias}/registered` | Once per alias the provider was registered under. | + +Both actions pass two arguments to listeners: the registered provider class +(`string`) and the list of aliases (`string[]`). + +```php +$container->register( My_Provider::class, 'my-alias' ); + +add_action( + 'stellarwp/foundation/container/wp/' . My_Provider::class . '/registered', + function ( string $provider_class, array $aliases ): void { + // React to My_Provider having been registered. + }, + 10, + 2 +); +``` + +### `register_on_action()` + +Register a provider when a WordPress action fires. If the action has already +fired, the provider is registered immediately; otherwise registration is deferred +until the action fires and happens only once. + +```php +// Register when `init` fires (or right away if it already has). +$container->register_on_action( 'init', My_Provider::class ); +``` + +### `register_after_provider()` + +Register a provider only after another provider has been registered. It builds on +the `.../registered` action above, so the dependant provider is wired up as soon +as the base provider is registered. + +```php +// Register Feature_Provider after Core_Provider has been registered. +$container->register_after_provider( Core_Provider::class, Feature_Provider::class ); +``` + +### `register_after_all_actions()` + +Register a provider only after *every* one of the given actions has fired. If all +of them have already fired, registration happens immediately; otherwise it waits +for the last one and then registers exactly once. + +```php +// Register once both `plugins_loaded` and `init` have fired. +$container->register_after_all_actions( [ 'plugins_loaded', 'init' ], My_Provider::class ); +``` From 1f6bfbddcd12b991513bfe1e19eee7afb04a4a84 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 17:32:07 +0300 Subject: [PATCH 07/35] chore: pint issues --- src/ContainerWordPress/ContainerAdapter.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index 03f3a76..1b498af 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -5,6 +5,7 @@ use Closure; use lucatume\DI52\Container as DI52Container; use StellarWP\Foundation\Container\ContainerAdapter as FoundationContainerAdapter; +use StellarWP\Foundation\Container\Contracts\Providable; use StellarWP\Foundation\ContainerWordPress\Contracts\Container; /** @@ -62,7 +63,7 @@ public function register(string $serviceProviderClass, ...$alias): void { * fully-qualified class name of the registered service provider. * * @param class-string $serviceProviderClass The registered service provider class. - * @param string[] $alias The aliases the provider was registered under. + * @param string[] $alias The aliases the provider was registered under. */ do_action($this->registered_action($serviceProviderClass), $serviceProviderClass, $alias); @@ -73,8 +74,8 @@ public function register(string $serviceProviderClass, ...$alias): void { * The dynamic portion of the hook name, `$slug`, refers to an alias the * service provider was registered under. * - * @param class-string $serviceProviderClass The registered service provider class. - * @param string[] $alias The aliases the provider was registered under. + * @param class-string $serviceProviderClass The registered service provider class. + * @param string[] $alias The aliases the provider was registered under. */ do_action($this->registered_action($slug), $serviceProviderClass, $alias); } From 3a99408d7e1703bdb5d0b265a3a920bcd66065a5 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 18:16:35 +0300 Subject: [PATCH 08/35] revert: change to container contract @param hinting --- src/Container/Contracts/Container.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Container/Contracts/Container.php b/src/Container/Contracts/Container.php index 2e05d5a..7d5fa5a 100644 --- a/src/Container/Contracts/Container.php +++ b/src/Container/Contracts/Container.php @@ -14,8 +14,8 @@ interface Container extends ContainerInterface /** * Register a service provider. * - * @param class-string $serviceProviderClass - * @param string ...$alias + * @param string $serviceProviderClass + * @param string ...$alias * * @throws \lucatume\DI52\ContainerException */ From 907b55a301d37e63f2e5df8f29ab1e1f9294e3f5 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 18:21:05 +0300 Subject: [PATCH 09/35] chore: fix pint issue --- src/Container/Contracts/Container.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Container/Contracts/Container.php b/src/Container/Contracts/Container.php index 7d5fa5a..d426e92 100644 --- a/src/Container/Contracts/Container.php +++ b/src/Container/Contracts/Container.php @@ -14,7 +14,6 @@ interface Container extends ContainerInterface /** * Register a service provider. * - * @param string $serviceProviderClass * @param string ...$alias * * @throws \lucatume\DI52\ContainerException From 0e73f8abdf379e7f1e3238f67982ec320767e81a Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 18:40:19 +0300 Subject: [PATCH 10/35] feat: added wordpress-stubs --- composer.json | 1 + phpstan.neon.dist | 2 + src/ContainerWordPress/ContainerAdapter.php | 81 ++++++++++++++++--- .../Contracts/Container.php | 2 +- 4 files changed, 73 insertions(+), 13 deletions(-) diff --git a/composer.json b/composer.json index ed974f9..41c1c2b 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "monorepo-php/monorepo": "^12.7", "nunomaduro/collision": "^8.9", "php-mock/php-mock-mockery": "^1.5", + "php-stubs/wordpress-stubs": ">=6.0", "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^2.2", "phpunit/phpunit": "^11.5", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 44a650b..0d0bfe2 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -3,6 +3,8 @@ parameters: paths: - src - tests + scanFiles: + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php excludePaths: analyse: - src/*/vendor/* diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index 1b498af..e7b3195 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -4,6 +4,7 @@ use Closure; use lucatume\DI52\Container as DI52Container; +use lucatume\DI52\ContainerException; use StellarWP\Foundation\Container\ContainerAdapter as FoundationContainerAdapter; use StellarWP\Foundation\Container\Contracts\Providable; use StellarWP\Foundation\ContainerWordPress\Contracts\Container; @@ -15,18 +16,10 @@ * container API and gain WordPress-specific helpers. Add WordPress-specific * methods here alongside the matching signatures on {@see Container}. * - * @method mixed make(string $id) - * @method mixed getVar(string $key, mixed|null $default = null) - * @method void singletonDecorators($id, array $decorators, ?array $afterBuildMethods = null) - * @method void bindDecorators($id, array $decorators, ?array $afterBuildMethods = null) - * @method void bind(string $id, mixed $implementation = null, ?array $afterBuildMethods = null) - * @method mixed get(string $id) - * @method DI52Container get_container() - * @method bool has(string $id) - * @method void singleton(string $id, mixed $implementation = null, ?array $afterBuildMethods = null) - * @method void give(mixed $implementation) - * @method Closure instance(mixed $id, array $buildArgs = [], ?array $afterBuildMethods = null) - * @method callable callback(object|string $id, string $method) + * @method mixed make(string $id) + * @method mixed getVar(string $key, mixed|null $default = null) + * @method void singletonDecorators($id, array $decorators, ?array $afterBuildMethods = null) + * @method void bindDecorators($id, array $decorators, ?array $afterBuildMethods = null) */ final class ContainerAdapter implements Container { @@ -45,6 +38,8 @@ public function __construct(DI52Container $container) { * Build the "registered" WordPress action name for a service provider or alias. * * @param string $identifier The service provider class or alias slug. + * + * @return non-empty-string */ private function registered_action(string $identifier): string { return self::REGISTERED_ACTION_PREFIX . $identifier . '/registered'; @@ -166,6 +161,68 @@ public function needs(string $id): Container { return $this; } + /** + * @param string[]|null $afterBuildMethods + * + * @throws \lucatume\DI52\ContainerException + */ + public function bind(string $id, mixed $implementation = null, ?array $afterBuildMethods = null): void { + $this->container->bind($id, $implementation, $afterBuildMethods); + } + + /** + * {@inheritDoc} + */ + public function get(string $id): mixed { + return $this->container->get($id); + } + + /** + * @codeCoverageIgnore + */ + public function get_container(): DI52Container { + return $this->container->get_container(); + } + + /** + * {@inheritDoc} + * + * @codeCoverageIgnore + */ + public function has(string $id): bool { + return $this->container->has($id); + } + + /** + * @param string[]|null $afterBuildMethods + * + * @throws \lucatume\DI52\ContainerException + */ + public function singleton(string $id, mixed $implementation = null, ?array $afterBuildMethods = null): void { + $this->container->singleton($id, $implementation, $afterBuildMethods); + } + + public function give(mixed $implementation): void { + $this->container->give($implementation); + } + + /** + * @param array $buildArgs + * @param string[]|null $afterBuildMethods + */ + public function instance(mixed $id, array $buildArgs = [], ?array $afterBuildMethods = null): Closure { + return $this->container->instance($id, $buildArgs, $afterBuildMethods); + } + + /** + * @param class-string|string|object $id + * + * @throws ContainerException + */ + public function callback(object|string $id, string $method): callable { + return $this->container->callback($id, $method); + } + /** * Defer all other calls to the wrapped Foundation container adapter. * diff --git a/src/ContainerWordPress/Contracts/Container.php b/src/ContainerWordPress/Contracts/Container.php index 65c9feb..72022bb 100644 --- a/src/ContainerWordPress/Contracts/Container.php +++ b/src/ContainerWordPress/Contracts/Container.php @@ -52,7 +52,7 @@ public function register_after_provider(string $baseProviderClass, string $depen * registered. * @param class-string $serviceProviderClass The Service Provider to register when the last action from $actions * is fired. - * @param ...$alias Alias(es) for the $serviceProviderClass. + * @param string ...$alias Alias(es) for the $serviceProviderClass. * * @throws \lucatume\DI52\ContainerException */ From d947f4fad4f4f26d1f8abced817dc6bc13203732 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 18:47:23 +0300 Subject: [PATCH 11/35] chore: static analysis --- src/Container/Contracts/Container.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Container/Contracts/Container.php b/src/Container/Contracts/Container.php index d426e92..5502a94 100644 --- a/src/Container/Contracts/Container.php +++ b/src/Container/Contracts/Container.php @@ -14,7 +14,8 @@ interface Container extends ContainerInterface /** * Register a service provider. * - * @param string ...$alias + * @param class-string $serviceProviderClass + * @param string ...$alias * * @throws \lucatume\DI52\ContainerException */ From f2e938398c35c552ff91aaa8135cad156ef18ed4 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 19:36:53 +0300 Subject: [PATCH 12/35] refactor: amend CR comments --- src/ContainerWordPress/ContainerAdapter.php | 33 +++++++++-------- .../Contracts/Container.php | 6 ++-- src/ContainerWordPress/README.md | 36 ++++++++++++++++++- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index e7b3195..511d340 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -5,6 +5,7 @@ use Closure; use lucatume\DI52\Container as DI52Container; use lucatume\DI52\ContainerException; +use RuntimeException; use StellarWP\Foundation\Container\ContainerAdapter as FoundationContainerAdapter; use StellarWP\Foundation\Container\Contracts\Providable; use StellarWP\Foundation\ContainerWordPress\Contracts\Container; @@ -23,15 +24,10 @@ */ final class ContainerAdapter implements Container { - /** - * Prefix for the WordPress actions fired when a service provider is registered. - */ - private const string REGISTERED_ACTION_PREFIX = 'stellarwp/foundation/container/wp/'; - - private readonly FoundationContainerAdapter $container; - - public function __construct(DI52Container $container) { - $this->container = new FoundationContainerAdapter($container); + public function __construct( + private readonly FoundationContainerAdapter $container, + private readonly string $prefix = 'stellarwp/foundation/container/wp/', + ) { } /** @@ -41,8 +37,11 @@ public function __construct(DI52Container $container) { * * @return non-empty-string */ - private function registered_action(string $identifier): string { - return self::REGISTERED_ACTION_PREFIX . $identifier . '/registered'; + private function registeredAction(string $identifier): string { + if (!$identifier) { + throw new RuntimeException( "You need to provide an identifier!" ) + } + return $this->prefix . $identifier . '/registered'; } /** @@ -60,7 +59,7 @@ public function register(string $serviceProviderClass, ...$alias): void { * @param class-string $serviceProviderClass The registered service provider class. * @param string[] $alias The aliases the provider was registered under. */ - do_action($this->registered_action($serviceProviderClass), $serviceProviderClass, $alias); + do_action($this->registeredAction($serviceProviderClass), $serviceProviderClass, $alias); foreach ($alias as $slug) { /** @@ -72,14 +71,14 @@ public function register(string $serviceProviderClass, ...$alias): void { * @param class-string $serviceProviderClass The registered service provider class. * @param string[] $alias The aliases the provider was registered under. */ - do_action($this->registered_action($slug), $serviceProviderClass, $alias); + do_action($this->registeredAction($slug), $serviceProviderClass, $alias); } } /** * {@inheritDoc} */ - public function register_after_all_actions(array $actions, string $serviceProviderClass, ...$alias): void { + public function registerAfterAllActions(array $actions, string $serviceProviderClass, ...$alias): void { $pending = array_values(array_filter($actions, static fn (string $action): bool => ! did_action($action))); if ($pending === []) { @@ -114,7 +113,7 @@ public function register_after_all_actions(array $actions, string $serviceProvid /** * {@inheritDoc} */ - public function register_on_action(string $action, string $serviceProviderClass, ...$alias): void { + public function registerOnAction(string $action, string $serviceProviderClass, ...$alias): void { if (did_action($action)) { // If the action has already fired, register the provider immediately. $this->register($serviceProviderClass, ...$alias); @@ -135,12 +134,12 @@ public function register_on_action(string $action, string $serviceProviderClass, /** * {@inheritDoc} */ - public function register_after_provider( + public function registerOnProvider( string $baseProviderClass, string $dependantProviderClass, ...$alias ): void { - $this->register_on_action($this->registered_action($baseProviderClass), $dependantProviderClass, ...$alias); + $this->registerOnAction($this->registeredAction($baseProviderClass), $dependantProviderClass, ...$alias); } /** diff --git a/src/ContainerWordPress/Contracts/Container.php b/src/ContainerWordPress/Contracts/Container.php index 72022bb..b0ad455 100644 --- a/src/ContainerWordPress/Contracts/Container.php +++ b/src/ContainerWordPress/Contracts/Container.php @@ -34,7 +34,7 @@ public function needs(string $id): Container; * * @throws \lucatume\DI52\ContainerException */ - public function register_on_action(string $action, string $serviceProviderClass, ...$alias): void; + public function registerOnAction(string $action, string $serviceProviderClass, ...$alias): void; /** * @param class-string|string $baseProviderClass The provider class or id that the $dependantProviderClass @@ -45,7 +45,7 @@ public function register_on_action(string $action, string $serviceProviderClass, * * @throws \lucatume\DI52\ContainerException */ - public function register_after_provider(string $baseProviderClass, string $dependantProviderClass, ...$alias): void; + public function registerOnProvider(string $baseProviderClass, string $dependantProviderClass, ...$alias): void; /** * @param list $actions A list of actions that all need to be fired for the Provider to be @@ -56,5 +56,5 @@ public function register_after_provider(string $baseProviderClass, string $depen * * @throws \lucatume\DI52\ContainerException */ - public function register_after_all_actions(array $actions, string $serviceProviderClass, ...$alias): void; + public function registerAfterAllActions(array $actions, string $serviceProviderClass, ...$alias): void; } diff --git a/src/ContainerWordPress/README.md b/src/ContainerWordPress/README.md index ec50c95..e59d5f1 100644 --- a/src/ContainerWordPress/README.md +++ b/src/ContainerWordPress/README.md @@ -24,10 +24,11 @@ Foundation container contract: namespace My\App; use lucatume\DI52\Container; +use StellarWP\Foundation\Container\ContainerAdapter; use StellarWP\Foundation\ContainerWordPress\ContainerAdapter; // This implements the Contracts/Container.php interface. -$container = new ContainerAdapter(new Container()); +$container = new ContainerAdapter(new FoundationContainerAdapter(new Container())); // Bind the concrete to the interface, so anytime we ask for a container we get this one. $container->bind(Container::class, $container); @@ -46,6 +47,39 @@ registration. These methods are declared on All of them accept the same optional trailing `...$alias` arguments as the base `register()` method. +## Hook Prefix + +The WordPress container adapter will fire registration hooks when a Provider is being registered. By default, we use +the `'stellarwp/foundation/container/wp/'` as the hook prefix, but you can easily change that by passing a second argument +during the adapter's initialization. + +```php +$container = new ContainerAdapter(new FoundationContainerAdapter(new Container()), 'my/hook/prefix/'); + +add_action( + 'my/hook/prefix/' . My_Provider::class . '/registered', + function ( string $provider_class, array $aliases ): void { + // React to My_Provider having been registered. + }, + 10, + 2 +); + +# Or by using it's alias +add_action( + 'my/hook/prefix/my-alias/registered', + function ( string $provider_class, array $aliases ): void { + // React to My_Provider having been registered. + }, + 10, + 2 +); + +$container->register( My_Provider::class, 'my-alias' ); + + +``` + ### Registration actions `register()` is overridden so that, once a provider has been registered, it fires From f9b97851281c253a55980004b4c373cf8b986c7e Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 19:37:47 +0300 Subject: [PATCH 13/35] tweak: make a callback static --- src/ContainerWordPress/ContainerAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index 511d340..4d9a1b1 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -90,7 +90,7 @@ public function registerAfterAllActions(array $actions, string $serviceProviderC // A single closure is hooked onto every pending action. Whichever action fires // last finds all actions done, registers once, and detaches from the rest. - $register_when_ready = function () use ($actions, $pending, $serviceProviderClass, $alias, &$register_when_ready): void { + $register_when_ready = static function () use ($actions, $pending, $serviceProviderClass, $alias, &$register_when_ready): void { foreach ($actions as $action) { if (! did_action($action)) { return; From c91e577f307bbc2d1a6a61cb6fb0ff7b52182120 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 19:39:25 +0300 Subject: [PATCH 14/35] refactor: rename method get_container to getContainer --- src/Container/ContainerAdapter.php | 2 +- src/ContainerWordPress/ContainerAdapter.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Container/ContainerAdapter.php b/src/Container/ContainerAdapter.php index 91d728a..ccfdc5e 100644 --- a/src/Container/ContainerAdapter.php +++ b/src/Container/ContainerAdapter.php @@ -39,7 +39,7 @@ public function get(string $id): mixed { /** * @codeCoverageIgnore */ - public function get_container(): DI52Container { + public function getContainer(): DI52Container { return $this->container; } diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index 4d9a1b1..b9b44a7 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -179,8 +179,8 @@ public function get(string $id): mixed { /** * @codeCoverageIgnore */ - public function get_container(): DI52Container { - return $this->container->get_container(); + public function getContainer(): DI52Container { + return $this->container->getContainer(); } /** From 986ba7a88213cd47b706721b1c6351540cae3c84 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 19:41:00 +0300 Subject: [PATCH 15/35] Revert "tweak: make a callback static" This reverts commit f9b97851281c253a55980004b4c373cf8b986c7e. --- src/ContainerWordPress/ContainerAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index b9b44a7..6826265 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -90,7 +90,7 @@ public function registerAfterAllActions(array $actions, string $serviceProviderC // A single closure is hooked onto every pending action. Whichever action fires // last finds all actions done, registers once, and detaches from the rest. - $register_when_ready = static function () use ($actions, $pending, $serviceProviderClass, $alias, &$register_when_ready): void { + $register_when_ready = function () use ($actions, $pending, $serviceProviderClass, $alias, &$register_when_ready): void { foreach ($actions as $action) { if (! did_action($action)) { return; From 2384e22cdeeafdf0167133b39036edf9791c8dd5 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 19:41:20 +0300 Subject: [PATCH 16/35] tweak: fix some typos --- src/ContainerWordPress/ContainerAdapter.php | 4 ++-- src/ContainerWordPress/Contracts/Container.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index 6826265..db2dc27 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -136,10 +136,10 @@ public function registerOnAction(string $action, string $serviceProviderClass, . */ public function registerOnProvider( string $baseProviderClass, - string $dependantProviderClass, + string $dependentProviderClass, ...$alias ): void { - $this->registerOnAction($this->registeredAction($baseProviderClass), $dependantProviderClass, ...$alias); + $this->registerOnAction($this->registeredAction($baseProviderClass), $dependentProviderClass, ...$alias); } /** diff --git a/src/ContainerWordPress/Contracts/Container.php b/src/ContainerWordPress/Contracts/Container.php index b0ad455..054ea82 100644 --- a/src/ContainerWordPress/Contracts/Container.php +++ b/src/ContainerWordPress/Contracts/Container.php @@ -37,15 +37,15 @@ public function needs(string $id): Container; public function registerOnAction(string $action, string $serviceProviderClass, ...$alias): void; /** - * @param class-string|string $baseProviderClass The provider class or id that the $dependantProviderClass + * @param class-string|string $baseProviderClass The provider class or id that the $dependentProviderClass * depends on. - * @param class-string $dependantProviderClass The Service Provider to register after + * @param class-string $dependentProviderClass The Service Provider to register after * $baseProviderClass has been registered. - * @param string ...$alias Alias(es) for the $dependantProviderClass. + * @param string ...$alias Alias(es) for the $dependentProviderClass. * * @throws \lucatume\DI52\ContainerException */ - public function registerOnProvider(string $baseProviderClass, string $dependantProviderClass, ...$alias): void; + public function registerOnProvider(string $baseProviderClass, string $dependentProviderClass, ...$alias): void; /** * @param list $actions A list of actions that all need to be fired for the Provider to be From 685e81d9decd0bc91043cf3d669410ee72c77d31 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 19:42:34 +0300 Subject: [PATCH 17/35] fix: added missed semicolon --- src/ContainerWordPress/ContainerAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index db2dc27..64211f3 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -39,7 +39,7 @@ public function __construct( */ private function registeredAction(string $identifier): string { if (!$identifier) { - throw new RuntimeException( "You need to provide an identifier!" ) + throw new RuntimeException( "You need to provide an identifier!" ); } return $this->prefix . $identifier . '/registered'; } From 964ef7764757c97038d55506827d9dbdd9cf36ed Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 19:45:35 +0300 Subject: [PATCH 18/35] tweak: rename default hook prefix --- src/ContainerWordPress/ContainerAdapter.php | 10 +++++----- src/ContainerWordPress/README.md | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index 64211f3..d08d8c1 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -3,9 +3,9 @@ namespace StellarWP\Foundation\ContainerWordPress; use Closure; +use InvalidArgumentException; use lucatume\DI52\Container as DI52Container; use lucatume\DI52\ContainerException; -use RuntimeException; use StellarWP\Foundation\Container\ContainerAdapter as FoundationContainerAdapter; use StellarWP\Foundation\Container\Contracts\Providable; use StellarWP\Foundation\ContainerWordPress\Contracts\Container; @@ -22,11 +22,11 @@ * @method void singletonDecorators($id, array $decorators, ?array $afterBuildMethods = null) * @method void bindDecorators($id, array $decorators, ?array $afterBuildMethods = null) */ -final class ContainerAdapter implements Container +final readonly class ContainerAdapter implements Container { public function __construct( - private readonly FoundationContainerAdapter $container, - private readonly string $prefix = 'stellarwp/foundation/container/wp/', + private FoundationContainerAdapter $container, + private string $prefix = 'nexcess/foundation/container/wp/', ) { } @@ -39,7 +39,7 @@ public function __construct( */ private function registeredAction(string $identifier): string { if (!$identifier) { - throw new RuntimeException( "You need to provide an identifier!" ); + throw new InvalidArgumentException( "You need to provide an identifier!" ); } return $this->prefix . $identifier . '/registered'; } diff --git a/src/ContainerWordPress/README.md b/src/ContainerWordPress/README.md index e59d5f1..cb6f550 100644 --- a/src/ContainerWordPress/README.md +++ b/src/ContainerWordPress/README.md @@ -50,7 +50,7 @@ All of them accept the same optional trailing `...$alias` arguments as the base ## Hook Prefix The WordPress container adapter will fire registration hooks when a Provider is being registered. By default, we use -the `'stellarwp/foundation/container/wp/'` as the hook prefix, but you can easily change that by passing a second argument +the `'nexcess/foundation/container/wp/'` as the hook prefix, but you can easily change that by passing a second argument during the adapter's initialization. ```php @@ -85,10 +85,10 @@ $container->register( My_Provider::class, 'my-alias' ); `register()` is overridden so that, once a provider has been registered, it fires WordPress actions other code can hook onto: -| Action | Fired | -| --- | --- | -| `stellarwp/foundation/container/wp/{$serviceProviderClass}/registered` | Once, for the registered provider class. | -| `stellarwp/foundation/container/wp/{$alias}/registered` | Once per alias the provider was registered under. | +| Action | Fired | +|----------------------------------------------------------------------| --- | +| `nexcess/foundation/container/wp/{$serviceProviderClass}/registered` | Once, for the registered provider class. | +| `nexcess/foundation/container/wp/{$alias}/registered` | Once per alias the provider was registered under. | Both actions pass two arguments to listeners: the registered provider class (`string`) and the list of aliases (`string[]`). @@ -97,7 +97,7 @@ Both actions pass two arguments to listeners: the registered provider class $container->register( My_Provider::class, 'my-alias' ); add_action( - 'stellarwp/foundation/container/wp/' . My_Provider::class . '/registered', + 'nexcess/foundation/container/wp/' . My_Provider::class . '/registered', function ( string $provider_class, array $aliases ): void { // React to My_Provider having been registered. }, From 72b86a643befe3a17aaeb5ebeb29ec71b006aa5d Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 19:47:21 +0300 Subject: [PATCH 19/35] fix: require 1.1 of the foundation container --- src/ContainerWordPress/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ContainerWordPress/composer.json b/src/ContainerWordPress/composer.json index a71c108..62199ba 100644 --- a/src/ContainerWordPress/composer.json +++ b/src/ContainerWordPress/composer.json @@ -9,7 +9,7 @@ }, "require": { "php": ">=8.3", - "stellarwp/foundation-container": "^1.2" + "stellarwp/foundation-container": "^1.1" }, "autoload": { "psr-4": { From 5631536bdc589ac4870ebae4f21bcaed278f0e00 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 19:48:25 +0300 Subject: [PATCH 20/35] chore: pint issue --- src/ContainerWordPress/ContainerAdapter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index d08d8c1..1aad4bb 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -38,8 +38,8 @@ public function __construct( * @return non-empty-string */ private function registeredAction(string $identifier): string { - if (!$identifier) { - throw new InvalidArgumentException( "You need to provide an identifier!" ); + if (! $identifier) { + throw new InvalidArgumentException('You need to provide an identifier!'); } return $this->prefix . $identifier . '/registered'; } From 086358738c77707b865e395909ac859f7901d9a4 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 20:00:49 +0300 Subject: [PATCH 21/35] feat: bump di52 to 4.1 --- composer.json | 4 ++-- src/Container/composer.json | 2 +- src/ContainerWordPress/composer.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 41c1c2b..25cd000 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "ext-curl": "*", "ext-exif": "*", "adbario/php-dot-notation": ">=2.5", - "lucatume/di52": ">=3.0", + "lucatume/di52": ">=4.1", "monolog/monolog": "^2.11", "psr/log": ">=1.0", "stellarwp/container-contract": "^1.1", @@ -45,8 +45,8 @@ "autoload": { "psr-4": { "StellarWP\\Foundation\\Cli\\": "src/Cli/", - "StellarWP\\Foundation\\Container\\": "src/Container/", "StellarWP\\Foundation\\ContainerWordPress\\": "src/ContainerWordPress/", + "StellarWP\\Foundation\\Container\\": "src/Container/", "StellarWP\\Foundation\\Log\\": "src/Log/", "StellarWP\\Foundation\\Pipeline\\": "src/Pipeline/", "StellarWP\\Foundation\\WPCli\\": "src/WPCli/" diff --git a/src/Container/composer.json b/src/Container/composer.json index 9e083a6..06734e7 100644 --- a/src/Container/composer.json +++ b/src/Container/composer.json @@ -10,7 +10,7 @@ "require": { "php": ">=8.3", "adbario/php-dot-notation": ">=2.5", - "lucatume/di52": ">=3.0", + "lucatume/di52": ">=4.1", "stellarwp/container-contract": "^1.1", "vlucas/phpdotenv": ">=4.3" }, diff --git a/src/ContainerWordPress/composer.json b/src/ContainerWordPress/composer.json index 62199ba..a71c108 100644 --- a/src/ContainerWordPress/composer.json +++ b/src/ContainerWordPress/composer.json @@ -9,7 +9,7 @@ }, "require": { "php": ">=8.3", - "stellarwp/foundation-container": "^1.1" + "stellarwp/foundation-container": "^1.2" }, "autoload": { "psr-4": { From 6accd508125b1f9023ac62795980a93a01504670 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 20:02:25 +0300 Subject: [PATCH 22/35] feat: added mergeArrayVar method --- src/Container/ContainerAdapter.php | 9 +++++++++ src/Container/Contracts/Container.php | 9 +++++++++ src/ContainerWordPress/ContainerAdapter.php | 9 +++++++++ 3 files changed, 27 insertions(+) diff --git a/src/Container/ContainerAdapter.php b/src/Container/ContainerAdapter.php index ccfdc5e..96bce13 100644 --- a/src/Container/ContainerAdapter.php +++ b/src/Container/ContainerAdapter.php @@ -95,6 +95,15 @@ public function instance(mixed $id, array $buildArgs = [], ?array $afterBuildMet return $this->container->instance($id, $buildArgs, $afterBuildMethods); } + /** + * {@inheritDoc} + * + * @throws ContainerException + */ + public function mergeArrayVar(string $id, mixed $implementation): void { + $this->container->mergeArrayVar($id, $implementation); + } + /** * @param class-string|string|object $id * diff --git a/src/Container/Contracts/Container.php b/src/Container/Contracts/Container.php index 5502a94..3c1aca2 100644 --- a/src/Container/Contracts/Container.php +++ b/src/Container/Contracts/Container.php @@ -37,6 +37,15 @@ public function needs(string $id): Container; public function give(mixed $implementation): void; + /** + * Add array values to an existing or future binding without replacing previous values. + * + * @param class-string|string $id + * + * @throws \lucatume\DI52\ContainerException + */ + public function mergeArrayVar(string $id, mixed $implementation): void; + /** * Returns a callable object (Closure) that will build an instance of the specified * class using the specified arguments when called. diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index 1aad4bb..7c35051 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -205,6 +205,15 @@ public function give(mixed $implementation): void { $this->container->give($implementation); } + /** + * {@inheritDoc} + * + * @throws ContainerException + */ + public function mergeArrayVar(string $id, mixed $implementation): void { + $this->container->mergeArrayVar($id, $implementation); + } + /** * @param array $buildArgs * @param string[]|null $afterBuildMethods From a6d2dd9c06b394f82de67f2ce1dc275ab593d6f3 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 20:04:11 +0300 Subject: [PATCH 23/35] chore: pint issue --- src/ContainerWordPress/ContainerAdapter.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index 7c35051..64c4010 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -41,6 +41,7 @@ private function registeredAction(string $identifier): string { if (! $identifier) { throw new InvalidArgumentException('You need to provide an identifier!'); } + return $this->prefix . $identifier . '/registered'; } From ade2a4d2cb24546094daf7fecb3b6118fbebf010 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 20:04:17 +0300 Subject: [PATCH 24/35] docs: fix binding --- src/ContainerWordPress/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ContainerWordPress/README.md b/src/ContainerWordPress/README.md index cb6f550..9f42f8d 100644 --- a/src/ContainerWordPress/README.md +++ b/src/ContainerWordPress/README.md @@ -24,6 +24,7 @@ Foundation container contract: namespace My\App; use lucatume\DI52\Container; +use StellarWP\Foundation\Container\Contracts\Container as ContainerContract; use StellarWP\Foundation\Container\ContainerAdapter; use StellarWP\Foundation\ContainerWordPress\ContainerAdapter; @@ -31,7 +32,7 @@ use StellarWP\Foundation\ContainerWordPress\ContainerAdapter; $container = new ContainerAdapter(new FoundationContainerAdapter(new Container())); // Bind the concrete to the interface, so anytime we ask for a container we get this one. -$container->bind(Container::class, $container); +$container->bind(ContainerContract::class, $container); ``` Everything the [Foundation Container](https://github.com/stellarwp/foundation-container) can do, From 864a2713fbb7cc1a3f11a06db15967c2d5c3fdd4 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 20:43:49 +0300 Subject: [PATCH 25/35] docs: amend change in method names --- src/ContainerWordPress/ContainerAdapter.php | 2 ++ src/ContainerWordPress/README.md | 16 +++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index 64c4010..5549d37 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -36,6 +36,8 @@ public function __construct( * @param string $identifier The service provider class or alias slug. * * @return non-empty-string + * + * @throws InvalidArgumentException When the provided $identifier is empty. */ private function registeredAction(string $identifier): string { if (! $identifier) { diff --git a/src/ContainerWordPress/README.md b/src/ContainerWordPress/README.md index 9f42f8d..e1075fe 100644 --- a/src/ContainerWordPress/README.md +++ b/src/ContainerWordPress/README.md @@ -25,7 +25,8 @@ namespace My\App; use lucatume\DI52\Container; use StellarWP\Foundation\Container\Contracts\Container as ContainerContract; -use StellarWP\Foundation\Container\ContainerAdapter; +use StellarWP\Foundation\ContainerWordPress\Contracts\Container as WPContainerContract; +use StellarWP\Foundation\Container\ContainerAdapter as FoundationContainerAdapter; use StellarWP\Foundation\ContainerWordPress\ContainerAdapter; // This implements the Contracts/Container.php interface. @@ -33,6 +34,7 @@ $container = new ContainerAdapter(new FoundationContainerAdapter(new Container() // Bind the concrete to the interface, so anytime we ask for a container we get this one. $container->bind(ContainerContract::class, $container); +$container->bind(WPContainerContract::class, $container); ``` Everything the [Foundation Container](https://github.com/stellarwp/foundation-container) can do, @@ -107,7 +109,7 @@ add_action( ); ``` -### `register_on_action()` +### `registerOnAction()` Register a provider when a WordPress action fires. If the action has already fired, the provider is registered immediately; otherwise registration is deferred @@ -115,10 +117,10 @@ until the action fires and happens only once. ```php // Register when `init` fires (or right away if it already has). -$container->register_on_action( 'init', My_Provider::class ); +$container->registerOnAction( 'init', My_Provider::class ); ``` -### `register_after_provider()` +### `registerOnProvider()` Register a provider only after another provider has been registered. It builds on the `.../registered` action above, so the dependant provider is wired up as soon @@ -126,10 +128,10 @@ as the base provider is registered. ```php // Register Feature_Provider after Core_Provider has been registered. -$container->register_after_provider( Core_Provider::class, Feature_Provider::class ); +$container->registerOnProvider( Core_Provider::class, Feature_Provider::class ); ``` -### `register_after_all_actions()` +### `registerAfterAllActions()` Register a provider only after *every* one of the given actions has fired. If all of them have already fired, registration happens immediately; otherwise it waits @@ -137,5 +139,5 @@ for the last one and then registers exactly once. ```php // Register once both `plugins_loaded` and `init` have fired. -$container->register_after_all_actions( [ 'plugins_loaded', 'init' ], My_Provider::class ); +$container->registerAfterAllActions( [ 'plugins_loaded', 'init' ], My_Provider::class ); ``` From 46e8806415ec0046b0973149b879e606cb6ee423 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 20:47:13 +0300 Subject: [PATCH 26/35] chore: pint issue --- src/ContainerWordPress/ContainerAdapter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index 5549d37..835c34c 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -35,9 +35,9 @@ public function __construct( * * @param string $identifier The service provider class or alias slug. * - * @return non-empty-string - * * @throws InvalidArgumentException When the provided $identifier is empty. + * + * @return non-empty-string */ private function registeredAction(string $identifier): string { if (! $identifier) { From ce6901c377c2f568d241fe5d8c7d436c646e6b6b Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 22:15:30 +0300 Subject: [PATCH 27/35] tests: adding integration coverage --- .../ContainerWordPress/FirstProvider.php | 20 ++ .../ContainerWordPress/SecondProvider.php | 20 ++ .../ContainerAdapterTest.php | 230 ++++++++++++++++++ 3 files changed, 270 insertions(+) create mode 100644 tests/Support/Fixtures/ContainerWordPress/FirstProvider.php create mode 100644 tests/Support/Fixtures/ContainerWordPress/SecondProvider.php create mode 100644 tests/wpunit/ContainerWordPress/ContainerAdapterTest.php diff --git a/tests/Support/Fixtures/ContainerWordPress/FirstProvider.php b/tests/Support/Fixtures/ContainerWordPress/FirstProvider.php new file mode 100644 index 0000000..e405239 --- /dev/null +++ b/tests/Support/Fixtures/ContainerWordPress/FirstProvider.php @@ -0,0 +1,20 @@ +adapter = new ContainerAdapter(new FoundationContainerAdapter(new DI52Container())); + + $this->adapter->bind(FoundationContainer::class, $this->adapter); + $this->adapter->bind(WPContainerContract::class, $this->adapter); + } + + /** + * Build the "registered" action name the adapter fires for a provider or alias. + */ + private function registered_action(string $identifier): string { + return 'nexcess/foundation/container/wp/' . $identifier . '/registered'; + } + + /** + * Count how many times a WordPress action fires while a callback is attached. + */ + private function count_action(string $action): callable { + $count = 0; + + add_action($action, static function () use (&$count): void { + $count++; + }); + + return static fn (): int => $count; + } + + public function test_register_fires_a_registered_action_for_the_provider(): void { + $fired = $this->count_action($this->registered_action(FirstProvider::class)); + + $this->adapter->register(FirstProvider::class); + + $this->assertSame(1, $fired()); + + $this->adapter->register(FirstProvider::class); + + $this->assertSame(2, $fired()); + } + + public function test_register_fires_a_registered_action_for_each_alias(): void { + $provider = $this->count_action($this->registered_action(FirstProvider::class)); + $alpha = $this->count_action($this->registered_action('alpha')); + $beta = $this->count_action($this->registered_action('beta')); + + $this->adapter->register(FirstProvider::class, 'alpha', 'beta'); + + $this->assertSame(1, $provider()); + $this->assertSame(1, $alpha()); + $this->assertSame(1, $beta()); + + $this->adapter->register(FirstProvider::class, 'alpha', 'beta'); + + $this->assertSame(2, $provider()); + $this->assertSame(2, $alpha()); + $this->assertSame(2, $beta()); + } + + public function test_register_passes_the_provider_class_and_aliases_to_listeners(): void { + $received = []; + + add_action( + $this->registered_action(FirstProvider::class), + static function ($serviceProviderClass = null, $alias = null) use (&$received): void { + $received = ['provider' => $serviceProviderClass, 'alias' => $alias]; + }, + 10, + 2 + ); + + $this->adapter->register(FirstProvider::class, 'alpha', 'beta'); + + $this->assertSame(FirstProvider::class, $received['provider']); + $this->assertSame(['alpha', 'beta'], $received['alias']); + } + + public function test_register_passes_the_provider_class_and_aliases_to_alias_listeners(): void { + $received = []; + + add_action( + $this->registered_action('alpha'), + static function ($serviceProviderClass = null, $alias = null) use (&$received): void { + $received = ['provider' => $serviceProviderClass, 'alias' => $alias]; + }, + 10, + 2 + ); + + $this->adapter->register(FirstProvider::class, 'alpha', 'beta'); + + $this->assertSame(FirstProvider::class, $received['provider']); + $this->assertSame(['alpha', 'beta'], $received['alias']); + } + + public function test_register_on_action_registers_immediately_when_the_action_already_fired(): void { + $fired = $this->count_action($this->registered_action(FirstProvider::class)); + + do_action('foundation_boot_done'); + + $this->assertSame(0, $fired()); + + $this->adapter->registerOnAction('foundation_boot_done', FirstProvider::class); + + $this->assertSame(1, $fired()); + } + + public function test_register_on_action_defers_registration_until_the_action_fires(): void { + $fired = $this->count_action($this->registered_action(FirstProvider::class)); + + $this->adapter->registerOnAction('foundation_boot_pending', FirstProvider::class); + $this->assertSame(0, $fired()); + + do_action('foundation_boot_pending'); + $this->assertSame(1, $fired()); + } + + public function test_register_on_action_only_registers_once_even_if_the_action_fires_again(): void { + $fired = $this->count_action($this->registered_action(FirstProvider::class)); + + $this->adapter->registerOnAction('foundation_boot_repeat', FirstProvider::class); + + do_action('foundation_boot_repeat'); + + $this->assertSame(1, $fired()); + do_action('foundation_boot_repeat'); + + $this->assertSame(1, $fired()); + } + + public function test_register_after_provider_registers_the_dependant_once_the_base_registers(): void { + $base = $this->count_action($this->registered_action(FirstProvider::class)); + $dependant = $this->count_action($this->registered_action(SecondProvider::class)); + + $this->adapter->registerOnProvider(FirstProvider::class, SecondProvider::class); + $this->assertSame(0, $base()); + $this->assertSame(0, $dependant()); + + $this->adapter->register(FirstProvider::class); + + $this->assertSame(1, $base()); + $this->assertSame(1, $dependant()); + } + + public function test_register_after_all_actions_registers_immediately_when_all_actions_are_done(): void { + $fired = $this->count_action($this->registered_action(FirstProvider::class)); + + do_action('foundation_all_first'); + do_action('foundation_all_second'); + + $this->adapter->registerAfterAllActions( + ['foundation_all_first', 'foundation_all_second'], + FirstProvider::class + ); + + $this->assertSame(1, $fired()); + + do_action('foundation_all_first'); + do_action('foundation_all_second'); + + $this->assertSame(1, $fired()); + } + + public function test_register_after_all_actions_defers_until_the_pending_action_fires(): void { + $fired = $this->count_action($this->registered_action(FirstProvider::class)); + + $this->adapter->registerAfterAllActions(['foundation_all_pending'], FirstProvider::class); + $this->assertSame(0, $fired()); + + do_action('foundation_all_pending'); + $this->assertSame(1, $fired()); + } + + public function test_register_after_all_actions_waits_for_every_action_before_registering(): void { + $fired = $this->count_action($this->registered_action(FirstProvider::class)); + + $this->adapter->registerAfterAllActions( + ['foundation_multi_first', 'foundation_multi_second'], + FirstProvider::class + ); + $this->assertSame(0, $fired()); + + do_action('foundation_multi_first'); + $this->assertSame(0, $fired()); + + do_action('foundation_multi_second'); + $this->assertSame(1, $fired()); + + do_action('foundation_multi_first'); + $this->assertSame(1, $fired()); + + do_action('foundation_multi_second'); + $this->assertSame(1, $fired()); + } + + public function test_register_after_all_actions_registers_once_when_actions_fire_repeatedly(): void { + $fired = $this->count_action($this->registered_action(FirstProvider::class)); + + $this->adapter->registerAfterAllActions( + ['foundation_repeat_first', 'foundation_repeat_second'], + FirstProvider::class + ); + + do_action('foundation_repeat_first'); + do_action('foundation_repeat_second'); + $this->assertSame(1, $fired()); + + // Firing the same actions again must not register the provider a second time. + do_action('foundation_repeat_first'); + do_action('foundation_repeat_second'); + $this->assertSame(1, $fired()); + } +} From b97b473832d8a8a24d127892b19183ed736e9db0 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 22:16:38 +0300 Subject: [PATCH 28/35] tests: add unit coverage for the WP container --- .../ContainerAdapterTest.php | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/Unit/ContainerWordPress/ContainerAdapterTest.php diff --git a/tests/Unit/ContainerWordPress/ContainerAdapterTest.php b/tests/Unit/ContainerWordPress/ContainerAdapterTest.php new file mode 100644 index 0000000..b842d7a --- /dev/null +++ b/tests/Unit/ContainerWordPress/ContainerAdapterTest.php @@ -0,0 +1,60 @@ +assertInstanceOf(Container::class, $adapter); + } + + public function test_it_binds_and_resolves_through_the_wrapped_container(): void { + $adapter = new ContainerAdapter(new DI52Container()); + $adapter->bind(ContainerAdapterSample::class, static fn (): ContainerAdapterSample => new ContainerAdapterSample('value')); + + $this->assertSame('value', $adapter->get(ContainerAdapterSample::class)->value); + } + + public function test_it_returns_instance_builders_from_the_wrapped_container(): void { + $adapter = new ContainerAdapter(new DI52Container()); + $factory = $adapter->instance(ContainerAdapterSample::class, ['value']); + + $this->assertInstanceOf(ContainerAdapterSample::class, $factory()); + $this->assertSame('value', $factory()->value); + } + + public function test_it_returns_callbacks_from_the_wrapped_container(): void { + $adapter = new ContainerAdapter(new DI52Container()); + $callback = $adapter->callback(new ContainerAdapterSample('value'), 'read'); + + $this->assertSame('value', $callback()); + } + + public function test_it_exposes_contextual_bindings_fluently(): void { + $adapter = new ContainerAdapter(new DI52Container()); + + $this->assertSame($adapter, $adapter->when(ContainerAdapterSample::class)); + $this->assertSame($adapter, $adapter->needs('$value')); + } + + public function test_it_forwards_unknown_method_calls_to_the_wrapped_container(): void { + $adapter = new ContainerAdapter(new DI52Container()); + + $this->assertSame('fallback', $adapter->getVar('missing', 'fallback')); + } + + public function test_it_exposes_the_underlying_di52_container(): void { + $di52 = new DI52Container(); + $adapter = new ContainerAdapter($di52); + + $this->assertSame($di52, $adapter->getContainer()); + } +} From 5801531450c5bb71942d929b5f320e62659e66f8 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 22:25:29 +0300 Subject: [PATCH 29/35] tests: mergeArrayVar method coverage added --- tests/Unit/Container/ContainerAdapterTest.php | 30 +++++++++++ .../ContainerAdapterTest.php | 53 +++++++++++++++---- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/tests/Unit/Container/ContainerAdapterTest.php b/tests/Unit/Container/ContainerAdapterTest.php index bafaea2..6c6c27b 100644 --- a/tests/Unit/Container/ContainerAdapterTest.php +++ b/tests/Unit/Container/ContainerAdapterTest.php @@ -3,6 +3,7 @@ namespace StellarWP\Foundation\Tests\Unit\Container; use lucatume\DI52\Container as DI52Container; +use lucatume\DI52\ContainerException; use StellarWP\Foundation\Container\ContainerAdapter; use StellarWP\Foundation\Tests\Support\Fixtures\Container\ContainerAdapterSample; use StellarWP\Foundation\Tests\TestCase; @@ -29,4 +30,33 @@ public function test_it_forwards_unknown_method_calls_to_the_wrapped_container() $this->assertSame('fallback', $adapter->getVar('missing', 'fallback')); } + + public function test_it_merges_array_values_across_multiple_calls(): void { + $adapter = new ContainerAdapter(new DI52Container()); + + $adapter->mergeArrayVar('list', ['a']); + $adapter->mergeArrayVar('list', ['b', 'c']); + + $this->assertSame(['a', 'b', 'c'], $adapter->getVar('list')); + } + + public function test_it_merges_associative_array_values_without_replacing_previous_ones(): void { + $adapter = new ContainerAdapter(new DI52Container()); + + $adapter->mergeArrayVar('config', ['x' => 1]); + $adapter->mergeArrayVar('config', ['y' => 2]); + + $this->assertSame(['x' => 1, 'y' => 2], $adapter->getVar('config')); + } + + public function test_it_throws_when_merging_into_an_already_resolved_singleton(): void { + $adapter = new ContainerAdapter(new DI52Container()); + + $adapter->singleton('resolved', static fn (): array => ['first']); + $adapter->get('resolved'); + + $this->expectException(ContainerException::class); + + $adapter->mergeArrayVar('resolved', ['second']); + } } diff --git a/tests/Unit/ContainerWordPress/ContainerAdapterTest.php b/tests/Unit/ContainerWordPress/ContainerAdapterTest.php index b842d7a..dcfdaeb 100644 --- a/tests/Unit/ContainerWordPress/ContainerAdapterTest.php +++ b/tests/Unit/ContainerWordPress/ContainerAdapterTest.php @@ -3,6 +3,8 @@ namespace StellarWP\Foundation\Tests\Unit\ContainerWordPress; use lucatume\DI52\Container as DI52Container; +use lucatume\DI52\ContainerException; +use StellarWP\Foundation\Container\ContainerAdapter as FoundationContainerAdapter; use StellarWP\Foundation\ContainerWordPress\ContainerAdapter; use StellarWP\Foundation\ContainerWordPress\Contracts\Container; use StellarWP\Foundation\Tests\Support\Fixtures\Container\ContainerAdapterSample; @@ -10,21 +12,23 @@ final class ContainerAdapterTest extends TestCase { - public function test_it_implements_the_wordpress_container_contract(): void { - $adapter = new ContainerAdapter(new DI52Container()); + private function make_adapter(): ContainerAdapter { + return new ContainerAdapter(new FoundationContainerAdapter(new DI52Container())); + } - $this->assertInstanceOf(Container::class, $adapter); + public function test_it_implements_the_wordpress_container_contract(): void { + $this->assertInstanceOf(Container::class, $this->make_adapter()); } public function test_it_binds_and_resolves_through_the_wrapped_container(): void { - $adapter = new ContainerAdapter(new DI52Container()); + $adapter = $this->make_adapter(); $adapter->bind(ContainerAdapterSample::class, static fn (): ContainerAdapterSample => new ContainerAdapterSample('value')); $this->assertSame('value', $adapter->get(ContainerAdapterSample::class)->value); } public function test_it_returns_instance_builders_from_the_wrapped_container(): void { - $adapter = new ContainerAdapter(new DI52Container()); + $adapter = $this->make_adapter(); $factory = $adapter->instance(ContainerAdapterSample::class, ['value']); $this->assertInstanceOf(ContainerAdapterSample::class, $factory()); @@ -32,29 +36,56 @@ public function test_it_returns_instance_builders_from_the_wrapped_container(): } public function test_it_returns_callbacks_from_the_wrapped_container(): void { - $adapter = new ContainerAdapter(new DI52Container()); + $adapter = $this->make_adapter(); $callback = $adapter->callback(new ContainerAdapterSample('value'), 'read'); $this->assertSame('value', $callback()); } public function test_it_exposes_contextual_bindings_fluently(): void { - $adapter = new ContainerAdapter(new DI52Container()); + $adapter = $this->make_adapter(); $this->assertSame($adapter, $adapter->when(ContainerAdapterSample::class)); $this->assertSame($adapter, $adapter->needs('$value')); } public function test_it_forwards_unknown_method_calls_to_the_wrapped_container(): void { - $adapter = new ContainerAdapter(new DI52Container()); - - $this->assertSame('fallback', $adapter->getVar('missing', 'fallback')); + $this->assertSame('fallback', $this->make_adapter()->getVar('missing', 'fallback')); } public function test_it_exposes_the_underlying_di52_container(): void { $di52 = new DI52Container(); - $adapter = new ContainerAdapter($di52); + $adapter = new ContainerAdapter(new FoundationContainerAdapter($di52)); $this->assertSame($di52, $adapter->getContainer()); } + + public function test_it_merges_array_values_through_the_wrapped_container(): void { + $adapter = $this->make_adapter(); + + $adapter->mergeArrayVar('list', ['a']); + $adapter->mergeArrayVar('list', ['b', 'c']); + + $this->assertSame(['a', 'b', 'c'], $adapter->getVar('list')); + } + + public function test_it_merges_associative_array_values_without_replacing_previous_ones(): void { + $adapter = $this->make_adapter(); + + $adapter->mergeArrayVar('config', ['x' => 1]); + $adapter->mergeArrayVar('config', ['y' => 2]); + + $this->assertSame(['x' => 1, 'y' => 2], $adapter->getVar('config')); + } + + public function test_it_throws_when_merging_into_an_already_resolved_singleton(): void { + $adapter = $this->make_adapter(); + + $adapter->singleton('resolved', static fn (): array => ['first']); + $adapter->get('resolved'); + + $this->expectException(ContainerException::class); + + $adapter->mergeArrayVar('resolved', ['second']); + } } From b16295b46d5e1f6d4e3f4581c49544a14a060aa6 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 23:03:26 +0300 Subject: [PATCH 30/35] tests: fix counting issue --- tests/wpunit/ContainerWordPress/ContainerAdapterTest.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/wpunit/ContainerWordPress/ContainerAdapterTest.php b/tests/wpunit/ContainerWordPress/ContainerAdapterTest.php index fcf34f9..578195f 100644 --- a/tests/wpunit/ContainerWordPress/ContainerAdapterTest.php +++ b/tests/wpunit/ContainerWordPress/ContainerAdapterTest.php @@ -35,13 +35,9 @@ private function registered_action(string $identifier): string { * Count how many times a WordPress action fires while a callback is attached. */ private function count_action(string $action): callable { - $count = 0; + $original = did_action( $action ); - add_action($action, static function () use (&$count): void { - $count++; - }); - - return static fn (): int => $count; + return static fn (): int => did_action( $action ) - $original; } public function test_register_fires_a_registered_action_for_the_provider(): void { From f67bc1523d469288c69f62863e05e57c66dbc288 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 1 Jul 2026 23:05:31 +0300 Subject: [PATCH 31/35] chore: pint fixes --- tests/wpunit/ContainerWordPress/ContainerAdapterTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/wpunit/ContainerWordPress/ContainerAdapterTest.php b/tests/wpunit/ContainerWordPress/ContainerAdapterTest.php index 578195f..6bb3e85 100644 --- a/tests/wpunit/ContainerWordPress/ContainerAdapterTest.php +++ b/tests/wpunit/ContainerWordPress/ContainerAdapterTest.php @@ -3,10 +3,10 @@ namespace StellarWP\Foundation\Tests\WPUnit\ContainerWordPress; use lucatume\DI52\Container as DI52Container; -use StellarWP\Foundation\Container\Contracts\Container as FoundationContainer; -use StellarWP\Foundation\ContainerWordPress\Contracts\Container as WPContainerContract; use StellarWP\Foundation\Container\ContainerAdapter as FoundationContainerAdapter; +use StellarWP\Foundation\Container\Contracts\Container as FoundationContainer; use StellarWP\Foundation\ContainerWordPress\ContainerAdapter; +use StellarWP\Foundation\ContainerWordPress\Contracts\Container as WPContainerContract; use StellarWP\Foundation\Tests\Support\Fixtures\ContainerWordPress\FirstProvider; use StellarWP\Foundation\Tests\Support\Fixtures\ContainerWordPress\SecondProvider; use StellarWP\Foundation\Tests\WPUnitSupport\WPTestCase; @@ -35,9 +35,9 @@ private function registered_action(string $identifier): string { * Count how many times a WordPress action fires while a callback is attached. */ private function count_action(string $action): callable { - $original = did_action( $action ); + $original = did_action($action); - return static fn (): int => did_action( $action ) - $original; + return static fn (): int => did_action($action) - $original; } public function test_register_fires_a_registered_action_for_the_provider(): void { From b369a632066399cf89050a68ac61fc6138ebdcdf Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Wed, 1 Jul 2026 14:15:05 -0600 Subject: [PATCH 32/35] Fix README.md --- src/ContainerWordPress/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ContainerWordPress/README.md b/src/ContainerWordPress/README.md index e1075fe..fccf42d 100644 --- a/src/ContainerWordPress/README.md +++ b/src/ContainerWordPress/README.md @@ -14,7 +14,7 @@ composer require stellarwp/foundation-container-wordpress ## Usage -Create a new `ContainerAdapter`, passing in an instance of di52. It implements the +Create a new `ContainerAdapter`, passing in a Foundation container adapter. It implements the [`Contracts/Container.php`](./Contracts/Container.php) interface, which extends the base Foundation container contract: @@ -52,9 +52,9 @@ All of them accept the same optional trailing `...$alias` arguments as the base ## Hook Prefix -The WordPress container adapter will fire registration hooks when a Provider is being registered. By default, we use -the `'nexcess/foundation/container/wp/'` as the hook prefix, but you can easily change that by passing a second argument -during the adapter's initialization. +The WordPress container adapter will fire registration hooks when a Provider is being registered. By default, it uses +`'nexcess/foundation/container/wp/'` as the hook prefix, but you can change that by passing a second argument during the +adapter's initialization. The adapter normalizes the prefix to include one trailing slash. ```php $container = new ContainerAdapter(new FoundationContainerAdapter(new Container()), 'my/hook/prefix/'); @@ -68,7 +68,7 @@ add_action( 2 ); -# Or by using it's alias +// Or by using its alias. add_action( 'my/hook/prefix/my-alias/registered', function ( string $provider_class, array $aliases ): void { @@ -123,7 +123,7 @@ $container->registerOnAction( 'init', My_Provider::class ); ### `registerOnProvider()` Register a provider only after another provider has been registered. It builds on -the `.../registered` action above, so the dependant provider is wired up as soon +the `.../registered` action above, so the dependent provider is wired up as soon as the base provider is registered. ```php From a23eca6c496382e36a2ad9653a1cc7109d897cb9 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Wed, 1 Jul 2026 14:22:50 -0600 Subject: [PATCH 33/35] Ignore dead catches in tests --- phpstan.neon.dist | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 0d0bfe2..a1cb3a6 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -9,3 +9,7 @@ parameters: analyse: - src/*/vendor/* - tests/CodeceptionSupport/* + ignoreErrors: + - + identifier: catch.neverThrown + path: tests/* From ee85172eb624b7269b0b7aa838097a763d6c389f Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Wed, 1 Jul 2026 14:28:59 -0600 Subject: [PATCH 34/35] Fix README.md example --- src/ContainerWordPress/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ContainerWordPress/README.md b/src/ContainerWordPress/README.md index fccf42d..3d944d7 100644 --- a/src/ContainerWordPress/README.md +++ b/src/ContainerWordPress/README.md @@ -54,7 +54,7 @@ All of them accept the same optional trailing `...$alias` arguments as the base The WordPress container adapter will fire registration hooks when a Provider is being registered. By default, it uses `'nexcess/foundation/container/wp/'` as the hook prefix, but you can change that by passing a second argument during the -adapter's initialization. The adapter normalizes the prefix to include one trailing slash. +adapter's initialization. The adapter normalizes non-empty prefixes to include one trailing slash. ```php $container = new ContainerAdapter(new FoundationContainerAdapter(new Container()), 'my/hook/prefix/'); @@ -97,8 +97,6 @@ Both actions pass two arguments to listeners: the registered provider class (`string`) and the list of aliases (`string[]`). ```php -$container->register( My_Provider::class, 'my-alias' ); - add_action( 'nexcess/foundation/container/wp/' . My_Provider::class . '/registered', function ( string $provider_class, array $aliases ): void { @@ -107,6 +105,8 @@ add_action( 10, 2 ); + +$container->register( My_Provider::class, 'my-alias' ); ``` ### `registerOnAction()` From e71fe90564af3a25f0db29a008d8650611c91d83 Mon Sep 17 00:00:00 2001 From: Justin Frydman Date: Wed, 1 Jul 2026 14:29:47 -0600 Subject: [PATCH 35/35] normalize $prefix, do a more strict identifier check, validate actions before registering provider to ensure an empty provider does not get registered --- src/ContainerWordPress/ContainerAdapter.php | 18 +++++-- .../ContainerAdapterTest.php | 50 +++++++++++++++++-- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/ContainerWordPress/ContainerAdapter.php b/src/ContainerWordPress/ContainerAdapter.php index 835c34c..98c12c8 100644 --- a/src/ContainerWordPress/ContainerAdapter.php +++ b/src/ContainerWordPress/ContainerAdapter.php @@ -24,10 +24,16 @@ */ final readonly class ContainerAdapter implements Container { + private FoundationContainerAdapter $container; + + private string $prefix; + public function __construct( - private FoundationContainerAdapter $container, - private string $prefix = 'nexcess/foundation/container/wp/', + FoundationContainerAdapter $container, + string $prefix = 'nexcess/foundation/container/wp/', ) { + $this->container = $container; + $this->prefix = $prefix === '' ? '' : rtrim($prefix, '/') . '/'; } /** @@ -40,7 +46,7 @@ public function __construct( * @return non-empty-string */ private function registeredAction(string $identifier): string { - if (! $identifier) { + if ($identifier === '') { throw new InvalidArgumentException('You need to provide an identifier!'); } @@ -51,6 +57,12 @@ private function registeredAction(string $identifier): string { * {@inheritDoc} */ public function register(string $serviceProviderClass, ...$alias): void { + $this->registeredAction($serviceProviderClass); + + foreach ($alias as $slug) { + $this->registeredAction($slug); + } + $this->container->register($serviceProviderClass, ...$alias); /** diff --git a/tests/wpunit/ContainerWordPress/ContainerAdapterTest.php b/tests/wpunit/ContainerWordPress/ContainerAdapterTest.php index 6bb3e85..08c212e 100644 --- a/tests/wpunit/ContainerWordPress/ContainerAdapterTest.php +++ b/tests/wpunit/ContainerWordPress/ContainerAdapterTest.php @@ -2,6 +2,7 @@ namespace StellarWP\Foundation\Tests\WPUnit\ContainerWordPress; +use InvalidArgumentException; use lucatume\DI52\Container as DI52Container; use StellarWP\Foundation\Container\ContainerAdapter as FoundationContainerAdapter; use StellarWP\Foundation\Container\Contracts\Container as FoundationContainer; @@ -31,6 +32,15 @@ private function registered_action(string $identifier): string { return 'nexcess/foundation/container/wp/' . $identifier . '/registered'; } + private function adapter_with_prefix(string $prefix): ContainerAdapter { + $adapter = new ContainerAdapter(new FoundationContainerAdapter(new DI52Container()), $prefix); + + $adapter->bind(FoundationContainer::class, $adapter); + $adapter->bind(WPContainerContract::class, $adapter); + + return $adapter; + } + /** * Count how many times a WordPress action fires while a callback is attached. */ @@ -70,6 +80,38 @@ public function test_register_fires_a_registered_action_for_each_alias(): void { $this->assertSame(2, $beta()); } + public function test_register_normalizes_custom_prefixes_to_one_trailing_slash(): void { + foreach (['custom/foundation/container', 'custom/foundation/container/', 'custom/foundation/container///'] as $prefix) { + $adapter = $this->adapter_with_prefix($prefix); + $fired = $this->count_action('custom/foundation/container/' . FirstProvider::class . '/registered'); + + $adapter->register(FirstProvider::class); + + $this->assertSame(1, $fired()); + } + } + + public function test_register_allows_an_empty_custom_prefix(): void { + $adapter = $this->adapter_with_prefix(''); + $fired = $this->count_action(FirstProvider::class . '/registered'); + + $adapter->register(FirstProvider::class); + + $this->assertSame(1, $fired()); + } + + public function test_register_validates_aliases_before_registering_the_provider(): void { + $fired = $this->count_action($this->registered_action(FirstProvider::class)); + + try { + $this->adapter->register(FirstProvider::class, ''); + + $this->fail('Expected an invalid alias to throw before provider registration.'); + } catch (InvalidArgumentException) { + $this->assertSame(0, $fired()); + } + } + public function test_register_passes_the_provider_class_and_aliases_to_listeners(): void { $received = []; @@ -141,18 +183,18 @@ public function test_register_on_action_only_registers_once_even_if_the_action_f $this->assertSame(1, $fired()); } - public function test_register_after_provider_registers_the_dependant_once_the_base_registers(): void { + public function test_register_after_provider_registers_the_dependent_once_the_base_registers(): void { $base = $this->count_action($this->registered_action(FirstProvider::class)); - $dependant = $this->count_action($this->registered_action(SecondProvider::class)); + $dependent = $this->count_action($this->registered_action(SecondProvider::class)); $this->adapter->registerOnProvider(FirstProvider::class, SecondProvider::class); $this->assertSame(0, $base()); - $this->assertSame(0, $dependant()); + $this->assertSame(0, $dependent()); $this->adapter->register(FirstProvider::class); $this->assertSame(1, $base()); - $this->assertSame(1, $dependant()); + $this->assertSame(1, $dependent()); } public function test_register_after_all_actions_registers_immediately_when_all_actions_are_done(): void {