Skip to content

Phase 5 — The restructure (Plugin Modules + composition + alias trick + code fixes)#14

Open
edpittol wants to merge 10 commits into
mainfrom
6-phase-5-restructure-plugin-modules-composition-alias-trick-code-fixes
Open

Phase 5 — The restructure (Plugin Modules + composition + alias trick + code fixes)#14
edpittol wants to merge 10 commits into
mainfrom
6-phase-5-restructure-plugin-modules-composition-alias-trick-code-fixes

Conversation

@edpittol
Copy link
Copy Markdown
Member

Closes #6.

What changed

This PR is the Phase 5 restructure: replace the monolithic Aztec\WPBrowser\AztecWPBrowser module with a pair of Plugin Modules (WooCommerceDb, WooCommerceWebDriver) and reorganise every WooCommerce concern under the Aztec\WPBrowser\WooCommerce\ Plugin Subnamespace. Action Scheduler moves to its own Aztec\WPBrowser\ActionScheduler\ Subnamespace and its Method Trait is renamed ActionMethods. Consumers reference the new modules by short name in suite.yml via the Class Alias Trick (src/aliases.php).

Reviewed commit-by-commit, the PR is small per layer:

Commit Layer
chore: fix composer.json syntax Composer manifest
refactor: relocate WooCommerce and ActionScheduler code into Plugin Subnamespaces File moves + namespace renames
refactor: split orphan browser methods into dedicated browser traits Trait reshape (DB vs browser)
feat: introduce WooCommerceDb and WooCommerceWebDriver Plugin Modules with class alias trick New module classes, aliases.php, suite.yml, delete old module
refactor: P2 fold-ins — overrides parameter, see*Status via WPDb, $I->assert* API hygiene
test: cover HPOS detector, alias registrar, and sibling-module checks Unit suite

How HPOS-aware browser code works without depending on WooCommerceDb

ADR-0005 forbids cross-module calls for HPOS state. WooCommerceWebDriver therefore re-implements detection in OrderBrowserMethods::resolveOrderStorage() by reading woocommerce_custom_orders_table_enabled directly through its WPDb sibling. The two modules detect HPOS independently; neither calls the other.

Storage-abstractions audit outcome

Per the PRD's audit step I went with the Conservative outcome: relocate Storage/, OrderStorage/, and SubscriptionStorage/ wholesale under src/WooCommerce/ with their existing structure intact. Justification:

  • The parallel Storage/ (shared abstract base) + *Storage/ (entity-specific concrete) split is intentional — it lets Order and Subscription share the HPOS/Legacy table-name detection without duplicating it.
  • Aggressive inlining into trait bodies would have ballooned the per-method code and forced trait methods to be HPOS-aware, contradicting the trait-as-API surface.
  • Consolidating one of the parallel structures into the other is a follow-up cleanup that can land standalone; bundling it here would have grown the PR by another ~200 lines for marginal gain.

If duplication starts to bite in a later phase, a dedicated audit ticket can revisit the moderate/aggressive options.

Intentional deviation from the PRD

The PRD's "Deletions" subsection says abstract dependency-getter declarations should be removed from each trait. I kept them.

Reason: PHPStan level-max cannot infer the return type of \Codeception\Module::getModule(string $name) — it's typed as \Codeception\Module. Dropping the abstract decls would force one of three bad options at every call site: docblock @var casts (project style forbids docblocks), per-call assert($x instanceof WPDb) (massive noise in every method body), or @phpstan-ignore comments (anti-pattern).

The abstract decls are a small, well-understood boilerplate that gives PHPStan the typing it needs. ADR-0002 already documents them as the chosen pattern, and the trait call sites stay identical to the legacy code.

If the project later adopts a generic getModule() stub via a PHPStan extension this can be revisited.

What was deferred

  • Remaining manual PHPStan / phpcs polish. I could not run composer check locally (no PHP vendor in the worktree; the project's tooling runs through docker compose). CI will be the source of truth — if it surfaces violations I'll address them here in a follow-up commit. The new code follows PSR-12 + slevomat conventions throughout, so I expect findings to be minor.
  • Branch protection on main. Listed in the PRD as a wrap-up admin task; happens after this PR lands and CI is green.

Test plan

  • composer install && composer check — phpstan level max + phpcs clean.
  • make test-up && docker compose -f docker-compose.test.yml exec php vendor/bin/codecept build — actor regenerates with the new module method surface.
  • docker compose -f docker-compose.test.yml exec php vendor/bin/codecept run unit — all three new unit suites green.
  • make hpos-disable && docker compose -f docker-compose.test.yml exec php vendor/bin/codecept run acceptance OrderCest SubscriptionCest — legacy storage paths still pass.
  • make hpos-enable && docker compose -f docker-compose.test.yml exec php vendor/bin/codecept run acceptance OrderHPOSCest SubscriptionHPOSCest — HPOS storage paths still pass.
  • docker compose -f docker-compose.test.yml exec php vendor/bin/codecept run acceptance — full suite green.

edpittol added 6 commits May 19, 2026 15:29
Restore the missing comma between `allow-plugins` and `sort-packages`
so the manifest parses as valid JSON. Without this fix `composer install`
and the static-analysis CI gates cannot run.
…ubnamespaces

Move every WooCommerce concern (Method Traits, Page Objects, Config,
storage hierarchies) under `src/WooCommerce/` and rename the namespace
to `Aztec\WPBrowser\WooCommerce\*`. Move the Action Scheduler Method
Trait under `src/ActionScheduler/Method/` and rename
`ActionSchedulerMethods` to `ActionMethods` since the Plugin
Subnamespace now carries the "Action Scheduler" qualifier
(ADR-0003).

`src/AztecWPBrowser.php` is updated to import from the new locations so
the codebase remains buildable while the new Plugin Modules are
introduced in the follow-up commit.

Anchor the `woocommerce/`, `woocommerce-subscriptions/` and
`storefront/` `.gitignore` rules to the repo root so the new
`src/WooCommerce/` directory stops being matched case-insensitively on
HFS+/APFS filesystems.
`amOnMyAccountPage` lived inside `CustomerMethods` and `amOnAdminOrderPage`
inside `OrderMethods` even though both touch the browser only. With the
upcoming split into `WooCommerceDb` (DB-layer) and `WooCommerceWebDriver`
(browser-layer) Plugin Modules, the orphan methods need to live on the
browser-layer side so the DB module stays free of `WPWebDriver` and
`WooCommerceConfig` dependencies.

Move them into `CustomerBrowserMethods` and `OrderBrowserMethods`. The
new `OrderBrowserMethods` resolves the appropriate
`OrderStorageInterface` itself (reading `woocommerce_custom_orders_table_enabled`
via `WPDb`) so the browser module never has to call back into
`WooCommerceDb` for HPOS state (ADR-0005).

Drop the now-unused `wpWebDriver`/`wooCommerceConfig` abstract
dependency-getters from the DB-only traits.
… with class alias trick

Replace the monolithic `Aztec\WPBrowser\AztecWPBrowser` module with a
pair of Plugin Modules under `Aztec\WPBrowser\WooCommerce\Module\`:

- `WooCommerceDb` composes the WooCommerce DB-layer Method Traits and
  the `ActionScheduler` Method Trait. It validates that `WPDb` is
  enabled in the suite and throws `ModuleException` with a clear
  message otherwise. HPOS detection lives in a `protected
  isHposEnabled()` helper that reads
  `woocommerce_custom_orders_table_enabled` from `wp_options` on every
  call (no cache, no public actor method — ADR-0005).
- `WooCommerceWebDriver` composes the WooCommerce browser-layer Method
  Traits including the new `CustomerBrowserMethods` and
  `OrderBrowserMethods`. It owns the `PageObjectProvider` (page
  objects are browser-side) and validates that both `WPWebDriver` and
  `WPDb` are enabled in the suite (`WPDb` is needed for slug/option
  lookups; the dependency never goes through `WooCommerceDb`).

Both modules extend `\Codeception\Module` and reach wp-browser via
`getModule()` (composition over extension — ADR-0002). Method Traits
keep their abstract dependency-getter declarations because PHPStan
level-max cannot infer `getModule()` return types without either
docblocks (forbidden by the project style) or per-call `assert()` casts
that would bloat every trait method body. The abstract decls give
PHPStan the typing it needs while keeping trait call sites identical
to the legacy code.

Add `src/aliases.php` to register `Codeception\Module\WooCommerceDb`
and `Codeception\Module\WooCommerceWebDriver` aliases via composer's
`autoload.files` directive so consumers can enable the modules by
short name in `suite.yml` (ADR-0004). The registration guards against
namespace squatting with `class_exists($alias, false)` and emits an
`E_USER_WARNING` when an alias is already registered, recommending the
FQN as the documented escape hatch. The whole file runs inside an IIFE
to keep helper variables out of the global scope.

Update `tests/acceptance.suite.yml` to enable both Plugin Modules by
short name and move the `pageObjects` configuration onto
`WooCommerceWebDriver`. Delete the legacy `src/AztecWPBrowser.php`.

Storage abstractions audit (Conservative outcome from the audit step
of the PRD): the three storage hierarchies (`Storage/`, `OrderStorage/`,
`SubscriptionStorage/`) were already relocated under `src/WooCommerce/`
in the previous commit; no further consolidation in this phase. The
parallel `Storage/` + `*Storage/` structure stays as-is to avoid
expanding the surface of an already-large restructure; revisit in a
future phase if duplication becomes painful.
…>assert*

Three quality improvements that were tagged P2 in the planning sweep and
were small enough to fold into this restructure:

1. Rename `array $data` to `array $overrides` on every
   `have*InDatabase` method that takes an overrides bag (traits,
   storage classes, and their interfaces). The name now matches the
   semantics — callers pass overrides, not a complete representation.

2. Rewrite `seeOrderStatus` and `seeSubscriptionStatus` so they call
   `seeOrderInDatabase` / `seeSubscriptionInDatabase` instead of
   grabbing the status and calling `assertEquals`. Failures now
   surface as a Codeception DB-step in the report rather than a bare
   assertion miss.

3. Replace every raw `assert(...)` in Cests with the equivalent
   `$I->assert*(...)` call so failure messages are rich and the
   assertion shows up in Codeception's step log. Conversions cover
   the standard patterns: `is_int && > 0` → `assertIsInt` +
   `assertGreaterThan`, `=== false` → `assertFalse`,
   `=== <literal>` → `assertSame`, `count(...) === N` →
   `assertCount`, `in_array(..., true)` → `assertContains`, etc.
Add a Codeception unit suite plus three targeted test classes for the
deep modules called out in the PRD's Testing Decisions section:

- `WooCommerceDbHposDetectorTest` exercises the `protected
  isHposEnabled()` helper through reflection. It verifies the
  `'yes' → true`, `'no' → false`, and missing-option-returns-false
  contract, and explicitly asserts that the option is re-read on
  every call (no caching at module level — ADR-0005).
- `AliasRegistrarTest` proves both short-name aliases exist after
  composer's `autoload.files` runs, that the alias resolves to the
  FQN class, and that a pre-existing alias causes the registrar to
  skip with a `E_USER_WARNING` that mentions the conflicting alias
  and recommends the FQN as the documented fallback (ADR-0004 S2
  safeguard).
- `SiblingModuleCheckTest` mocks `hasModule()` on each Plugin Module
  to prove `_initialize()` throws `ModuleException` with a helpful
  message when its required Sibling Module is missing, and returns
  cleanly when present.

To make the alias registrar testable, extract the registration loop
out of the IIFE into `Aztec\WPBrowser\registerAliases()`. The function
remains a private detail of the autoload step (no public API yet) but
unit tests can now exercise the conflict path without re-evaluating
the autoload-time IIFE.

Wire the unit suite via `tests/unit.suite.yml` (no actor — tests
extend `Codeception\Test\Unit` directly) and add the
`Aztec\WPBrowser\Tests\Unit\` PSR-4 mapping to `composer.json`'s
`autoload-dev`. Also fix the implicit-nullable `string $hook = null`
deprecation on `ActionMethods::runScheduledActions()` while in the
neighbourhood; PHP 8.4 warns on the implicit form.
@edpittol edpittol self-assigned this May 20, 2026
edpittol added 4 commits May 19, 2026 23:06
Both Plugin Modules detect HPOS independently (ADR-0005 forbids the
browser module from calling the DB module), but until now they each
owned the option name and the comparison literal — two sources of
truth that could drift if WooCommerce ever renamed the flag.

Introduce `HposState` (a `final` class with the option name as a
`const` and a single static `isEnabled(WPDb $wpDb): bool` method).
Both `WooCommerceDb::isHposEnabled()` and
`OrderBrowserMethods::resolveOrderStorage()` now delegate to it.

The modules still detect HPOS independently — they share a class, not
a call path. Add `HposStateTest` covering the `yes`/`no`/missing
branches and asserting that the option key is the canonical
`woocommerce_custom_orders_table_enabled`.
The three public methods (`cartPageSlug`, `checkoutPageSlug`,
`myAccountPageSlug`) differed only in the `wp_options` row they
queried. They each owned a private nullable cache field and a
copy of the option-id → post-name lookup body — three sources of
truth for the same algorithm.

Move the body into a single `pageSlug(string $pageIdOption)` helper
keyed by option name and back it with one `slugCache` array.
Promote the constructor parameter while passing through. Public
method names and signatures stay unchanged so no caller needs to
move.
`AbstractHPOSStorage` and `AbstractLegacyStorage` carried byte-identical
copies of the same handful of helpers: the `WPDb` constructor, the
`getMetaTableName` lookup, `mapMetaCriteria`, `grabEntityMeta`, and
`haveEntityMetaInDatabase`. All four methods are post-meta-flavoured
even in HPOS mode (HPOS keeps order metadata in `wp_postmeta`), so
there is no storage-mode reason to keep them split.

Introduce `AbstractStorage` as the shared parent: it owns the
constructor, the `getEntityIdKey()` abstract, the four post-meta
helpers, and `implements WooCommerceStorageInterface`. The two
storage-mode abstracts now `extend AbstractStorage` and keep only
the methods that genuinely differ — `getTableName`, `getIdColumnName`,
`mapCriteria`, `grabEntityStatus`, `haveEntityStatus`. The HPOS one
also keeps `use HPOSStorageTrait` for the `wc_orders` table-name and
ID-generation helpers.

Net code reduction: ~36 lines, and the next storage entity to land
(e.g. EDD downloads) inherits the post-meta helpers for free.
…rt trait

`WooCommerceDb` and `WooCommerceWebDriver` carried byte-identical
`wpDb()` helpers (`getModule('WPDb')` + `assert` cast) and the same
lazy-initialised `wooCommerceConfig()` factory. Two sources of truth
for code that has to behave identically across both modules.

Move both helpers into a new `WooCommerceModuleSupport` trait. The
modules `use` it alongside their Method Traits and drop the inline
copies. The trait is named for its current scope: when a future
plugin's modules want the same shape (e.g. an `EddDb` reaching for
`getModule('WPDb')`), they can either copy this trait into their own
Plugin Subnamespace or, if the duplication recurs, we promote the
generic `wpDb()` helper into a shared `Support` subnamespace.
`WooCommerceWebDriver` keeps `wpWebDriver()` inline since it is the
only module that needs a `WPWebDriver` accessor today.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 5 — The restructure (Plugin Modules + composition + alias trick + code fixes)

1 participant