Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ seams a host may rebind:
| `Registry\Contracts\Registrar_Interface` | `Registry\Registrar` | holds registered `Sub_Plugin` objects |
| `Notices\Contracts\Writer_Interface` | `Notices\Writer` | what each notice says |
| `Conflict\Contracts\Resolver_Interface` | `Conflict\Resolver` | one method: which policy branch a conflict takes |
| `Contracts\Plugin_Deactivator_Interface` | `Plugin_Deactivator` | deactivates the standalone, network-aware |
| `Contracts\Plugin_Checker_Interface` | `Plugin_Checker` | answers whether a plugin is active |
| `Plugin\Contracts\Deactivator_Interface` | `Plugin\Deactivator` | deactivates the standalone, network-aware |
| `Plugin\Contracts\Checker_Interface` | `Plugin\Checker` | answers whether a plugin is active |
| `Contracts\Activator_Interface` | `Activator` | run-once activation-callback tracking |

The rest — `Boot\Scheduler`, `Loader`, `Registry\Reader`, `Conflict\Detector`, `Conflict\Gatekeeper`,
Expand All @@ -94,9 +94,9 @@ under `ContainerInterface::class`, first and before anything else, so that a con
unbound classes reflectively can still satisfy the collaborators that take one.

An interface belonging to a folder-scoped concern lives in that folder's `Contracts\`, not beside its
implementation and not in the top-level `src/Contracts/`. `src/Contracts/` is for the interfaces whose
implementations sit at the root — `Plugin_Deactivator`, `Plugin_Checker`, `Activator` — plus
`Provider_Interface`.
implementation and not in the top-level `src/Contracts/`. What is left in `src/Contracts/` is the
interfaces whose implementations sit at the root — `Activator_Interface` — plus `Provider_Interface`,
which belongs to no folder because `Provider` is the file that names every folder.

**`Notices\Writer` and `Notices\Presenter` split because they change for different reasons.** One
answers "what does this notice say", the other "who may see the pending set, and is it gone once they
Expand Down Expand Up @@ -175,7 +175,7 @@ and a pass is complete the moment it is built.
container-bound collaborator** (`is_enabled()`, `is_already_loaded()`, `has_standalone_plugin()`,
`get_conflict_policy()`, …). Note that this is not the same as "config alone": `is_already_loaded()`
reads the global constant table and `is_enabled()` may invoke a host callable that queries anything
it likes. The line is about *dependency direction* — anything needing `Plugin_Checker_Interface` or
it likes. The line is about *dependency direction* — anything needing `Plugin\Contracts\Checker_Interface` or
the notice queue would drag a container resolution into `Absorber::register()`, which deliberately
resolves nothing so the container can arrive at any point before boot. So `Sub_Plugin` only *names*
the plugin to ask about, and the collaborator does the asking.
Expand All @@ -194,13 +194,13 @@ that drives the whole of it against a real WordPress is `tests/unit/Scenario/`.
| `src/Loader.php` | The load pass: the gate chain, the `require_once`, the activation callback. |
| `src/Sub_Plugin.php` | Value object; validates config and answers what it can without a container-bound collaborator. |
| `src/Conflict_Policy.php` | The three policy constants, `default()`, `is_valid()`. |
| `src/Plugin_Deactivator.php`, `src/Plugin_Checker.php` | The only files that touch WordPress plugin functions, through `Traits\Loads_Plugin_Functions`. |
| `src/Plugin/` | `Deactivator` (turns the standalone off), `Checker` (answers whether a plugin is active), `Loads_Plugin_Functions` (pulls in `wp-admin/includes/plugin.php`), `Contracts\Deactivator_Interface`, `Contracts\Checker_Interface`. The only files that touch WordPress plugin functions. |
| `src/Registry/` | `Registrar` (holds registered `Sub_Plugin` objects), `Reader` (the registration buffer, drained into the registrar on the way past; the object every pass reads the registry through), `Contracts\Registrar_Interface`. |
| `src/Activator.php` | Runs a sub-plugin's activation callback once ever, recorded in one option. |
| `src/Conflict/` | `Detector` (whether a standalone is in the way), `Resolver` (which policy branch to take), `Gatekeeper` (which requests, and which users, may have one resolved), `Redirector` (where the user lands afterwards), `Rewriter` (rewrites the activation-error screen for a registered standalone), `Contracts\Resolver_Interface`. |
| `src/Traits/` | `Loads_Plugin_Functions` (pulls in `wp-admin/includes/plugin.php`), `Guards_Hook_Prefix` (a missing prefix warns and stands down rather than throwing). |
| `src/Traits/` | `Guards_Hook_Prefix` (a missing prefix warns and stands down rather than throwing). Cross-cutting only: a trait used by one folder lives in that folder. |
| `src/Notices/` | `Writer` (what a notice says, stored under `slug:type`), `Presenter` (who may consume it, render-then-clear), `Store` (keeps it), `Renderer` (draws it), `Contracts\Writer_Interface`. |
| `src/Contracts/`, `src/Exceptions/` | `Provider_Interface`, `Plugin_Deactivator_Interface`, `Plugin_Checker_Interface`, `Activator_Interface`, `Config_Exception`. |
| `src/Contracts/`, `src/Exceptions/` | `Provider_Interface`, `Activator_Interface`, `Config_Exception`. |

### Boot lifecycle

Expand Down Expand Up @@ -291,15 +291,15 @@ the screens the mistaken registration would have to be corrected from.

The container is no longer the other half of that. A pass is handed a reader that already holds its
registrar, so a container that cannot supply one fails while the *pass* is being built — where an
unbuildable `Writer_Interface` or `Plugin_Checker_Interface` has always failed. Read-time and
unbuildable `Writer_Interface` or `Plugin\Contracts\Checker_Interface` has always failed. Read-time and
build-time failures stopped being the same event when the registry became an argument, and the
registrar now fails like every other binding rather than being the one collaborator whose broken
binding surfaced late and politely.

`Conflict\Resolver` switches on the policy: `DEFER` no-ops, `NOTICE_ONLY` queues a notice, and
`DEACTIVATE` (the default) deactivates network-aware, queues a merge notice, and redirects. It is
the worked example of required injection — `Conflict\Detector` to say which sub-plugins are in
conflict, `Plugin_Deactivator_Interface` to turn the standalone off, `Writer_Interface` for the notice
conflict, `Plugin\Contracts\Deactivator_Interface` to turn the standalone off, `Writer_Interface` for the notice
and `Conflict\Redirector` for the destination, all four constructor arguments with no default — so
the object a test builds is the object the provider builds, and a host's rebinding of either plugin
seam reaches it, the deactivator directly and the checker through the detector, without the resolver
Expand Down Expand Up @@ -549,7 +549,7 @@ against real WordPress state. `Bootstrap_Test_Case.php` is the abstract parent o
- **`deactivate_plugins()` is called silent, with no `$network_wide` argument.** Silent because a
`flush_rewrite_rules()` in the standalone's deactivation hook at `plugins_loaded` 404s the site.
The `null` default takes both the network and blog branches; a computed `true` strands an entry.
- **`Traits\Loads_Plugin_Functions` guards on `deactivate_plugins()`**, not `is_plugin_active()` —
- **`Plugin\Loads_Plugin_Functions` guards on `deactivate_plugins()`**, not `is_plugin_active()` —
the latter is a common third-party shim.
- **Strauss must not rewrite `plugin_loaded_constant` values.** They are shared runtime constants;
prefixing them defeats the entire mechanism.
Expand Down
6 changes: 3 additions & 3 deletions docs/extending.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ $container->singleton( Registrar_Interface::class, My_Registrar::class );
|---|---|---|
| `Registry\Contracts\Registrar_Interface` | `Registry\Registrar` | Holds the registered sub-plugins. |
| `Notices\Contracts\Writer_Interface` | `Notices\Writer` | Words the admin notices. |
| `Contracts\Plugin_Deactivator_Interface` | `Plugin_Deactivator` | Deactivates the standalone. |
| `Contracts\Plugin_Checker_Interface` | `Plugin_Checker` | Answers whether a plugin is active. |
| `Plugin\Contracts\Deactivator_Interface` | `Plugin\Deactivator` | Deactivates the standalone. |
| `Plugin\Contracts\Checker_Interface` | `Plugin\Checker` | Answers whether a plugin is active. |
| `Conflict\Contracts\Resolver_Interface` | `Conflict\Resolver` | Applies the policy to a conflict. |
| `Contracts\Activator_Interface` | `Activator` | Runs a sub-plugin's activation callback once, ever. |

**Rebind `Plugin_Checker_Interface` when your plugin filters `option_active_plugins` or
**Rebind `Plugin\Contracts\Checker_Interface` when your plugin filters `option_active_plugins` or
`site_option_active_sitewide_plugins`** — LearnDash injects and then strips a synthetic path — because
`is_plugin_active()` then does not report what is in the database.

Expand Down
10 changes: 5 additions & 5 deletions src/Conflict/Detector.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

namespace Nexcess\PluginAbsorber\Conflict;

use Nexcess\PluginAbsorber\Contracts\Plugin_Checker_Interface;
use Nexcess\PluginAbsorber\Exceptions\Config_Exception;
use Nexcess\PluginAbsorber\Plugin\Contracts\Checker_Interface;
use Nexcess\PluginAbsorber\Registry\Reader;
use Nexcess\PluginAbsorber\Sub_Plugin;

Expand Down Expand Up @@ -41,17 +41,17 @@ class Detector {
/**
* @since 1.0.0
*
* @var Plugin_Checker_Interface
* @var Checker_Interface
*/
private $plugin_checker;

/**
* @since 1.0.0
*
* @param Reader $registry Which sub-plugins are registered.
* @param Plugin_Checker_Interface $plugin_checker Whether the standalone is active.
* @param Reader $registry Which sub-plugins are registered.
* @param Checker_Interface $plugin_checker Whether the standalone is active.
*/
public function __construct( Reader $registry, Plugin_Checker_Interface $plugin_checker ) {
public function __construct( Reader $registry, Checker_Interface $plugin_checker ) {
$this->registry = $registry;
$this->plugin_checker = $plugin_checker;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Conflict/Gatekeeper.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public function request_may_resolve(): bool {
* the standalone off site-wide by requesting a page they are about to be bounced off.
*
* The capability asked for matches what resolution can do, which is why the two differ. The
* deactivation is network-wide: Plugin_Deactivator leaves deactivate_plugins()'s $network_wide
* deactivation is network-wide: Deactivator leaves deactivate_plugins()'s $network_wide
* at its default, and core reads that as both scopes, so the standalone comes out of the
* network's active plugins whichever site the request arrived on. That is authority a single
* site's administrator does not hold, and asking for activate_plugins would not establish it --
Expand Down
18 changes: 9 additions & 9 deletions src/Conflict/Resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@

use Nexcess\PluginAbsorber\Conflict\Contracts\Resolver_Interface;
use Nexcess\PluginAbsorber\Conflict_Policy;
use Nexcess\PluginAbsorber\Contracts\Plugin_Deactivator_Interface;
use Nexcess\PluginAbsorber\Exceptions\Config_Exception;
use Nexcess\PluginAbsorber\Notices\Contracts\Writer_Interface;
use Nexcess\PluginAbsorber\Plugin\Contracts\Deactivator_Interface;
use Nexcess\PluginAbsorber\Registry\Reader;
use Nexcess\PluginAbsorber\Sub_Plugin;
use Nexcess\PluginAbsorber\Traits\Guards_Hook_Prefix;
Expand All @@ -24,7 +24,7 @@
* complete the moment it exists and a test can hand it doubles instead of standing up global state.
* Finding the conflict and turning the standalone off arrive separately because they are separate
* jobs: `Detector` answers whether a standalone is in the way, and a host that wants deactivation to
* be a no-op rebinds `Plugin_Deactivator_Interface` without touching how detection works.
* be a no-op rebinds `Plugin\Contracts\Deactivator_Interface` without touching how detection works.
*
* Neither of the two questions asked ahead of this class is asked here. `Detector` reports that
* there is something to resolve and `Gatekeeper` decides who may have it resolved, and the conflict
Expand Down Expand Up @@ -55,7 +55,7 @@ class Resolver implements Resolver_Interface {
/**
* @since 1.0.0
*
* @var Plugin_Deactivator_Interface
* @var Deactivator_Interface
*/
private $plugin_deactivator;

Expand All @@ -76,16 +76,16 @@ class Resolver implements Resolver_Interface {
/**
* @since 1.0.0
*
* @param Reader $registry Which sub-plugins are registered.
* @param Detector $detector Whether a sub-plugin is in conflict.
* @param Plugin_Deactivator_Interface $plugin_deactivator Turns the standalone off.
* @param Writer_Interface $notices Where the user is told what happened.
* @param Redirector $redirector Where the user lands afterwards.
* @param Reader $registry Which sub-plugins are registered.
* @param Detector $detector Whether a sub-plugin is in conflict.
* @param Deactivator_Interface $plugin_deactivator Turns the standalone off.
* @param Writer_Interface $notices Where the user is told what happened.
* @param Redirector $redirector Where the user lands afterwards.
*/
public function __construct(
Reader $registry,
Detector $detector,
Plugin_Deactivator_Interface $plugin_deactivator,
Deactivator_Interface $plugin_deactivator,
Writer_Interface $notices,
Redirector $redirector
) {
Expand Down
7 changes: 3 additions & 4 deletions src/Plugin_Checker.php → src/Plugin/Checker.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@
* @package Nexcess\PluginAbsorber
*/

namespace Nexcess\PluginAbsorber;
namespace Nexcess\PluginAbsorber\Plugin;

use Nexcess\PluginAbsorber\Contracts\Plugin_Checker_Interface;
use Nexcess\PluginAbsorber\Traits\Loads_Plugin_Functions;
use Nexcess\PluginAbsorber\Plugin\Contracts\Checker_Interface;

/**
* Plugin state, straight from WordPress.
*
* @since 1.0.0
*/
class Plugin_Checker implements Plugin_Checker_Interface {
class Checker implements Checker_Interface {
use Loads_Plugin_Functions;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @package Nexcess\PluginAbsorber
*/

namespace Nexcess\PluginAbsorber\Contracts;
namespace Nexcess\PluginAbsorber\Plugin\Contracts;

/**
* The library's one way of asking WordPress about a plugin.
Expand All @@ -12,15 +12,15 @@
* only identifier WordPress itself accepts. Bind a replacement to answer from somewhere other than
* the active-plugins option.
*
* Separate from `Plugin_Deactivator_Interface` because the two are asked for by different code for
* Separate from `Plugin\Contracts\Deactivator_Interface` because the two are asked for by different code for
* different reasons: reading plugin state is a question anything may ask, while turning a plugin
* off is an action exactly one policy branch takes. A host that wants deactivation to be a no-op —
* plugin state managed outside WordPress, say — should not have to reimplement the reading half
* to say so.
*
* @since 1.0.0
*/
interface Plugin_Checker_Interface {
interface Checker_Interface {
/**
* Whether the plugin is active, in either scope.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @package Nexcess\PluginAbsorber
*/

namespace Nexcess\PluginAbsorber\Contracts;
namespace Nexcess\PluginAbsorber\Plugin\Contracts;

/**
* The library's one way of turning a plugin off.
Expand All @@ -14,7 +14,7 @@
*
* @since 1.0.0
*/
interface Plugin_Deactivator_Interface {
interface Deactivator_Interface {
/**
* Deactivate the plugin in every scope it is active in.
*
Expand Down
7 changes: 3 additions & 4 deletions src/Plugin_Deactivator.php → src/Plugin/Deactivator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@
* @package Nexcess\PluginAbsorber
*/

namespace Nexcess\PluginAbsorber;
namespace Nexcess\PluginAbsorber\Plugin;

use Nexcess\PluginAbsorber\Contracts\Plugin_Deactivator_Interface;
use Nexcess\PluginAbsorber\Traits\Loads_Plugin_Functions;
use Nexcess\PluginAbsorber\Plugin\Contracts\Deactivator_Interface;

/**
* Turns a plugin off, the way WordPress's own unattended paths do.
*
* @since 1.0.0
*/
class Plugin_Deactivator implements Plugin_Deactivator_Interface {
class Deactivator implements Deactivator_Interface {
use Loads_Plugin_Functions;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @package Nexcess\PluginAbsorber
*/

namespace Nexcess\PluginAbsorber\Traits;
namespace Nexcess\PluginAbsorber\Plugin;

/**
* Makes WordPress's plugin functions available to the class using it.
Expand Down
14 changes: 8 additions & 6 deletions src/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@
use Nexcess\PluginAbsorber\Conflict\Resolver;
use Nexcess\PluginAbsorber\Conflict\Rewriter;
use Nexcess\PluginAbsorber\Contracts\Activator_Interface;
use Nexcess\PluginAbsorber\Contracts\Plugin_Checker_Interface;
use Nexcess\PluginAbsorber\Contracts\Plugin_Deactivator_Interface;
use Nexcess\PluginAbsorber\Contracts\Provider_Interface;
use Nexcess\PluginAbsorber\Loader;
use Nexcess\PluginAbsorber\Notices\Contracts\Writer_Interface;
use Nexcess\PluginAbsorber\Notices\Presenter;
use Nexcess\PluginAbsorber\Notices\Renderer;
use Nexcess\PluginAbsorber\Notices\Store;
use Nexcess\PluginAbsorber\Notices\Writer;
use Nexcess\PluginAbsorber\Plugin\Checker;
use Nexcess\PluginAbsorber\Plugin\Contracts\Checker_Interface;
use Nexcess\PluginAbsorber\Plugin\Contracts\Deactivator_Interface;
use Nexcess\PluginAbsorber\Plugin\Deactivator;
use Nexcess\PluginAbsorber\Registry\Contracts\Registrar_Interface;
use Nexcess\PluginAbsorber\Registry\Reader;
use Nexcess\PluginAbsorber\Registry\Registrar;
Expand Down Expand Up @@ -70,8 +72,8 @@ public function register(): void {
$this->bind_once( ContainerInterface::class, $container );

$this->bind_once( Registrar_Interface::class, Registrar::class );
$this->bind_once( Plugin_Checker_Interface::class, Plugin_Checker::class );
$this->bind_once( Plugin_Deactivator_Interface::class, Plugin_Deactivator::class );
$this->bind_once( Checker_Interface::class, Checker::class );
$this->bind_once( Deactivator_Interface::class, Deactivator::class );
$this->bind_once( Activator_Interface::class, Activator::class );
$this->bind_once( Store::class );
$this->bind_once( Renderer::class );
Expand Down Expand Up @@ -114,7 +116,7 @@ static function () use ( $container ): Rewriter {
static function () use ( $container ): Detector {
return new Detector(
$container->get( Reader::class ),
$container->get( Plugin_Checker_Interface::class )
$container->get( Checker_Interface::class )
);
}
);
Expand All @@ -125,7 +127,7 @@ static function () use ( $container ): Resolver {
return new Resolver(
$container->get( Reader::class ),
$container->get( Detector::class ),
$container->get( Plugin_Deactivator_Interface::class ),
$container->get( Deactivator_Interface::class ),
$container->get( Writer_Interface::class ),
$container->get( Redirector::class )
);
Expand Down
2 changes: 1 addition & 1 deletion src/Sub_Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*
* Deliberately not a window onto WordPress. Asking whether the standalone counterpart is active is
* a question about the site rather than about this configuration, and it belongs to
* Plugin_Checker_Interface; this object only names the plugin to ask about.
* Checker_Interface; this object only names the plugin to ask about.
*
* @since 1.0.0
*
Expand Down
8 changes: 4 additions & 4 deletions tests/unit/Boot/SchedulerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
use Nexcess\PluginAbsorber\Conflict\Gatekeeper;
use Nexcess\PluginAbsorber\Conflict\Rewriter;
use Nexcess\PluginAbsorber\Conflict_Policy;
use Nexcess\PluginAbsorber\Contracts\Plugin_Checker_Interface;
use Nexcess\PluginAbsorber\Loader;
use Nexcess\PluginAbsorber\Notices\Presenter;
use Nexcess\PluginAbsorber\Plugin\Contracts\Checker_Interface;
use Nexcess\PluginAbsorber\Tests\Support\Absorber_State;
use Nexcess\PluginAbsorber\Tests\Support\Config_State;
use Nexcess\PluginAbsorber\Tests\Support\Spy_Presenter;
Expand Down Expand Up @@ -1076,9 +1076,9 @@ public function has_conflict(): bool {
private function bind_active_standalone(): void {
$container = new Test_Container();
$container->singleton(
Plugin_Checker_Interface::class,
static function (): Plugin_Checker_Interface {
return new class() implements Plugin_Checker_Interface {
Checker_Interface::class,
static function (): Checker_Interface {
return new class() implements Checker_Interface {
/**
* @param string $basename Plugin basename.
*
Expand Down
Loading
Loading