From 3322f7af3165dbf7f01e7c67b4444d955c15eff4 Mon Sep 17 00:00:00 2001 From: Nikolay Strikhar Date: Mon, 24 Aug 2026 15:13:19 +0200 Subject: [PATCH] Hand a host the policy list its own settings control has to offer --- docs/configuration.md | 5 ++++ docs/recipes.md | 25 ++++++++++++++++++++ src/Conflict_Policy.php | 38 +++++++++++++++++++------------ tests/unit/ConflictPolicyTest.php | 25 ++++++++++++++++++++ 4 files changed, 78 insertions(+), 15 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 13ac09b..406ae76 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -169,6 +169,11 @@ and all plausible values, so wherever a string *is* accepted it is the value its 'conflict_policy' => static fn( Sub_Plugin $sub_plugin ) => give_conflict_policy_for( $sub_plugin ), ``` +`Conflict_Policy::all()` is every policy string, in a stable order, for a settings control that has +to offer them — see +[ship the absorption over several releases](recipes.md#ship-the-absorption-over-several-releases). +It carries no labels: how each policy is described to a site owner is yours to word. + `standalone_plugin_basename` takes a string only: it names a file already on disk. `dependency_check` and `activation_callback` have nothing a string could collide with, so they accept every callable form, a plain function name included. diff --git a/docs/recipes.md b/docs/recipes.md index 93a5566..0e7a21d 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -128,6 +128,31 @@ if the policy comes from a callable reading a value you can move without shippin An unrecognised value is treated as `NOTICE_ONLY`, never as consent to deactivate, so a stale or misspelt option cannot turn a plugin off. +The control that writes that option asks `Conflict_Policy::all()` for its choices rather than +listing the constants by hand, so a policy added in a later release reaches your screen without you +editing it. It returns the policy strings and nothing else: what a site owner reads is your wording, +in your textdomain, and a label shipped from here would be neither. + +```php +$labels = [ + Conflict_Policy::DEFER => __( 'Bundled, dormant on sites running the add-on', 'give' ), + Conflict_Policy::NOTICE_ONLY => __( 'Ask those sites to deactivate it', 'give' ), + Conflict_Policy::DEACTIVATE => __( 'Merge them in', 'give' ), +]; + +$stage = get_option( 'give_absorption_stage', Conflict_Policy::DEFER ); + +foreach ( Conflict_Policy::all() as $policy ) { + printf( + '', + esc_attr( $policy ), + selected( $policy, $stage, false ), + // A policy you have not written a label for yet still appears, under its own name. + esc_html( $labels[ $policy ] ?? $policy ) + ); +} +``` + ## Defer to a standalone that is a new codebase This is an edge case, but one that has occurred before: a later version of a standalone that shares diff --git a/src/Conflict_Policy.php b/src/Conflict_Policy.php index 0b44339..c42782a 100644 --- a/src/Conflict_Policy.php +++ b/src/Conflict_Policy.php @@ -41,6 +41,29 @@ final class Conflict_Policy { */ public const NOTICE_ONLY = 'notice_only'; + /** + * Every policy this library understands, in declaration order. + * + * A host staging an absorption puts the policy behind a setting, and the control that writes + * it has to list the policies from somewhere: this, rather than three constants written out + * by hand, so a policy added later reaches that control without the host editing it. + * + * Bare strings, and no labels beside them. What a site owner should be told a policy does is + * the host's wording in the host's textdomain, and a label produced here would be neither — + * which is also why the order is only stable, not meaningful: a screen orders its own options. + * + * @since 1.0.0 + * + * @return string[] + */ + public static function all(): array { + return [ + self::DEACTIVATE, + self::DEFER, + self::NOTICE_ONLY, + ]; + } + /** * The policy that applies when a sub-plugin configures none. * @@ -77,19 +100,4 @@ public static function default(): string { public static function is_valid( string $policy ): bool { return in_array( $policy, self::all(), true ); } - - /** - * Every policy this library understands. - * - * @since 1.0.0 - * - * @return string[] - */ - private static function all(): array { - return [ - self::DEACTIVATE, - self::DEFER, - self::NOTICE_ONLY, - ]; - } } diff --git a/tests/unit/ConflictPolicyTest.php b/tests/unit/ConflictPolicyTest.php index 4d87e6c..2c1d919 100644 --- a/tests/unit/ConflictPolicyTest.php +++ b/tests/unit/ConflictPolicyTest.php @@ -39,6 +39,31 @@ public function test_no_policy_is_added_or_removed_unnoticed(): void { ); } + /** + * The list a host builds its own settings control from. Order is pinned because a control + * rendering the list as it is handed would otherwise shuffle its options between releases. + */ + public function test_it_lists_every_policy(): void { + $this->assertSame( + [ + Conflict_Policy::DEACTIVATE, + Conflict_Policy::DEFER, + Conflict_Policy::NOTICE_ONLY, + ], + Conflict_Policy::all() + ); + } + + /** + * Cross-checked against the constants, so a fourth policy declared without being added to + * the list fails here rather than going silently missing from every control built on it. + */ + public function test_the_list_holds_every_policy_constant(): void { + $constants = ( new ReflectionClass( Conflict_Policy::class ) )->getConstants(); + + $this->assertSame( array_values( $constants ), Conflict_Policy::all() ); + } + public function test_the_default_is_to_deactivate(): void { $this->assertSame( Conflict_Policy::DEACTIVATE, Conflict_Policy::default() ); }