From f30ec9d1f937f6485946de53afe15c6db46bd277 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sun, 21 Jun 2026 16:37:17 +0200 Subject: [PATCH 01/21] refactor(framework): relocate WC-order field store out of settings; add ObjectField module; group WC SettingsPage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-Phase-4 hardening. settings shipped WordPressObjectFieldStore — a WooCommerce-order implementation (OrderUtil, shop_order, wc_get_order, woocommerce_process_shop_order_meta) — inside a package documented and deptrac-scoped as WooCommerce-free. deptrac could not catch it: WC symbols are external, so no framework-layer collector sees them, and the package would fatal on load when scoped without WooCommerce. Move the implementation to woocommerce as OrderData/OrderFieldStore (a rename that also corrects the misleading "WordPress" name on an order store), and keep only the generic, WP/WC-agnostic contract in settings, now under an ObjectField/ concept module: settings/src/ObjectField/{ObjectFieldStoreInterface, ValueObjects/ ObjectMetaBox, Exceptions/InvalidObjectMetaBoxException}. settings drops its WooCommerce composer "suggest" and php-scoper stub, so it is WooCommerce-free in code and tooling. The contract stays a single- implementation interface by design (the impl must live in woocommerce); a docblock note records why it must not be removed on an impl count. Also group the WC settings-page trio (WooCommerceSettingsBackend, WCSettingsBuilder, DescriptorBackedWCSettingsPage) into a SettingsPage/ concept folder, so woocommerce/src reads as peer concept folders (SettingsPage/, ProductData/, OrderData/). deptrac unchanged and 0 violations; the existing ruleset already encodes the correct architecture. PHPStan + Unit (290) green for the changed packages; integration suite to be run under wp-env. Assisted-by: Claude Code:claude-opus-4-8 --- packages/settings/composer.json | 7 +-- .../InvalidObjectMetaBoxException.php | 2 +- .../src/ObjectField/Exceptions/index.php | 1 + .../ObjectFieldStoreInterface.php | 9 +++- .../ValueObjects/ObjectMetaBox.php | 4 +- .../src/ObjectField/ValueObjects/index.php | 1 + packages/settings/src/ObjectField/index.php | 1 + .../settings/tests/Unit/ObjectMetaBoxTest.php | 4 +- .../src/OrderData/OrderFieldStore.php} | 16 ++++-- packages/woocommerce/src/OrderData/index.php | 1 + .../DescriptorBackedWCSettingsPage.php | 2 +- .../{ => SettingsPage}/WCSettingsBuilder.php | 2 +- .../WooCommerceSettingsBackend.php | 2 +- .../woocommerce/src/SettingsPage/index.php | 1 + .../DescriptorBackedWCSettingsPageTest.php | 2 +- .../Fixtures/BarWCSettingsPage.php | 2 +- .../Fixtures/FooWCSettingsPage.php | 2 +- .../Fixtures/LazyBindWCSettingsPage.php | 2 +- .../Fixtures/UnboundWCSettingsPage.php | 2 +- .../OrderData/OrderFieldStoreTest.php} | 52 +++++++++---------- .../tests/Integration/OrderData/index.php | 1 + .../WooCommerceSettingsBackendTest.php | 2 +- .../tests/Unit/WCSettingsBuilderTest.php | 2 +- phpstan.dist.neon | 4 +- tests/Fixtures/consumer-smoke/smoke.php | 8 +-- 25 files changed, 72 insertions(+), 60 deletions(-) rename packages/settings/src/{ => ObjectField}/Exceptions/InvalidObjectMetaBoxException.php (86%) create mode 100644 packages/settings/src/ObjectField/Exceptions/index.php rename packages/settings/src/{ => ObjectField}/ObjectFieldStoreInterface.php (81%) rename packages/settings/src/{ => ObjectField}/ValueObjects/ObjectMetaBox.php (94%) create mode 100644 packages/settings/src/ObjectField/ValueObjects/index.php create mode 100644 packages/settings/src/ObjectField/index.php rename packages/{settings/src/WordPressObjectFieldStore.php => woocommerce/src/OrderData/OrderFieldStore.php} (94%) create mode 100644 packages/woocommerce/src/OrderData/index.php rename packages/woocommerce/src/{ => SettingsPage}/DescriptorBackedWCSettingsPage.php (98%) rename packages/woocommerce/src/{ => SettingsPage}/WCSettingsBuilder.php (99%) rename packages/woocommerce/src/{ => SettingsPage}/WooCommerceSettingsBackend.php (99%) create mode 100644 packages/woocommerce/src/SettingsPage/index.php rename packages/{settings/tests/Integration/WordPressObjectFieldStoreTest.php => woocommerce/tests/Integration/OrderData/OrderFieldStoreTest.php} (92%) create mode 100644 packages/woocommerce/tests/Integration/OrderData/index.php diff --git a/packages/settings/composer.json b/packages/settings/composer.json index 3a2258f..af56ca0 100644 --- a/packages/settings/composer.json +++ b/packages/settings/composer.json @@ -23,10 +23,6 @@ "psr/log": "^3" }, - "suggest": { - "wp-plugin/woocommerce": "Required at runtime by WordPressObjectFieldStore, which stores fields as WooCommerce order meta." - }, - "minimum-stability": "dev", "prefer-stable": true, @@ -49,8 +45,7 @@ "dev-trunk": "2.0.x-dev" }, "scoping-stubs": [ - "php-stubs/wordpress-stubs", - "php-stubs/woocommerce-stubs" + "php-stubs/wordpress-stubs" ], "changelogger": { "versioning": "semver", diff --git a/packages/settings/src/Exceptions/InvalidObjectMetaBoxException.php b/packages/settings/src/ObjectField/Exceptions/InvalidObjectMetaBoxException.php similarity index 86% rename from packages/settings/src/Exceptions/InvalidObjectMetaBoxException.php rename to packages/settings/src/ObjectField/Exceptions/InvalidObjectMetaBoxException.php index 705dea3..007f649 100644 --- a/packages/settings/src/Exceptions/InvalidObjectMetaBoxException.php +++ b/packages/settings/src/ObjectField/Exceptions/InvalidObjectMetaBoxException.php @@ -1,6 +1,6 @@ set( $this->order_id, '_dws_unlocked', 'yes' ); @@ -105,13 +105,13 @@ public function test_set_and_get_round_trip_on_an_order(): void { } public function test_get_returns_the_default_when_nothing_is_stored(): void { - $store = new WordPressObjectFieldStore(); + $store = new OrderFieldStore(); self::assertSame( 'fallback', $store->get( $this->order_id, '_dws_absent', 'fallback' ) ); } public function test_has_reports_presence_and_delete_removes_the_value(): void { - $store = new WordPressObjectFieldStore(); + $store = new OrderFieldStore(); self::assertFalse( $store->has( $this->order_id, '_dws_flag' ) ); @@ -127,7 +127,7 @@ public function test_crud_falls_back_to_post_meta_for_a_non_order_id(): void { $post_id = \wp_insert_post( array( 'post_title' => 'Probe', 'post_status' => 'publish' ) ); \assert( \is_int( $post_id ) ); self::assertFalse( \wc_get_order( $post_id ) ); // guarantee the non-order fallback branch, not an id collision - $store = new WordPressObjectFieldStore(); + $store = new OrderFieldStore(); $store->set( $post_id, '_dws_post_key', 'value' ); @@ -144,7 +144,7 @@ public function test_register_meta_box_registers_on_the_resolved_order_screen(): $screen = OrderUtil::custom_orders_table_usage_is_enabled() ? 'woocommerce_page_wc-orders' : 'shop_order'; \set_current_screen( $screen ); - ( new WordPressObjectFieldStore() )->register_meta_box( $this->box() ); + ( new OrderFieldStore() )->register_meta_box( $this->box() ); \do_action( "add_meta_boxes_$screen" ); self::assertArrayHasKey( self::BOX_ID, $this->boxes_on( $screen ) ); @@ -162,7 +162,7 @@ public function test_the_registered_box_renders_a_nonce_and_its_field_control(): $screen = OrderUtil::custom_orders_table_usage_is_enabled() ? 'woocommerce_page_wc-orders' : 'shop_order'; \set_current_screen( $screen ); - ( new WordPressObjectFieldStore() )->register_meta_box( $this->box() ); + ( new OrderFieldStore() )->register_meta_box( $this->box() ); \do_action( "add_meta_boxes_$screen" ); $definition = (array) ( $this->boxes_on( $screen )[ self::BOX_ID ] ?? array() ); @@ -194,7 +194,7 @@ public function test_a_bespoke_render_and_save_descriptor_emits_the_nonce_and_sa $saved_for = $object_id; }, ); - $store = new WordPressObjectFieldStore(); + $store = new OrderFieldStore(); $store->register_meta_box( $box ); \do_action( "add_meta_boxes_$screen" ); @@ -215,7 +215,7 @@ public function test_a_bespoke_render_and_save_descriptor_emits_the_nonce_and_sa } public function test_a_truthy_submission_is_stored_and_a_falsy_one_deletes_the_meta(): void { - $store = new WordPressObjectFieldStore(); + $store = new OrderFieldStore(); $store->register_meta_box( $this->box() ); // Checkbox checked → meta stored. @@ -243,7 +243,7 @@ public function test_a_zero_value_is_stored_not_revoked(): void { new SettingsField( id: 'note', type: 'text', label: 'Note' ), ), ); - $store = new WordPressObjectFieldStore(); + $store = new OrderFieldStore(); $store->register_meta_box( $box ); // A literal "0" is a real value, not an empty submission, so it must persist rather than revoke. @@ -258,7 +258,7 @@ public function test_a_zero_value_is_stored_not_revoked(): void { } public function test_save_is_skipped_without_a_valid_nonce(): void { - $store = new WordPressObjectFieldStore(); + $store = new OrderFieldStore(); $store->register_meta_box( $this->box() ); $_POST = array( self::BOX_ID => array( 'unlocked' => '1' ) ); @@ -281,7 +281,7 @@ public function test_the_post_meta_fallback_preserves_backslashes(): void { $post_id = \wp_insert_post( array( 'post_title' => 'Probe', 'post_status' => 'publish' ) ); \assert( \is_int( $post_id ) ); self::assertFalse( \wc_get_order( $post_id ) ); - $store = new WordPressObjectFieldStore(); + $store = new OrderFieldStore(); $store->set( $post_id, '_dws_path', 'C:\\Users\\dev\\file.txt' ); @@ -305,7 +305,7 @@ public function test_clearing_a_field_with_a_default_revokes_it_without_restorin new SettingsField( id: 'note', type: 'text', label: 'Note', default: 'preset' ), ), ); - $store = new WordPressObjectFieldStore(); + $store = new OrderFieldStore(); $store->register_meta_box( $box ); // Store a value, then submit it empty: delete-on-falsy revokes the meta. @@ -334,7 +334,7 @@ public function test_register_meta_box_also_registers_on_the_restricted_hpos_scr } \set_current_screen( 'admin_page_wc-orders' ); - ( new WordPressObjectFieldStore() )->register_meta_box( $this->box() ); + ( new OrderFieldStore() )->register_meta_box( $this->box() ); \do_action( 'add_meta_boxes_admin_page_wc-orders' ); self::assertArrayHasKey( self::BOX_ID, $this->boxes_on( 'admin_page_wc-orders' ) ); @@ -351,7 +351,7 @@ public function test_a_field_meta_key_overrides_the_id_for_storage(): void { new SettingsField( id: 'unlocked', type: 'checkbox', label: 'Unlocked', meta_key: '_lpm_unlocked' ), ), ); - $store = new WordPressObjectFieldStore(); + $store = new OrderFieldStore(); $store->register_meta_box( $box ); $_POST = array( self::NONCE_NAME => $this->nonce(), self::BOX_ID => array( 'unlocked' => '1' ) ); @@ -369,7 +369,7 @@ public function test_save_is_skipped_for_a_user_without_the_orders_capability(): \assert( \is_int( $subscriber ) ); \wp_set_current_user( $subscriber ); - $store = new WordPressObjectFieldStore(); + $store = new OrderFieldStore(); $store->register_meta_box( $this->box() ); // A valid nonce for this user, but the user lacks edit_shop_orders: the save must be refused. @@ -392,7 +392,7 @@ public function test_a_multi_field_save_persists_the_order_once(): void { new SettingsField( id: 'second', type: 'text', label: 'Second' ), ), ); - $store = new WordPressObjectFieldStore(); + $store = new OrderFieldStore(); $store->register_meta_box( $box ); $saves = 0; @@ -416,7 +416,7 @@ static function () use ( &$saves ): void { } public function test_a_no_op_save_does_not_persist_the_order(): void { - $store = new WordPressObjectFieldStore(); + $store = new OrderFieldStore(); $store->register_meta_box( $this->box() ); $saves = 0; @@ -446,7 +446,7 @@ public function test_a_duplicate_field_id_in_a_box_is_rejected(): void { new SettingsField( id: 'flag', type: 'checkbox', label: 'B' ), ), ); - $store = new WordPressObjectFieldStore(); + $store = new OrderFieldStore(); $store->register_meta_box( $box ); $_POST = array( self::NONCE_NAME => $this->nonce(), self::BOX_ID => array( 'flag' => '1' ) ); @@ -467,7 +467,7 @@ public function test_register_meta_box_rejects_a_non_order_screen(): void { $this->expectException( InvalidObjectMetaBoxException::class ); - ( new WordPressObjectFieldStore() )->register_meta_box( $box ); + ( new OrderFieldStore() )->register_meta_box( $box ); } public function test_a_meta_box_title_is_escaped_before_registration(): void { @@ -482,7 +482,7 @@ public function test_a_meta_box_title_is_escaped_before_registration(): void { priority: 'default', fields_provider: static fn ( int $object_id ): array => array(), ); - ( new WordPressObjectFieldStore() )->register_meta_box( $box ); + ( new OrderFieldStore() )->register_meta_box( $box ); \do_action( "add_meta_boxes_$screen" ); // WordPress echoes the stored title raw in do_meta_boxes(), so the store hands it pre-escaped. diff --git a/packages/woocommerce/tests/Integration/OrderData/index.php b/packages/woocommerce/tests/Integration/OrderData/index.php new file mode 100644 index 0000000..f767346 --- /dev/null +++ b/packages/woocommerce/tests/Integration/OrderData/index.php @@ -0,0 +1 @@ + Date: Sun, 21 Jun 2026 16:55:37 +0200 Subject: [PATCH 02/21] test(bootstrap): drop redundant always-true const assertion in AggregatorTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PHPStan flagged assertTrue( defined( '…\Requirements\FRAMEWORK_MIN_PHP' ) ) as staticMethod.alreadyNarrowedType: the namespaced const is resolved statically, so the assertion is always true. The Requirements concern file's wiring is already covered by the same-file check_requirements function_exists assertion, so the const check was redundant. Removing it restores a PHPStan-green tree and keeps exactly one wiring assertion per concern file (Environment / Plugin / Requirements / Notice). Assisted-by: Claude Code:claude-opus-4-8 --- packages/bootstrap/tests/Unit/AggregatorTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bootstrap/tests/Unit/AggregatorTest.php b/packages/bootstrap/tests/Unit/AggregatorTest.php index c9bb263..ca0f761 100644 --- a/packages/bootstrap/tests/Unit/AggregatorTest.php +++ b/packages/bootstrap/tests/Unit/AggregatorTest.php @@ -10,6 +10,5 @@ public function test_aggregator_wires_every_concern_file(): void { self::assertTrue( \function_exists( 'DeepWebSolutions\Framework\Bootstrap\Plugin\get_plugin_metadata' ) ); self::assertTrue( \function_exists( 'DeepWebSolutions\Framework\Bootstrap\Requirements\check_requirements' ) ); self::assertTrue( \function_exists( 'DeepWebSolutions\Framework\Bootstrap\Notice\output_requirements_error' ) ); - self::assertTrue( \defined( 'DeepWebSolutions\Framework\Bootstrap\Requirements\FRAMEWORK_MIN_PHP' ) ); } } From 951183965f8960813525639e4a13fc13f23e3b53 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sun, 21 Jun 2026 17:26:33 +0200 Subject: [PATCH 03/21] refactor(framework): establish component-grammar conventions (interface placement + VO/descriptor) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-Phase-4 hardening — codify two grammar rules that replace improvised conventions, restoring a predictable structure without reintroducing inheritance. Interface placement: an interface lives at its concept-folder root; a single-concept package keeps its contract flat at the package src root (the locked PluginInterface/PluginKernel pair is the one flat exception); no generic Contracts/ subfolder. Applied by removing the lone outlier — Hooks/Contracts/HookHandlerInterface moves to Hooks/HookHandlerInterface (the only Contracts/ folder in the tree). Value object vs descriptor: ValueObjects/ folders hold two carrier kinds, distinguished by base class + docblock, not a marker interface. A value object extends AbstractValueObject only when structural equals()/JSON is consumed (PluginHeader, Version; docblock opens "Value object ..."); a descriptor is a bare final readonly carrier, required when it holds closures/providers/WP objects (SettingsField, SettingsPage, SettingsSection, ObjectMetaBox, AdminNotice, DependencyRequirement; docblock opens "Descriptor for ..."). A marker DescriptorInterface is deferred until a consumer needs it. Both rules recorded as decision blocks in AGENTS.md. settings is multi-concept but under-foldered; its concept-folder reorganization is a separate follow-up. Assisted-by: Claude Code:claude-opus-4-8 --- AGENTS.md | 8 ++++++++ packages/core/src/ValueObjects/PluginHeader.php | 2 +- .../src/ObjectField/ValueObjects/ObjectMetaBox.php | 2 +- packages/settings/src/ValueObjects/SettingsField.php | 2 +- packages/settings/src/ValueObjects/SettingsPage.php | 4 ++-- packages/settings/src/ValueObjects/SettingsSection.php | 2 +- packages/shared/src/Version/Version.php | 2 +- .../src/AdminNotices/ValueObjects/AdminNotice.php | 2 +- .../ValueObjects/DependencyRequirement.php | 10 +++++----- packages/utilities/src/Hooks/Contracts/index.php | 1 - .../src/Hooks/Handlers/BufferedHookHandler.php | 2 +- .../utilities/src/Hooks/Handlers/DirectHookHandler.php | 2 +- .../utilities/src/Hooks/Handlers/ScopedHookHandler.php | 2 +- .../src/Hooks/{Contracts => }/HookHandlerInterface.php | 2 +- packages/utilities/src/Hooks/HooksService.php | 1 - .../utilities/tests/Unit/Hooks/HooksServiceTest.php | 2 +- 16 files changed, 26 insertions(+), 20 deletions(-) delete mode 100644 packages/utilities/src/Hooks/Contracts/index.php rename packages/utilities/src/Hooks/{Contracts => }/HookHandlerInterface.php (97%) diff --git a/AGENTS.md b/AGENTS.md index 72c08d5..516d950 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -365,3 +365,11 @@ Resolves parity finding A1 (the v1→v2 parity reconciliation — reports archiv **Cross-package change:** `SettingsField` gains an optional `description` (additive; the WP-backed `FieldRenderer` now renders it too, so the WP options page + object meta box benefit). `desc_tip` stays WC-only (defaulted true in the product renderer). `php-stubs/woocommerce-stubs` added to the woocommerce package's php-scoper `scoping-stubs` (the product-data + existing WC settings code reference WC symbols a consumer's scoper must leave external). **deptrac:** unchanged — `WooCommerce: [Core, Shared, Settings]` already covers the reuse of `SettingsField`/`SettingsSection`/`FieldProcessor`/`FieldType`/`OptionsResolver` + the settings exceptions; `Shared` for `InvalidProductDataTabException`'s base. No abstract base (the "intrinsically requires" exception does not fire — a tab registers via filters, with no WC class to subclass) and no new interface (single impl). Gates at landing: Unit 288 · Integration 261 · PHPStan clean · deptrac 0 · Infection 100% covered-code MSI on `woocommerce/src`. Design spec in the project vault (`drafts/2026-06-15-wc-product-settings-tab-mini-spec`). + +### Component grammar — interface placement (2026-06-21) + +One rule replaces three improvised conventions. An interface lives at the ROOT of the concept folder it names; concrete variants live in plural sub-bags beneath it (`Handlers/`, `Stores/`, `Backends/`, `ValueObjects/`, `Exceptions/`). A package whose ENTIRE content is a single concept keeps its contract flat at the package `src/` root — the package boundary IS the concept boundary (e.g. `storage`: `KeyValueStoreInterface` beside its stores). The locked core entrypoint pair (`PluginInterface` / `PluginKernel`) is the one explicit flat exception; do not generalize it. There is NO generic `Contracts/` subfolder — an interface is the concept's root type, not a nested artifact. Placement test for a new interface: does its concept share the package with other concepts? → concept-folder root. Is the package a single concept (or the locked entrypoint)? → package root. Applied: `Hooks/Contracts/HookHandlerInterface` moved to `Hooks/HookHandlerInterface`. (`settings` is multi-concept but currently under-foldered; its concept-folder reorganization is tracked as a separate item.) + +### Component grammar — value object vs descriptor (2026-06-21) + +`ValueObjects/` folders hold immutable carriers of two kinds; the folder name is kept, and the kinds are distinguished by base class + docblock, not by a marker interface. **Value object** — extends `Shared\ValueObject\AbstractValueObject`; use ONLY when reflection-driven structural `equals()` / `jsonSerialize()` is actually consumed and every public property can participate safely in structural equality (`PluginHeader`, `Version`); its docblock opens "Value object …". **Descriptor** — a bare `final readonly` class with no base; the default immutable carrier, and REQUIRED when it holds closures/callables, runtime providers, or WordPress/WooCommerce objects (`SettingsField`, `SettingsPage`, `SettingsSection`, `ObjectMetaBox`, `AdminNotice`, `DependencyRequirement`); its docblock opens "Descriptor for …". A marker `DescriptorInterface` is deferred until real code must typehint a polymorphic descriptor collection — a taxonomy-only marker is ceremony (service interfaces only on plurality). diff --git a/packages/core/src/ValueObjects/PluginHeader.php b/packages/core/src/ValueObjects/PluginHeader.php index 15cf161..9bd064d 100644 --- a/packages/core/src/ValueObjects/PluginHeader.php +++ b/packages/core/src/ValueObjects/PluginHeader.php @@ -7,7 +7,7 @@ use function DeepWebSolutions\Framework\Bootstrap\Plugin\get_plugin_metadata; /** - * Typed read-once value object wrapping the WP plugin file header. + * Value object wrapping the WP plugin file header, read once and typed. * * The single source of truth is the file header comment of the plugin's main * file. Consumers must NOT type metadata values manually — pass the file path diff --git a/packages/settings/src/ObjectField/ValueObjects/ObjectMetaBox.php b/packages/settings/src/ObjectField/ValueObjects/ObjectMetaBox.php index 736a56e..b696c4f 100644 --- a/packages/settings/src/ObjectField/ValueObjects/ObjectMetaBox.php +++ b/packages/settings/src/ObjectField/ValueObjects/ObjectMetaBox.php @@ -5,7 +5,7 @@ use DeepWebSolutions\Framework\Settings\ObjectField\Exceptions\InvalidObjectMetaBoxException; /** - * Declarative description of a per-entity meta box (object fields). + * Descriptor for a per-entity meta box (object fields). * * Drives the object-field backend: the box attaches to an object screen and its * fields are built per object at render time by the provider closure — so a box diff --git a/packages/settings/src/ValueObjects/SettingsField.php b/packages/settings/src/ValueObjects/SettingsField.php index 432a063..4c2bbb0 100644 --- a/packages/settings/src/ValueObjects/SettingsField.php +++ b/packages/settings/src/ValueObjects/SettingsField.php @@ -6,7 +6,7 @@ use DeepWebSolutions\Framework\Settings\SettingsOptionsProviderInterface; /** - * Declarative, storage- and UI-agnostic description of a single settings field. + * Descriptor for a single settings field, storage- and UI-agnostic. * * Immutable carrier of everything a backend needs to register, render, sanitize, * and validate one field: identifier, type token, presentation metadata, an diff --git a/packages/settings/src/ValueObjects/SettingsPage.php b/packages/settings/src/ValueObjects/SettingsPage.php index 2897e54..921c949 100644 --- a/packages/settings/src/ValueObjects/SettingsPage.php +++ b/packages/settings/src/ValueObjects/SettingsPage.php @@ -3,8 +3,8 @@ namespace DeepWebSolutions\Framework\Settings\ValueObjects; /** - * Declarative description of a settings page: a titled, capability-gated screen - * composed of sections. + * Descriptor for a settings page: a titled, capability-gated screen composed + * of sections. * * The location is interpreted by the backend the page is registered with — a * WordPress parent slug for the options backend, a tab id for the WooCommerce diff --git a/packages/settings/src/ValueObjects/SettingsSection.php b/packages/settings/src/ValueObjects/SettingsSection.php index 3a8b951..e2a6922 100644 --- a/packages/settings/src/ValueObjects/SettingsSection.php +++ b/packages/settings/src/ValueObjects/SettingsSection.php @@ -3,7 +3,7 @@ namespace DeepWebSolutions\Framework\Settings\ValueObjects; /** - * Declarative description of a settings section: a titled group of fields. + * Descriptor for a settings section: a titled group of fields. * * A section is the unit of storage for the WordPress options backend — its * identifier doubles as the persisted option-group key segment — and the unit of diff --git a/packages/shared/src/Version/Version.php b/packages/shared/src/Version/Version.php index 5234110..b1f1d46 100644 --- a/packages/shared/src/Version/Version.php +++ b/packages/shared/src/Version/Version.php @@ -6,7 +6,7 @@ use DeepWebSolutions\Framework\Shared\Version\Exceptions\InvalidVersionException; /** - * Immutable representation of a software version. SemVer-shaped: + * Value object representing a software version. SemVer-shaped: * MAJOR[.MINOR[.PATCH]][-PRERELEASE][+BUILD]. Provides type-safe ordering and * equality. * diff --git a/packages/utilities/src/AdminNotices/ValueObjects/AdminNotice.php b/packages/utilities/src/AdminNotices/ValueObjects/AdminNotice.php index e3be73b..a8dea7a 100644 --- a/packages/utilities/src/AdminNotices/ValueObjects/AdminNotice.php +++ b/packages/utilities/src/AdminNotices/ValueObjects/AdminNotice.php @@ -3,7 +3,7 @@ namespace DeepWebSolutions\Framework\Utilities\AdminNotices\ValueObjects; /** - * Value object representing a single WordPress admin notice. + * Descriptor for a single WordPress admin notice. * * The $is_persistent flag governs post-render retention in a persistent store: a non-persistent * notice is consumed (removed) after it renders once, a persistent one recurs until dismissed or diff --git a/packages/utilities/src/AdminNotices/ValueObjects/DependencyRequirement.php b/packages/utilities/src/AdminNotices/ValueObjects/DependencyRequirement.php index 4a41bb7..76ad015 100644 --- a/packages/utilities/src/AdminNotices/ValueObjects/DependencyRequirement.php +++ b/packages/utilities/src/AdminNotices/ValueObjects/DependencyRequirement.php @@ -5,11 +5,11 @@ use DeepWebSolutions\Framework\Core\Conditional\ConditionalInterface; /** - * A dependency a plugin declares for missing-dependency admin notices: the conditional that decides - * whether it is satisfied, a human-readable label, and whether it is required (blocking) or optional - * (recommended). The getters derive the notice's identity and presentation purely from those fields, - * so {@see \DeepWebSolutions\Framework\Utilities\AdminNotices\DependencyAdminNoticeRenderer} owns only - * the translatable message and the queueing. + * Descriptor for a dependency a plugin declares for missing-dependency admin notices: the conditional + * that decides whether it is satisfied, a human-readable label, and whether it is required (blocking) + * or optional (recommended). The getters derive the notice's identity and presentation purely from + * those fields, so {@see \DeepWebSolutions\Framework\Utilities\AdminNotices\DependencyAdminNoticeRenderer} + * owns only the translatable message and the queueing. * * @since 2.0.0 * @version 2.0.0 diff --git a/packages/utilities/src/Hooks/Contracts/index.php b/packages/utilities/src/Hooks/Contracts/index.php deleted file mode 100644 index f767346..0000000 --- a/packages/utilities/src/Hooks/Contracts/index.php +++ /dev/null @@ -1 +0,0 @@ - Date: Sun, 21 Jun 2026 17:26:34 +0200 Subject: [PATCH 04/21] fix(shared): make AbstractValueObject::equals() recurse arrays like jsonSerialize() equals() recursed only direct ValueObjectInterface properties and fell back to strict !== for everything else, so an array of value objects was compared by identity -- while jsonSerialize() (via convert_to_primitives) recurses arrays element-by-element. Two value objects holding a structurally-equal list of distinct instances therefore reported equals() === false yet serialized identically. Compare the canonical primitive projections instead -- the same convert_to_primitives() traversal jsonSerialize() uses -- so equality and serialization share one source of truth and can no longer drift. This also fixes DateTimeInterface equality (equal-but-distinct instances now compare equal) and drops two property.dynamicName PHPStan ignores plus the now-unused get_public_property_names import. Adds an Integration regression: a value object holding list asserts equals() agrees with jsonSerialize() across distinct-but-equal instances. Assisted-by: Claude Code:claude-opus-4-8 --- .../src/ValueObject/AbstractValueObject.php | 18 +------- .../ValueObject/AbstractValueObjectTest.php | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/packages/shared/src/ValueObject/AbstractValueObject.php b/packages/shared/src/ValueObject/AbstractValueObject.php index 7004033..c5c8885 100644 --- a/packages/shared/src/ValueObject/AbstractValueObject.php +++ b/packages/shared/src/ValueObject/AbstractValueObject.php @@ -3,7 +3,6 @@ namespace DeepWebSolutions\Framework\Shared\ValueObject; use function DeepWebSolutions\Framework\Shared\Reflection\convert_to_primitives; -use function DeepWebSolutions\Framework\Shared\Reflection\get_public_property_names; /** * Base class for value objects. Provides reflection-driven structural equality, @@ -47,22 +46,7 @@ final public function equals( ValueObjectInterface $other ): bool { return false; } - foreach ( get_public_property_names( $this ) as $property_name ) { - // @phpstan-ignore-next-line property.dynamicName - $value_this = $this->{ $property_name }; - // @phpstan-ignore-next-line property.dynamicName - $value_other = $other->{ $property_name }; - - if ( $value_this instanceof ValueObjectInterface ) { - if ( ! $value_other instanceof ValueObjectInterface || ! $value_this->equals( $value_other ) ) { - return false; - } - } elseif ( $value_this !== $value_other ) { - return false; - } - } - - return true; + return convert_to_primitives( $this ) === convert_to_primitives( $other ); } /** diff --git a/packages/shared/tests/Integration/ValueObject/AbstractValueObjectTest.php b/packages/shared/tests/Integration/ValueObject/AbstractValueObjectTest.php index 255ad20..d585cab 100644 --- a/packages/shared/tests/Integration/ValueObject/AbstractValueObjectTest.php +++ b/packages/shared/tests/Integration/ValueObject/AbstractValueObjectTest.php @@ -25,6 +25,15 @@ public function __construct( ) {} } +final readonly class CollectionFixtureValueObject extends AbstractValueObject { + /** + * @param list $items + */ + public function __construct( + public array $items, + ) {} +} + #[CoversClass( AbstractValueObject::class )] final class AbstractValueObjectTest extends TestCase { public function test_equals_returns_true_for_structurally_identical_objects(): void { @@ -57,6 +66,41 @@ public function test_equals_returns_false_when_nested_value_objects_differ(): vo self::assertFalse( $a->equals( $b ) ); } + public function test_equals_recurses_into_arrays_of_value_objects(): void { + $a = new CollectionFixtureValueObject( + array( + new FixtureValueObject( 'foo', 1 ), + new FixtureValueObject( 'bar', 2 ), + ), + ); + $b = new CollectionFixtureValueObject( + array( + new FixtureValueObject( 'foo', 1 ), + new FixtureValueObject( 'bar', 2 ), + ), + ); + + self::assertTrue( $a->equals( $b ) ); + self::assertSame( $a->jsonSerialize(), $b->jsonSerialize() ); + } + + public function test_equals_false_when_value_object_inside_array_differs(): void { + $a = new CollectionFixtureValueObject( + array( + new FixtureValueObject( 'foo', 1 ), + new FixtureValueObject( 'bar', 2 ), + ), + ); + $b = new CollectionFixtureValueObject( + array( + new FixtureValueObject( 'foo', 1 ), + new FixtureValueObject( 'bar', 3 ), + ), + ); + + self::assertFalse( $a->equals( $b ) ); + } + public function test_json_serialize_returns_public_properties(): void { $vo = new FixtureValueObject( 'foo', 7 ); self::assertSame( From 0182e1be31b8fb8f6ef9655ed858b86a76adf385 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Sun, 21 Jun 2026 18:24:05 +0200 Subject: [PATCH 05/21] refactor(settings,woocommerce): reorganize settings into Schema/ + Backend/ concept folders; align woocommerce Backend/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-Phase-4 hardening — resolve settings' under-fragmentation (the flattest yet most multi-concept package) by folding its flat files into three peer concept folders, per the codified component grammar: Schema/ shared declarative field model + render/process engine (FieldType, FieldRenderer, FieldProcessor, OptionsResolver, SettingsFieldAggregator, provider interfaces, the SettingsField/Page/Section descriptors, field exceptions) Backend/ WP options-page surface (SettingsBackendInterface + WordPressSettingsBackend) ObjectField/ unchanged Schema/ is the cross-cutting core consumed by every surface (the WP backend, ObjectField, and the woocommerce package), and is the clean lift-out point should the field core ever be extracted to its own package. Also rename woocommerce/SettingsPage/ -> woocommerce/Backend/ so the settings-page backend surface carries one name in both packages (settings/Backend + woocommerce/Backend), beside woocommerce's ProductData/ + OrderData/. Pure moves + namespace/reference rewrites; no behavior change. deptrac unchanged (0 violations). PHPStan clean, Unit 290, Integration 274. Assisted-by: Claude Code:claude-opus-4-8 --- .../{ => Backend}/SettingsBackendInterface.php | 4 ++-- .../{ => Backend}/WordPressSettingsBackend.php | 18 ++++++++++-------- .../src/{Exceptions => Backend}/index.php | 0 .../DuplicateSettingsFieldException.php | 2 +- .../DuplicateSettingsSectionException.php | 2 +- .../InvalidSettingsFieldException.php | 2 +- .../InvalidSettingsOptionsException.php | 2 +- .../Exceptions/UnknownFieldTypeException.php | 2 +- .../Exceptions}/index.php | 0 .../src/{ => Schema}/FieldProcessor.php | 6 +++--- .../src/{ => Schema}/FieldRenderer.php | 6 +++--- .../settings/src/{ => Schema}/FieldType.php | 2 +- .../src/{ => Schema}/OptionsResolver.php | 4 ++-- .../{ => Schema}/SettingsFieldAggregator.php | 6 +++--- .../SettingsFieldProviderInterface.php | 4 ++-- .../SettingsOptionsProviderInterface.php | 2 +- .../ValueObjects/SettingsField.php | 6 +++--- .../{ => Schema}/ValueObjects/SettingsPage.php | 2 +- .../ValueObjects/SettingsSection.php | 2 +- .../src/Schema/ValueObjects}/index.php | 0 packages/settings/src/Schema/index.php | 1 + .../Integration/CrossComponentSettingsTest.php | 14 +++++++------- .../tests/Integration/FieldRendererTest.php | 6 +++--- .../WordPressSettingsBackendTest.php | 12 ++++++------ .../settings/tests/Unit/FieldProcessorTest.php | 8 ++++---- .../settings/tests/Unit/ObjectMetaBoxTest.php | 2 +- .../tests/Unit/OptionsResolverTest.php | 6 +++--- .../tests/Unit/SettingsFieldAggregatorTest.php | 8 ++++---- .../settings/tests/Unit/SettingsFieldTest.php | 6 +++--- .../settings/tests/Unit/SettingsPageTest.php | 4 ++-- .../tests/Unit/SettingsSectionTest.php | 4 ++-- .../DescriptorBackedWCSettingsPage.php | 6 +++--- .../WCSettingsBuilder.php | 10 +++++----- .../WooCommerceSettingsBackend.php | 12 ++++++------ packages/woocommerce/src/Backend/index.php | 1 + .../src/OrderData/OrderFieldStore.php | 8 ++++---- .../ProductData/ProductDataFieldRenderer.php | 8 ++++---- .../src/ProductData/ProductDataFieldStore.php | 10 +++++----- .../src/ProductData/ProductDataTab.php | 2 +- .../DescriptorBackedWCSettingsPageTest.php | 8 ++++---- .../Integration/Fixtures/BarWCSettingsPage.php | 2 +- .../Integration/Fixtures/FooWCSettingsPage.php | 2 +- .../Fixtures/LazyBindWCSettingsPage.php | 2 +- .../Fixtures/UnboundWCSettingsPage.php | 2 +- .../OrderData/OrderFieldStoreTest.php | 12 ++++++------ .../ProductDataFieldRendererTest.php | 6 +++--- .../Integration/ProductDataFieldStoreTest.php | 8 ++++---- .../WooCommerceSettingsBackendTest.php | 12 ++++++------ .../Unit/ProductDataFieldRendererTest.php | 6 +++--- .../tests/Unit/ProductDataTabTest.php | 4 ++-- .../tests/Unit/WCSettingsBuilderTest.php | 10 +++++----- tests/Fixtures/consumer-smoke/smoke.php | 4 ++-- 52 files changed, 141 insertions(+), 137 deletions(-) rename packages/settings/src/{ => Backend}/SettingsBackendInterface.php (93%) rename packages/settings/src/{ => Backend}/WordPressSettingsBackend.php (94%) rename packages/settings/src/{Exceptions => Backend}/index.php (100%) rename packages/settings/src/{ => Schema}/Exceptions/DuplicateSettingsFieldException.php (82%) rename packages/settings/src/{ => Schema}/Exceptions/DuplicateSettingsSectionException.php (82%) rename packages/settings/src/{ => Schema}/Exceptions/InvalidSettingsFieldException.php (86%) rename packages/settings/src/{ => Schema}/Exceptions/InvalidSettingsOptionsException.php (83%) rename packages/settings/src/{ => Schema}/Exceptions/UnknownFieldTypeException.php (82%) rename packages/settings/src/{ValueObjects => Schema/Exceptions}/index.php (100%) rename packages/settings/src/{ => Schema}/FieldProcessor.php (94%) rename packages/settings/src/{ => Schema}/FieldRenderer.php (97%) rename packages/settings/src/{ => Schema}/FieldType.php (92%) rename packages/settings/src/{ => Schema}/OptionsResolver.php (91%) rename packages/settings/src/{ => Schema}/SettingsFieldAggregator.php (88%) rename packages/settings/src/{ => Schema}/SettingsFieldProviderInterface.php (82%) rename packages/settings/src/{ => Schema}/SettingsOptionsProviderInterface.php (92%) rename packages/settings/src/{ => Schema}/ValueObjects/SettingsField.php (95%) rename packages/settings/src/{ => Schema}/ValueObjects/SettingsPage.php (95%) rename packages/settings/src/{ => Schema}/ValueObjects/SettingsSection.php (93%) rename packages/{woocommerce/src/SettingsPage => settings/src/Schema/ValueObjects}/index.php (100%) create mode 100644 packages/settings/src/Schema/index.php rename packages/woocommerce/src/{SettingsPage => Backend}/DescriptorBackedWCSettingsPage.php (95%) rename packages/woocommerce/src/{SettingsPage => Backend}/WCSettingsBuilder.php (95%) rename packages/woocommerce/src/{SettingsPage => Backend}/WooCommerceSettingsBackend.php (93%) create mode 100644 packages/woocommerce/src/Backend/index.php diff --git a/packages/settings/src/SettingsBackendInterface.php b/packages/settings/src/Backend/SettingsBackendInterface.php similarity index 93% rename from packages/settings/src/SettingsBackendInterface.php rename to packages/settings/src/Backend/SettingsBackendInterface.php index 0af5202..788b424 100644 --- a/packages/settings/src/SettingsBackendInterface.php +++ b/packages/settings/src/Backend/SettingsBackendInterface.php @@ -1,8 +1,8 @@ + * @return list<\DeepWebSolutions\Framework\Settings\Schema\ValueObjects\SettingsField> */ public function fields( int $object_id ): array { return array(); diff --git a/packages/settings/tests/Unit/OptionsResolverTest.php b/packages/settings/tests/Unit/OptionsResolverTest.php index 92c4db4..9be32e3 100644 --- a/packages/settings/tests/Unit/OptionsResolverTest.php +++ b/packages/settings/tests/Unit/OptionsResolverTest.php @@ -2,9 +2,9 @@ namespace DeepWebSolutions\Framework\Settings\Tests\Unit; -use DeepWebSolutions\Framework\Settings\Exceptions\InvalidSettingsOptionsException; -use DeepWebSolutions\Framework\Settings\OptionsResolver; -use DeepWebSolutions\Framework\Settings\SettingsOptionsProviderInterface; +use DeepWebSolutions\Framework\Settings\Schema\Exceptions\InvalidSettingsOptionsException; +use DeepWebSolutions\Framework\Settings\Schema\OptionsResolver; +use DeepWebSolutions\Framework\Settings\Schema\SettingsOptionsProviderInterface; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; diff --git a/packages/settings/tests/Unit/SettingsFieldAggregatorTest.php b/packages/settings/tests/Unit/SettingsFieldAggregatorTest.php index 989d9ca..72c1baa 100644 --- a/packages/settings/tests/Unit/SettingsFieldAggregatorTest.php +++ b/packages/settings/tests/Unit/SettingsFieldAggregatorTest.php @@ -2,10 +2,10 @@ namespace DeepWebSolutions\Framework\Settings\Tests\Unit; -use DeepWebSolutions\Framework\Settings\Exceptions\DuplicateSettingsFieldException; -use DeepWebSolutions\Framework\Settings\SettingsFieldAggregator; -use DeepWebSolutions\Framework\Settings\SettingsFieldProviderInterface; -use DeepWebSolutions\Framework\Settings\ValueObjects\SettingsField; +use DeepWebSolutions\Framework\Settings\Schema\Exceptions\DuplicateSettingsFieldException; +use DeepWebSolutions\Framework\Settings\Schema\SettingsFieldAggregator; +use DeepWebSolutions\Framework\Settings\Schema\SettingsFieldProviderInterface; +use DeepWebSolutions\Framework\Settings\Schema\ValueObjects\SettingsField; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; diff --git a/packages/settings/tests/Unit/SettingsFieldTest.php b/packages/settings/tests/Unit/SettingsFieldTest.php index 34f576f..8026ce1 100644 --- a/packages/settings/tests/Unit/SettingsFieldTest.php +++ b/packages/settings/tests/Unit/SettingsFieldTest.php @@ -2,9 +2,9 @@ namespace DeepWebSolutions\Framework\Settings\Tests\Unit; -use DeepWebSolutions\Framework\Settings\Exceptions\InvalidSettingsFieldException; -use DeepWebSolutions\Framework\Settings\SettingsOptionsProviderInterface; -use DeepWebSolutions\Framework\Settings\ValueObjects\SettingsField; +use DeepWebSolutions\Framework\Settings\Schema\Exceptions\InvalidSettingsFieldException; +use DeepWebSolutions\Framework\Settings\Schema\SettingsOptionsProviderInterface; +use DeepWebSolutions\Framework\Settings\Schema\ValueObjects\SettingsField; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; diff --git a/packages/settings/tests/Unit/SettingsPageTest.php b/packages/settings/tests/Unit/SettingsPageTest.php index ec4a0b7..0d32afb 100644 --- a/packages/settings/tests/Unit/SettingsPageTest.php +++ b/packages/settings/tests/Unit/SettingsPageTest.php @@ -2,8 +2,8 @@ namespace DeepWebSolutions\Framework\Settings\Tests\Unit; -use DeepWebSolutions\Framework\Settings\ValueObjects\SettingsPage; -use DeepWebSolutions\Framework\Settings\ValueObjects\SettingsSection; +use DeepWebSolutions\Framework\Settings\Schema\ValueObjects\SettingsPage; +use DeepWebSolutions\Framework\Settings\Schema\ValueObjects\SettingsSection; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; diff --git a/packages/settings/tests/Unit/SettingsSectionTest.php b/packages/settings/tests/Unit/SettingsSectionTest.php index b81917b..0d2000f 100644 --- a/packages/settings/tests/Unit/SettingsSectionTest.php +++ b/packages/settings/tests/Unit/SettingsSectionTest.php @@ -2,8 +2,8 @@ namespace DeepWebSolutions\Framework\Settings\Tests\Unit; -use DeepWebSolutions\Framework\Settings\ValueObjects\SettingsField; -use DeepWebSolutions\Framework\Settings\ValueObjects\SettingsSection; +use DeepWebSolutions\Framework\Settings\Schema\ValueObjects\SettingsField; +use DeepWebSolutions\Framework\Settings\Schema\ValueObjects\SettingsSection; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; diff --git a/packages/woocommerce/src/SettingsPage/DescriptorBackedWCSettingsPage.php b/packages/woocommerce/src/Backend/DescriptorBackedWCSettingsPage.php similarity index 95% rename from packages/woocommerce/src/SettingsPage/DescriptorBackedWCSettingsPage.php rename to packages/woocommerce/src/Backend/DescriptorBackedWCSettingsPage.php index add1b34..0404a05 100644 --- a/packages/woocommerce/src/SettingsPage/DescriptorBackedWCSettingsPage.php +++ b/packages/woocommerce/src/Backend/DescriptorBackedWCSettingsPage.php @@ -1,9 +1,9 @@ Date: Sun, 21 Jun 2026 21:13:27 +0200 Subject: [PATCH 06/21] refactor(settings,woocommerce): consolidate duplicated field policy into Schema free functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-Phase-4 hardening — collapse copy-pasted field logic across the settings/woocommerce tier into namespaced free functions (the framework's convention, not static utility classes): Settings\Schema\filter_field_attributes() (#6, #16) Settings\Schema\is_valid_identifier() (#20) Settings\Schema\is_checkbox_checked() (#6, canonical) Settings\Schema\is_field_editable_by_current_user(SettingsField) (#6) WooCommerce\to_yes_no() (#6, Schema wrapper) #6 — the field-capability gate (copy-pasted across ~6 sites) and the attribute security filter + checkbox yes/no coercion (copy-pasted 3x with divergent rules) now have one owner each. The capability gate is a free function, not a method, to keep current_user_can() off the readonly SettingsField descriptor. Fixes a live checkbox bug: WCSettingsBuilder's (bool) cast (and FieldRenderer's checked state) mapped 'no' and arbitrary non-empty strings to checked/'yes'; the canonical rule (true/1/'1'/'yes' => checked) maps them to 'no'. The two ProductData sites already matched the canonical rule. #20 — id-charset validation now also covers SettingsSection id and SettingsPage slug (both feed wp_options keys), throwing new typed InvalidSettingsSectionException / InvalidSettingsPageException. #16 — the attribute filter keeps its strict on*-prefix block (a security control; narrowing to a handler denylist would risk under-blocking). Populating settings + woocommerce functions.php also clears the empty- aggregator finding (#9) for those two packages. deptrac unchanged. PHPStan clean, Unit 377, Integration 277. Assisted-by: Claude Code:claude-opus-4-8 --- packages/settings/functions.php | 4 +- .../src/Backend/WordPressSettingsBackend.php | 20 +-- .../ValueObjects/ObjectMetaBox.php | 14 +- .../InvalidSettingsPageException.php | 13 ++ .../InvalidSettingsSectionException.php | 13 ++ .../settings/src/Schema/FieldRenderer.php | 15 +- .../src/Schema/ValueObjects/SettingsField.php | 14 +- .../src/Schema/ValueObjects/SettingsPage.php | 15 +- .../Schema/ValueObjects/SettingsSection.php | 15 +- packages/settings/src/Schema/functions.php | 81 +++++++++ .../tests/Integration/SchemaFunctionsTest.php | 69 ++++++++ .../tests/Unit/SchemaFunctionsTest.php | 163 ++++++++++++++++++ .../settings/tests/Unit/SettingsPageTest.php | 53 ++++++ .../tests/Unit/SettingsSectionTest.php | 43 +++++ packages/woocommerce/functions.php | 4 +- .../DescriptorBackedWCSettingsPage.php | 4 +- .../src/Backend/WCSettingsBuilder.php | 53 +----- .../Backend/WooCommerceSettingsBackend.php | 11 +- .../src/OrderData/OrderFieldStore.php | 20 +-- .../ProductData/ProductDataFieldRenderer.php | 47 +---- .../src/ProductData/ProductDataFieldStore.php | 39 +---- .../src/ProductData/ProductDataTab.php | 14 +- packages/woocommerce/src/functions.php | 21 +++ .../woocommerce/tests/Unit/FunctionsTest.php | 44 +++++ .../tests/Unit/WCSettingsBuilderTest.php | 32 ++++ 25 files changed, 608 insertions(+), 213 deletions(-) create mode 100644 packages/settings/src/Schema/Exceptions/InvalidSettingsPageException.php create mode 100644 packages/settings/src/Schema/Exceptions/InvalidSettingsSectionException.php create mode 100644 packages/settings/src/Schema/functions.php create mode 100644 packages/settings/tests/Integration/SchemaFunctionsTest.php create mode 100644 packages/settings/tests/Unit/SchemaFunctionsTest.php create mode 100644 packages/woocommerce/src/functions.php create mode 100644 packages/woocommerce/tests/Unit/FunctionsTest.php diff --git a/packages/settings/functions.php b/packages/settings/functions.php index c993c4d..8dfe40f 100644 --- a/packages/settings/functions.php +++ b/packages/settings/functions.php @@ -1,5 +1,3 @@ /functions.php file is added under src/, require_once it here. +require_once __DIR__ . '/src/Schema/functions.php'; diff --git a/packages/settings/src/Backend/WordPressSettingsBackend.php b/packages/settings/src/Backend/WordPressSettingsBackend.php index 6fa106f..b23a6b3 100644 --- a/packages/settings/src/Backend/WordPressSettingsBackend.php +++ b/packages/settings/src/Backend/WordPressSettingsBackend.php @@ -13,6 +13,8 @@ use DeepWebSolutions\Framework\Storage\OptionsStore; use Psr\Log\LoggerInterface; +use function DeepWebSolutions\Framework\Settings\Schema\is_field_editable_by_current_user; + /** * WordPress options-backed settings backend for a single page. * @@ -292,7 +294,7 @@ private function render_page( SettingsPage $page ): void { \settings_fields( $option_name ); echo ''; foreach ( $section->fields as $field ) { - if ( ! $this->can_edit( $field ) ) { + if ( ! is_field_editable_by_current_user( $field ) ) { continue; } echo '