Tighten DDD layering: pure Domain, injected renderers, trimmed EntryOperations#874
Open
Tighten DDD layering: pure Domain, injected renderers, trimmed EntryOperations#874
Conversation
LiveblogPost calls update_post_meta, delete_post_meta, post_type_supports, current_user_can, do_action and apply_filters directly. That makes it a WordPress-aware aggregate, not the persistence-agnostic entity that the Domain layer is supposed to hold. Move it to Application/Aggregate/, where calling WordPress functions is appropriate. The Domain layer now contains only Entry, the value objects and EntryRepositoryInterface, all of which are genuinely free of WordPress dependencies. The class itself is unchanged; only its namespace and the imports that reference it are updated. This is the lighter of two options. A heavier alternative would extract a LiveblogPostRepository so the aggregate held only state, but that is a larger refactor with no current query need driving it.
Two related changes that together stop the Application layer from reaching back into the Infrastructure container at runtime, and untangle the cycle between EntryService and KeyEventService. EntryPresenter, KeyEventShortcodeHandler and LazyloadConfiguration each called Container::instance() inside Application code to fetch the template or content renderer. That inverted the dependency direction (Application reaching into Infrastructure) and made dependencies implicit at construction time. Each now declares the renderer it needs as a constructor parameter, with the wiring done once in Container or PluginBootstrapper. A new Application\Renderer\TemplateRendererInterface mirrors the existing ContentRendererInterface so Application code never imports the concrete TemplateRenderer from Infrastructure. EntryPresenter loses an unreachable render() method in passing. EntryService had an optional KeyEventService constructor parameter with a lazy fallback that constructed a fresh KeyEventService (and a fresh EntryQueryService) on demand. That was the classic shape of a circular dependency: KeyEventService also wanted EntryService in some flows, and the lazy create silently bypassed any test double a caller had wired up. The only path that needed it was EntryService::delete_key, which is called exclusively from EntryOperations::execute_delete_key. The orchestration moves up to that caller, which already has both services injected, so EntryService now has a single repository-only constructor and the cycle is gone.
Call-site mapping showed seven public methods on EntryOperations (insert, update, delete, is_key_event, remove_key_action, plus entry_service() and key_event_service() accessors) had no callers outside the class itself. They were leftover pass-throughs from an earlier refactor that had been documented as a facade for legacy container access. Anyone now reading the class is forced to pattern match across four similar-sounding services to find which one a method actually lives on. Remove the dead methods and rewrite the class docblock to describe what the class actually does: it dispatches HTTP CRUD action strings to the appropriate flow, applies the liveblog_before_*_entry filters and liveblog_*_entry actions around each mutation, and prepares the JSON payload that REST and admin-ajax callers return to the client. The public surface is now do_crud, format_preview and is_valid_action plus the private execute helpers, which matches what RestApiController and RequestRouter actually use.
Integration tests caught a regression: the previous commit gave LazyloadConfiguration a required TemplateRendererInterface constructor parameter, but six other callers (RequestRouter, AssetManager, AmpIntegration, the liveblog-loop template) instantiate it directly with `new LazyloadConfiguration()` for read-only config access. Forcing the renderer on those callers would have spread an admin-only, edge-case dependency through the read path. The renderer was only needed for one method, render_deprecated_plugin_notice, which surfaces an admin notice when the obsolete standalone Lazyload Liveblog Entries plugin is active. That logic doesn't belong inside a config reader anyway, so it moves up to PluginBootstrapper alongside the other one-time hook wiring. PluginBootstrapper already has the template renderer via the container. LazyloadConfiguration is now a plain stateless config reader again.
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
Architectural follow-up against
2.xafter reviewing the DDD refactor. Three concerns surfaced from the review and are addressed here in three commits, each independently green.LiveblogPostlived underDomain/Entity/but calledupdate_post_meta,delete_post_meta,post_type_supports,current_user_can,do_actionandapply_filtersdirectly. That made it a WordPress-aware aggregate, not a persistence-agnostic domain entity. Moving it toApplication/Aggregate/matches what the class actually is, and leaves theDomainlayer holding onlyEntry, the value objects andEntryRepositoryInterface— all of which are genuinely free of WordPress dependencies. No behaviour change; only the namespace and 28 imports move with it.Three Application-layer classes (
EntryPresenter,KeyEventShortcodeHandler,LazyloadConfiguration) reached back intoContainer::instance()at runtime to fetch a renderer. That inverts the dependency direction and hides the dependency from the constructor. Each one now declares the renderer it needs as a constructor parameter, with the wiring done once inContainerorPluginBootstrapper. A newApplication\Renderer\TemplateRendererInterfacemirrors the existingContentRendererInterface, so Application code never imports the concreteTemplateRendererfromInfrastructure.EntryPresenterloses an unreachablerender()method in passing.The same commit also untangles a circular dependency:
EntryServiceaccepted an optionalKeyEventServiceconstructor argument with a lazy fallback that constructed a freshKeyEventServiceandEntryQueryServiceon demand. The lazy create silently bypassed any test double a caller had wired up. The only path that needed it —delete_key— is called exclusively fromEntryOperations::execute_delete_key, which already has both services injected. Moving the two-step orchestration up to that caller letsEntryServicecollapse to a single repository-only constructor, and the cycle is gone.Finally,
EntryOperationshad seven public pass-through methods (insert,update,delete,is_key_event,remove_key_actionplusentry_service()andkey_event_service()accessors) that call-site mapping showed have no external callers. They were leftover scaffolding from an earlier refactor, documented as a facade for legacy container access that no longer exists. Removing them, together with rewriting the class docblock to describe what the class actually does — dispatch CRUD action strings, apply theliveblog_*_entryfilters and actions around each mutation, and prepare the JSON payload for REST and admin-ajax callers — leaves a clear public surface ofdo_crud,format_previewandis_valid_action.What this PR does not do: replace the hand-rolled service-locator container, collapse the single-implementation
ContentRendererInterfaceandEmbedHandlerInterface, or extract aLiveblogPostRepository. Those were considered and judged either out of scope or actively unhelpful at the current size of the codebase.Test plan
composer install && vendor/bin/phpunit --testsuite unit— 190 tests, all green at HEAD and at each intermediate commit.vendor/bin/phpcs src/php --standard=.phpcs.xml.dist— clean.