Repository navigation
Add the WooCommerce package: requirement, logger, uninstall handlers and settings helpers - #32
Merged
Merged
Conversation
The lock still recorded the settings package's manifest from before its text domain key, so it disagreed with packages/settings/composer.json. composer outdated --direct and npm outdated list nothing else, so only the settings path package's reference and extra moved. php-stubs/woocommerce-stubs is already a root dev dependency at 11.2.0, matching WooCommerce 11.2.1, and the configs workflows already pin 6302f1f, configs' trunk. Assisted-by: Claude Code:claude-opus-5-5
The woocommerce package had no code, so a plugin could neither refuse to start on an old or missing WooCommerce nor log through WooCommerce's log under its own source. WooCommerceRequirementTest covers a floor met, at, below and without its header, the F1 notice for users who can activate plugins only, its escaping, and translation only when the notice renders. In the without-woocommerce group, a missing WooCommerce shows WordPress core's dependency message to a super admin only. WooCommerceLoggerTest runs through a real WC_Logger: every level under WP_DEBUG, a threshold that replaces the WP_DEBUG default, rejected levels, the constructor's source winning over the context's, the shared interpolation cases, an exception written as a string, a context value WooCommerce's file format cannot encode, and an error reaching the error log exactly when WooCommerce's switch or threshold drops it. Without WooCommerce, errors reach the error log and lower levels go nowhere. PackageManifestsTest checks that every autoloaded package maps only its namespace to src/, which the woocommerce manifest does not do yet. Assisted-by: Claude Code:claude-opus-5-5
WooCommerceRequirement reads the entry file's WC requires at least header. check() returns true when WooCommerce is loaded at that floor, and otherwise hooks render_notice() on all_admin_notices. For users who can activate plugins, the notice is WordPress core's dependency message when WooCommerce is missing, or F1 when it is too old: "%1$s needs WooCommerce %2$s or later, so its features stay off until WooCommerce is updated." It translates only when it renders. WooCommerceLogger writes PSR-3 records to WooCommerce's log under the constructor's source, which wins over a source in the context. It resolves wc_get_logger() on the first record, so constructing it never needs WooCommerce. A threshold, when given, replaces the default of warning, or of every level under WP_DEBUG; an invalid level throws. Its interpolate() is a protected copy of ErrorLogLogger's. The context reaches WooCommerce with a Throwable under exception as a string, and any value that json_encode() rejects or throws on replaced by its type, because LogHandlerFileV2::format_entry() encodes the context unguarded. An error that WooCommerce drops, because it is not loaded, its logging is off or its threshold is above the level, also goes to the PHP error log. The woocommerce manifest maps its namespace to src/ and declares the wp-framework text domain, and phpstan.woocommerce.neon analyzes src/ with the WordPress and WooCommerce stubs. The escaping row expected the plugin's name with its tags escaped, but get_plugin_data() strips them, so the row now checks the notice's text and that no tag survives from the name or the version. Assisted-by: Claude Code:claude-opus-5-5
A plugin that stores order meta had no way to remove it on uninstall, because the bootstrap engine knows no WooCommerce table and WooCommerce is often deactivated before the plugin is deleted. The tests run the engine with WooCommerceUninstaller::get_handlers(), loaded by path from the woocommerce package's uninstall.php, as a plugin's own uninstall file will: - the handlers cover order_meta, order_meta_prefixes and order_item_meta; - a consenting run deletes exactly the footprint's keys from post meta, wc_orders_meta and the order item meta table, keeping each key's letter-case twin and the keys that only resemble a prefix; - a deleted post meta key is gone from the post's meta cache, and each of the six cache groups WooCommerce keeps orders and order items in is flushed; - the network pass leaves order data alone, and a value a handler cannot read is logged once and skipped; - in its own request, with WooCommerce left out of the active plugins and its order tables present, the run still deletes from both order tables; - in the without-woocommerce group, on a site without WooCommerce's tables, the run deletes from post meta and completes without a failure. Assisted-by: Claude Code:claude-opus-5-5
A plugin's own uninstall file requires the woocommerce package's uninstall.php beside the bootstrap package's, and passes WooCommerceUninstaller::get_handlers() to Uninstaller::run(). Neither package calls the other. The handlers cover order_meta, order_meta_prefixes and order_item_meta, and use only WordPress and $wpdb, so they work after WooCommerce is deactivated: - keys match byte for byte, because the tables' collation would also match another plugin's key that differs only in letter case; post meta rows go through delete_metadata_by_mid(), which clears each post's meta cache, and the wc_orders_meta and order item meta rows are deleted with prepared SQL where the site has those tables; - prefixes are listed as bytes from post meta and wc_orders_meta, as the engine lists its own; - afterwards, where the object cache flushes single groups, the groups WooCommerce 11.2.1 keeps orders, their meta and order items in are flushed: orders, order_objects, orders_data, orders_meta, order-items and order_item_meta; - a handler returns on the network pass, because order data belongs to a site, and logs and skips a value that is not a list of non-empty strings, because an empty prefix would match every row; - a failed query or delete throws, so the engine keeps the consent row and the retry completes. The loader and the class parse on PHP 7.4, sit outside every autoload key, and join the bootstrap package in phpstan.bootstrap.neon (PHP 7.4, WordPress 5.2) and in the PHP 7.4 syntax lane. Assisted-by: Claude Code:claude-opus-5-5
The woocommerce package had no settings helpers, so a plugin could not add a WooCommerce settings tab or section whose fields share the framework's field contract: validate, render, the disabled rule and the checkboxes type. SettingsTest drives WooCommerce's own settings screen, each request in its own PHP process, with the tab added on woocommerce_get_settings_pages and the section registered through WooCommerce's registry by a fixture, and its REST rows through WooCommerce's settings controller: - both constructors reject invalid ids and accept every v1 key, and a section's fields are built only when WooCommerce asks for them; - a tab lists its sections and renders only the current one's fields; saving one section leaves the other untouched, with an unchecked checkbox stored as no; a field round-trips through WC REST; - a registered section renders under its parent tab and saves its fields, and the registry rejects a taken id; - the bridge skips a disabled field even when its value is posted, keeps WooCommerce's value for a field without validate, hands validate the raw submission and stores its return value, and keeps the stored value on a rejection while WooCommerce shows its message once, as text; - validate receives no for an unchecked checkbox and an empty list for a cleared multiselect, and over REST receives WooCommerce's normalized value; - an option no helper owns passes through, each field's validate runs once when a section sits on a tab, and a field another plugin contributes through WooCommerce's filter is owned by the section it joins; - a custom type renders in WooCommerce's row through its render callable, saves through validate, is rejected without one, and is never printed for a field the helper does not own; - a checkboxes group renders, round-trips and rejects an unknown choice. The form serializer moves from OptionsPageTest into SerializesFormsTrait, so both suites post forms as a browser does. Assisted-by: Claude Code:claude-opus-5-5
SettingsTab is a final WC_Settings_Page, constructed in a woocommerce_get_settings_pages callback, whose definition gives its label and its sections. A section's fields are built only when WooCommerce first asks for them, and a section another plugin registers on the tab still reaches WooCommerce's registry. SettingsSection is a final WooCommerce SettingsSection, constructed in a woocommerce_settings_sections_registration callback and registered with the registry it receives. Both constructors reject an id that is not a valid key, through their own is_valid_key() copy.
Each helper owns its sections' fields as WooCommerce's woocommerce_get_settings_{tab} filter leaves them, claimed at PHP_INT_MAX, so fields other plugins add there are owned too. A section starts claiming only when WooCommerce asks it for its fields, so an instance the registry refused never claims the other section's fields. On woocommerce_admin_settings_sanitize_option, a helper returns any option it does not own unchanged, and hands its own to the internal Fields class:
- a disabled field returns null, which WooCommerce reads as keeping the stored value;
- a field without validate keeps WooCommerce's value, unless it has a render callable or the checkboxes type, which Forms sanitizes;
- an unchecked WooCommerce checkbox reaches validate as no and a cleared multiselect as an empty list, while any other field that posted nothing was not rendered and stays untouched;
- a rejection adds WooCommerce's settings error as plain text, the composed F4 message OptionsPage builds, because WooCommerce escapes its errors when it prints them.
For a field with a render callable or the checkboxes type, which WooCommerce does not draw, the helper hooks woocommerce_admin_field_{type}. It prints WooCommerce's row around Forms::render_control(), and only for fields it owns.
Two test rows are corrected. The laziness row recorded its calls through an arrow function's copy of the array, so it now builds its sections outside the definition. The custom row needs WooCommerce's title and sectionend entries, which open and close the table its tr belongs to.
Assisted-by: Claude Code:claude-opus-5-5
The file map still showed the design's bootstrap/uninstall.php and a settings function class that was never built. The map now names the root uninstall.php loader, the uninstall/ class directory and Settings' internal Fields class, and lists phpstan.woocommerce.neon. Convention 10 names the new by-path files. The style rule on shared internals keeps protected copies for small ones and allows an internal final class for a larger collaborator, as Fields is. Limitation 6 adds that WC REST cannot reach custom-type and checkboxes fields, and limitation 7 that the row around them imitates WooCommerce's markup. Assisted-by: Claude Code:claude-opus-5-5
The section's four implementations of WooCommerce's section interface sat under GETTERS, while the tab keeps its overrides under INHERITED METHODS, and get_settings() also hooks the section, so it is no plain getter. Assisted-by: Claude Code:claude-opus-5-5
Codex review: the bridge returned WooCommerce's value for an owned field without validate before it checked whether the form posted the field. WooCommerce's save turns an absent select into its default and an absent textarea into an empty string, so a field a script disabled, or one a section rendered conditionally, lost its stored value on the next save. That breaks the field contract, which keeps an absent field untouched on every surface. The absence check now runs first. A WooCommerce checkbox, multiselect or multi_select_countries field that posts nothing still counts as cleared, because WooCommerce prints no hidden input for them; any other absent field returns null, which keeps its stored value. Regression rows: an unposted select and text field keep their values, and cleared multiselect and multi_select_countries fields without validate store an empty list. The select row failed against the previous code. A textarea row is left out, because WooCommerce's own save raises a deprecation for an absent textarea, passing null to trim(), before the bridge runs. Assisted-by: Claude Code:claude-opus-5-5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The woocommerce package had no code. A plugin could not refuse to start on a missing or outdated WooCommerce, log through WooCommerce's log under its own source, remove its order data on uninstall, or add WooCommerce settings whose fields share the framework's field contract.
WooCommerceRequirementchecks the entry file'sWC requires at least. When WooCommerce is missing, users who can activate plugins see WordPress core's dependency message; when it is too old, they see "%1$s needs WooCommerce %2$s or later, so its features stay off until WooCommerce is updated."WooCommerceLoggeris a PSR-3 logger over WooCommerce's log under the plugin's source, resolved on the first record so construction never needs WooCommerce. A threshold replaces the WP_DEBUG default. The context reaches WooCommerce's file format safely: a Throwable as a string, and any value JSON cannot encode as its type. Errors WooCommerce drops also go to the PHP error log.uninstall.phploadsWooCommerceUninstallerby path, parseable on PHP 7.4 and outside every autoload key. A plugin's own uninstall file passesWooCommerceUninstaller::get_handlers()to the bootstrap package'sUninstaller::run(), which then removes the footprint'sorder_meta,order_meta_prefixesandorder_item_meta:wc_orders_metaand the order item meta table, byte for byte, so another plugin's letter-case twin survives;Settings\SettingsTab(built inwoocommerce_get_settings_pages) andSettings\SettingsSection(registered through WooCommerce's section registry) hold framework field arrays. Each owns its section's fields as WooCommerce's settings filter leaves them, contributed fields included, and on save:validate, also over WC REST, where WooCommerce has already normalized it;rendercallable, and the framework'scheckboxestype, render in WooCommerce's row throughForms::render_control()and save throughForms.phpstan.woocommerce.neonanalyzes the package'ssrc/. The uninstall files join the bootstrap package inphpstan.bootstrap.neon(PHP 7.4, WordPress 5.2) and in the PHP 7.4 syntax lane. AGENTS.md records the layout, limitations 6 and 7 for WooCommerce settings screens, and the rule for a larger internal that several classes share.Affected packages
wp-framework-bootstrapwp-framework-sharedwp-framework-corewp-framework-settingswp-framework-woocommerceChecklist
tests/Integration/WooCommerce/holds 102 rows in the WooCommerce environment and 3 in thewithout-woocommercegroup. Settings rows post through WooCommerce's own settings screen, each request in its own PHP process, and through WC REST. Uninstall rows also run in a request with WooCommerce deactivated.PackageManifestsTestchecks that every autoloaded package maps only its namespace tosrc/.composer quality-checkpasses locally (Unit 204, Integration 787), pluscomposer test:integration:multisite(29) and PHP 7.4php -loverpackages/bootstrapand the woocommerce uninstall filesREADME.md(the package READMEs stay pointer-form; AGENTS.md carries the file map)