Skip to content

refactor: keep WordPress and the Infrastructure layer out of the domain - #523

Open
ogorzalka wants to merge 7 commits into
developfrom
refactor/pure-domain-layer
Open

ogorzalka wants to merge 7 commits into
developfrom
refactor/pure-domain-layer

Conversation

@ogorzalka

@ogorzalka ogorzalka commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Why

The new Pollora harness checks two rules on every ...\Domain namespace:

  • ARCH-01: domain code imports neither Illuminate\... nor any ...\Infrastructure\... or ...\UI\... namespace.
  • ARCH-02: domain code calls no global WordPress function (add_action, get_option, wp_*, __, esc_*, get_bloginfo…). PHP built-ins are fine.

The domain is what keeps Pollora portable. The same rules have to run inside Laravel, inside a plain WordPress through the upcoming mu-plugin, and in unit tests with no WordPress loaded. A WordPress call or an Infrastructure import in the domain ties all three to one host.

This PR clears every ARCH-02 offence and every ARCH-01 offence that can be fixed without changing a public signature.

What changed

Each offence moves behind a port. The port is an interface in the module's Domain, the implementation lives in the module's Infrastructure layer, and the module's existing service provider binds the two.

Domain file Offence Port introduced Adapter (bound in)
src/Theme/Domain/Models/Menus.php register_nav_menus() Theme\Domain\Contracts\ThemeFeatureRegistrarInterface Theme\Infrastructure\Services\WordPressThemeFeatureRegistrar (ThemeServiceProvider)
src/Theme/Domain/Models/Sidebar.php register_sidebar() same same
src/Theme/Domain/Models/ImageSize.php add_image_size() same same
src/Theme/Domain/Models/ThemeInitializer.php content_url() Theme\Domain\Contracts\ContentUrlResolverInterface Theme\Infrastructure\Services\WordPressContentUrlResolver (ThemeServiceProvider)
src/Theme/Domain/Models/ThemeInitializer.php imported Asset\Infrastructure\Services\AssetFile Theme\Domain\Contracts\ThemeAssetUrlResolverInterface Theme\Infrastructure\Services\ThemeAssetFileResolver (ThemeServiceProvider)
src/Dashboard/Domain/Services/SystemInfoCollector.php get_locale, get_bloginfo, is_multisite, get_option, get_post_type_object, get_taxonomy, wp_get_theme Dashboard\Domain\Contracts\WordPressEnvironmentInterface Dashboard\Infrastructure\Adapters\WordPressEnvironment (DashboardServiceProvider)
src/Taxonomy/Domain/Models/AbstractTaxonomy.php __() ×14 in getLabels() Taxonomy\Domain\Contracts\LabelTranslatorInterface Taxonomy\Infrastructure\Services\WordPressLabelTranslator (TaxonomyServiceProvider)
src/ThirdParty/WooCommerce/Domain/Services/WooCommerceService.php is_child_theme, get_template_directory, WC() ThirdParty\WooCommerce\Domain\Contracts\WooCommerceEnvironmentInterface the existing WordPressWooCommerceAdapter, which now implements it (WooCommerceServiceProvider)
src/VersionCheck/Domain/Contracts/VersionCheckerInterface.php imported Infrastructure\Services\PackagistVersionChecker (only for a @see) none needed: the import is gone and the docblock names the class in prose n/a

Public API is unchanged

  • Theme components (Menus, Sidebar, ImageSize, ThemeInitializer) already take a container. They get the new ports from it, the same way they already get Action, Filter and WordPressThemeInterface. Their constructors stay as they were.
  • SystemInfoCollector gets a trailing optional WordPressEnvironmentInterface $wordpress parameter. Its default, Dashboard\Domain\Support\UnavailableWordPressEnvironment, reports "not loaded", which is what the old function_exists() guards answered without WordPress.
  • WooCommerceService gets an optional constructor parameter. Its default, DefaultWooCommerceEnvironment, means no child theme and the woocommerce/ folder, which is what the old guards answered without WordPress. new WooCommerceService keeps working.
  • AbstractTaxonomy is extended by applications, and discovery instantiates subclasses without arguments, so constructor injection would break every subclass. The translator is set once, statically, by TaxonomyServiceProvider (AbstractTaxonomy::useLabelTranslator()). Without one, the label templates come back untranslated. The text domain stays pollora, so the msgids are unchanged and the existing .po catalogues still apply.

The behaviour of every adapter is the same code that used to run in the domain, function_exists() guards included.

Deliberately left out (follow-up)

These two files still break ARCH-01, and fixing them would change public API:

  • src/Modules/Domain/Models/ModuleCollection.php
  • src/Plugin/Domain/Support/PluginCollection.php

Both extend Illuminate\Support\Collection. Any project can call or type-hint the whole Collection API on them (map, filter, toArray, …). Removing the parent class, or swapping it for a domain collection, is a breaking change. It belongs in a major version, or behind a deprecation path that ships a domain collection type next to these two first.

Found while scanning, outside the brief (not changed here):

  • src/Theme/Domain/Models/ThemeInitializer.php still calls Laravel's base_path() helper.
  • src/Theme/Domain/Models/ThemeMetadata.php and src/Plugin/Domain/Models/PluginMetadata.php call app_path().
  • src/Modules/Domain/Models/AbstractModule.php calls data_get() / data_set(), and AbstractTaxonomy and SystemInfoCollector call class_basename().

These are Laravel global helpers, not WordPress functions and not use Illuminate\... imports, so neither ARCH-01 nor ARCH-02 flags them. They still tie the domain to Laravel, so they are worth their own pass.

How it was verified

  • Re-scanned every file under src/**/Domain/**:
    • no use Illuminate\... import except the two collections above;
    • no use Pollora\...\(Infrastructure|UI)\... import;
    • no call to a global WordPress function. The list of WordPress functions comes from php-stubs/wordpress-stubs, reading only top-level function declarations, as the harness's NoWordPressCallInDomainRule does.
  • New or updated Pest tests:
    • tests/Unit/Theme/Infrastructure/Services/WordPressThemePortsTest.php: the three Theme adapters call the right WordPress function or asset container.
    • tests/Unit/Theme/{Menus,Sidebar,ImageSize}Test.php: each component registers its configured features through the port.
    • tests/Unit/Theme/ThemeInitializerTest.php: the harness passes port doubles instead of stubbing content_url(). New cases cover a built asset URL and a resolver that throws.
    • tests/Unit/Dashboard/Infrastructure/WordPressEnvironmentTest.php: the adapter and the "unavailable" default. activeTheme() is tested in tests/Feature/Dashboard/WordPressEnvironmentThemeTest.php, and isMultisite() only on its not-loaded path. Once Brain Monkey defines wp_get_theme() or is_multisite(), the function stays defined for the rest of the process. PatternService's unit tests and the pollora/hook async runner both rely on those functions being undefined.
    • tests/Unit/Dashboard/SystemInfoCollectorTest.php: the collector reads the version, multisite flag, permalinks, labels, theme and locale from the port.
    • tests/Unit/Taxonomy/AbstractTaxonomyLabelsTest.php: untranslated fallback, translation in the pollora domain, the adapter, and the provider binding.
    • tests/Unit/Plugins/WooCommerce/...: the service on a port double and on its default environment, plus the provider binding the port to the WordPress adapter. WooCommerceService stays autowired (singleton(WooCommerceService::class)) and receives the bound port.
  • Local checks (Composer cannot install in the authoring environment): php -l and Pint (pint --test, repo pint.json) on every changed file, plus a rough PHPStan level 5 pass over the new and changed classes. The full suite (Pest on 8.3/8.4, Pint, PHPStan with Larastan, Rector, type coverage, skeleton install, E2E) runs in this PR's CI.
  • CI is green on the head commit: Tests PHP 8.3 and 8.4, Code Quality, Patches, Skeleton install, both E2E jobs, Upload Coverage, and Validate PR title.
  • Two commits on the branch temporarily wrote JUnit and coverage reports to GitHub annotations, because the job logs could not be fetched from the authoring environment. Both changes are reverted: phpunit.xml.dist and tests/bootstrap.php are identical to develop. Please squash-merge so they stay out of develop's history.

https://claude.ai/code/session_01B7X25PLDN5YXW2HX2YpYQi


Generated by Claude Code

Code in a Domain namespace now neither imports an Infrastructure namespace
(ARCH-01) nor calls a global WordPress function (ARCH-02). Each call moves
behind a port declared in the module's domain, implemented in its
Infrastructure layer and bound by its existing service provider:

- Theme: ThemeFeatureRegistrarInterface (register_nav_menus,
  register_sidebar, add_image_size), ContentUrlResolverInterface
  (content_url) and ThemeAssetUrlResolverInterface (AssetFile)
- Dashboard: WordPressEnvironmentInterface for SystemInfoCollector
- Taxonomy: LabelTranslatorInterface for AbstractTaxonomy's labels
- WooCommerce: WooCommerceEnvironmentInterface, implemented by the
  existing WordPressWooCommerceAdapter
- VersionCheck: the contract no longer imports its Packagist implementation

New constructor parameters are optional with a WordPress-free default, so
no public signature changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B7X25PLDN5YXW2HX2YpYQi
Brain Monkey leaves a function it defined declared for the rest of the
process, and PatternService's unit tests rely on wp_get_theme() being
undefined. The adapter's theme test moves to the Feature suite, where the
doctor checks already stub it. Also drop a type guard PHPStan reports as
always true, and cover the default WooCommerce environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B7X25PLDN5YXW2HX2YpYQi
…I debug

The phpunit/bootstrap change only reports failing tests as annotations so
they can be read without the job log; it will be reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B7X25PLDN5YXW2HX2YpYQi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B7X25PLDN5YXW2HX2YpYQi
get_option() is stubbed for every test by the suite, so the permalink test
overrides the stub instead of expecting the call. The provider test that
resolved WooCommerceService through the real adapter reached WC() and is
gone: WooCommerceService is now autowired onto the environment port, which
the remaining provider test checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B7X25PLDN5YXW2HX2YpYQi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B7X25PLDN5YXW2HX2YpYQi
Brain Monkey keeps a function it defined declared for the rest of the run,
and the async hook runner tests rely on is_multisite() being absent. The
adapter is covered on its WordPress-not-loaded path instead. Drops the
temporary CI debug output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B7X25PLDN5YXW2HX2YpYQi
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.

2 participants