refactor: split eight PHP god-objects (>1000 LOC each) into focused responsibilities (#141) - #154
Merged
Merged
Conversation
Pure code movement — no behavior changes.
class-ffc-recruitment-notice-edit-page.php drops from 1727 to 291 LOC.
The orchestrator class keeps register(), handle_download_csv_example(),
render(), handle_save(), handle_transition(), back_url(), and
redirect_with_notice(). The 5 section-render calls in render() and the
columns_label_map() call in handle_save() now delegate to
RecruitmentNoticeEditPageRenderer.
Two new files:
- class-ffc-recruitment-notice-edit-page-renderer.php (1330 LOC)
All render_*, columns_label_map, preview_status_label_map, cls_button,
lookup_map, transitions_from, and the two inline-script emitters.
The 6 methods called from the EditPage facade are public; the rest
stay private (only sibling-renderer callers).
- class-ffc-recruitment-classification-filter-manager.php (169 LOC)
read_classification_filters() → read_filters() and
apply_classification_filters() → apply_filters(). Bodies byte-
identical; only the names changed per the issue spec.
PSR-4 autoloader resolves both new class names from kebab-case
filenames automatically — no loader/composer changes needed.
Verification:
- php -l: clean on all 3 files.
- vendor/bin/phpunit --filter Recruitment: 227/227 OK.
- vendor/bin/phpstan analyse <3 files> --level=8: no errors.
- Re-ran phpstan over includes/recruitment/ (34 files): no errors.
https://claude.ai/code/session_01HiExaniSqvNBLpVTszCLNx
… controllers (S2 of #141) Pure code movement — no runtime behavior changes. The single RecruitmentRestController (1456 LOC) is replaced by four focused domain controllers + a shared support trait. New controllers (each with its own register_routes()): - RecruitmentNoticesRestController — 179 LOC, 3 endpoints (notices CRUD: list, create, update) - RecruitmentClassificationsRestController — 509 LOC, 9 endpoints (list, import, promote-preview, call, bulk-call, change status, change preview status, delete, cancel-call) - RecruitmentAdjutanciesRestController — 420 LOC, 10 endpoints (adjutancy CRUD, notice attach/detach, reasons CRUD — Reasons folded here since both are catalog domains and the issue spec names only 4 controllers) - RecruitmentCandidatesRestController — 344 LOC, 5 endpoints (candidates CRUD + me/recruitment) Shared trait `RecruitmentRestSupport` (164 LOC) holds the 6 cap-check methods and the 2 wp_error envelope helpers used by all controllers. The loader (`RecruitmentLoader`) now instantiates and registers all four controllers on `rest_api_init`. The original `class-ffc-recruitment-rest-controller.php` is deleted (no class_alias — user explicitly approved no aliases for this refactor). Tests: `tests/Unit/RecruitmentRestControllerTest.php` updated to instantiate the new controllers — cap-check tests use RecruitmentNoticesRestController (any controller works since they all `use` the trait); the route-registration tests aggregate register_routes() from all four into a single accumulator and run the original spot-checks unchanged (the union of registered routes is identical to before). Verification: - php -l: clean on all 5 new files + loader + test. - vendor/bin/phpunit --filter Recruitment: 227/227 OK. - vendor/bin/phpstan analyse includes/recruitment/ --level=8: no errors. https://claude.ai/code/session_01HiExaniSqvNBLpVTszCLNx
…classes (S3 of #141) Pure code movement — no behavior changes. The single FormEditorMetaboxRenderer (1245 LOC) becomes a thin facade (99 LOC) that delegates each metabox to its own class. Nine new per-metabox classes (one render(WP_Post $post) method each): - FormEditorShortcodeMetabox 52 LOC - FormEditorLayoutMetabox 98 LOC - FormEditorBuilderMetabox 201 LOC (incl. render_field_row helper) - FormEditorRestrictionMetabox 152 LOC - FormEditorEmailMetabox 89 LOC - FormEditorGeofenceMetabox 329 LOC (incl. inline toggleGeoSource JS) - FormEditorQuizMetabox 87 LOC - FormEditorPublicCsvDownloadMetabox 300 LOC - FormEditorDeviceLimitMetabox 151 LOC The facade `FormEditorMetaboxRenderer` keeps its public API intact (render_box_*, render_shortcode_metabox, render_field_row) so existing call sites in `FormEditor::add_custom_metaboxes()` and the unit test work unchanged. Each public method is now a one-line forwarder. PSR-4 autoloader resolves all 9 new class names from kebab-case filenames automatically — no loader/composer changes needed. Verification: - php -l: clean on all 10 files. - vendor/bin/phpunit --filter FormEditorMetaboxRenderer|FormEditorSaveHandler: 29/29 OK. - vendor/bin/phpstan analyse includes/admin/ --level=8: no errors. https://claude.ai/code/session_01HiExaniSqvNBLpVTszCLNx
…#141) Pure code movement — no behavior changes. The single `RateLimiter` (1178 LOC) becomes a thin facade (213 LOC) that forwards every public static call to one of three focused classes. - RateLimitChecker (1073 LOC) — get_settings, check_all, check_* (ip/email/cpf/global/device/verification/user_limit), record_*, plus all internal helpers (counters, blacklist, whitelist, temporary blocks, format/window utilities, get_user_ip). - RateLimitLogger (99 LOC) — log_attempt, cleanup_old_logs (private), cleanup_expired (cron entry point). - RateLimitStats (44 LOC) — get_stats aggregation. Cross-class adjustments (no behavior change): - 9 `self::log_attempt(...)` call sites in `check_all` and `check_verification` rewritten to `RateLimitLogger::log_attempt(...)`. - `get_user_ip()` promoted from private to public so `RateLimitLogger::log_attempt` can call it as `RateLimitChecker::get_user_ip()` (alternative would have been to duplicate the helper, violating the no-logic-changes rule). - `CACHE_GROUP` constant kept on the facade — many external callers reference `RateLimiter::CACHE_GROUP` directly. Methods inside the new classes use `RateLimiter::CACHE_GROUP` (a deliberate one-way dependency that preserves the public API). Test harness updated (4 files): the reflection-based `$settings_cache` reset in setUp() now targets `RateLimitChecker` (the property's new home) instead of `RateLimiter`. Two of the test files used `hasProperty` guards that would have silently no-op'd after the move and leaked settings between tests — repointing the guard restores their original protective behavior. Public API of `RateLimiter` is byte-identical: every external caller (13 production files + tests) keeps working without any change. Deviation: `RateLimitChecker` is 1073 LOC, above the issue's <800-LOC target. The three-class split named in the issue spec (Checker / Logger / Stats) puts every `check_*` method in Checker by design; hitting <800 would require further sub-splitting (e.g. extracting Device-fingerprint or Blocklist sub-classes), which deviates from the explicit issue plan more than the LOC overrun does. Flagging here so this can be revisited if the LOC bar is the binding constraint. Verification: - php -l: clean on all 4 files. - vendor/bin/phpunit: 3870/3870 OK. - vendor/bin/phpstan analyse includes/security/: no errors. https://claude.ai/code/session_01HiExaniSqvNBLpVTszCLNx
Pure code movement — no behavior changes. PublicCsvDownload drops
from 1143 to 717 LOC; two new focused collaborators take the
extracted slices.
- CsvDownloadValidator (291 LOC) — validate_form_access,
validate_hash_only, validate_cpf_requirement, plus the private
record_download_log_entry helper they share.
- CsvDownloadFormInfoBuilder (277 LOC) — build_form_info plus its
private build_restrictions_info / build_datetime_info /
build_geolocation_info / format_locations_for_info /
build_quiz_info helpers.
The facade keeps its public API intact so external callers
(PublicCsvExporter, FormEditorPublicCsvDownloadMetabox, Frontend) and
the test (`$this->handler->validate_cpf_requirement(...)`) keep
working unchanged. Constructor wires two private properties
(`$validator`, `$form_info_builder`); the three validation methods
become one-line forwarders; `ajax_info()` calls
`$this->form_info_builder->build_form_info(...)` inline.
Constants stay on PublicCsvDownload (SHORTCODE, ACTION,
NONCE_ACTION, META_*, DOWNLOAD_LOG_MAX). Methods inside the new
classes reference them as fully-qualified `PublicCsvDownload::*`.
The static helpers `get_audit_log_summary`, `maybe_wipe_legacy_logs`,
and the private `decrypt_log_entry_cpf` stay on the facade — they
are not validation/info-builder logic.
Test update: two `\ReflectionMethod( PublicCsvDownload::class, ... )`
tests targeting the moved `build_datetime_info` private method now
reflect on `CsvDownloadFormInfoBuilder::class` — same coverage, new
home.
Verification:
- php -l: clean on all 3 files.
- vendor/bin/phpunit --filter PublicCsvDownload: 42/42 OK (119 assertions).
- vendor/bin/phpunit: 3870/3870 OK.
- vendor/bin/phpstan analyse includes/frontend/: no errors.
https://claude.ai/code/session_01HiExaniSqvNBLpVTszCLNx
…enderer (S6 of #141) Pure code movement — no behavior changes. Two focused collaborators extracted from ReregistrationAdmin (1132 LOC). - ReregistrationSubmissionDetailsRenderer (123 LOC) — owns build_submission_details_html, the modal HTML builder for the "View submission" AJAX endpoint. Promoted from private to public so the new AJAX handler can call it. - ReregistrationAjaxHandler (142 LOC) — owns the three wp_ajax_* callbacks (ajax_generate_ficha, ajax_view_submission_details, ajax_count_members). Composes the details renderer via constructor and stores it as a private property. The handler carries its own CAPABILITY constant; no facade helpers are needed because the bodies only touch WP globals + static repository/generator classes. The facade `ReregistrationAdmin::init()` now instantiates the AJAX handler and registers the three wp_ajax_* hooks against `array( $this->ajax_handler, '...' )` instead of `array( $this, ... )`. Public surface preserved: existing tests in `tests/Unit/ReregistrationAdminTest.php` (and any external caller) invoke `$admin->ajax_generate_ficha()` / `->ajax_count_members()` / `->ajax_view_submission_details()` directly on the facade. To honor that surface without keeping the bodies on the facade, added three one-line public delegators plus a `private get_ajax_handler()` that lazily instantiates the handler when the test calls a delegator before `init()` has run. Logic still lives entirely in `ReregistrationAjaxHandler`. Cleanup: removed a stale duplicate copy of `ajax_count_members()` that earlier work had left in the facade alongside the moved version. Deviation: facade is 1002 LOC (above the issue's <800 target). The issue spec explicitly named multiple large render_* methods (render_list, render_form, render_submissions, render_submission_row, render_audience_options, render_audience_transfer_list) plus handle_save/handle_delete to keep on the facade. Hitting <800 would require extracting render helpers the spec did not authorize. Net drop is ~130 LOC for the two extractions the spec did call out. Verification: - php -l: clean on all 3 files. - grep '$this->' on new files: 2 refs, both resolve internally. - vendor/bin/phpunit --filter Reregistration: 299/299 OK. - vendor/bin/phpunit: 3870/3870 OK. - vendor/bin/phpstan analyse includes/reregistration/: no errors. https://claude.ai/code/session_01HiExaniSqvNBLpVTszCLNx
Pure code movement — no behavior changes. RecruitmentPublicShortcode
drops from 1136 to 476 LOC after extracting all the badge/label/
format/layout helpers into a focused renderer.
- RecruitmentPublicShortcodeRenderer (692 LOC) — render_section,
render_row, render_filters_bar, render_subscription_filter_inputs,
render_name_search_input, render_adjutancy_filter_inputs,
render_pagination, parse_columns_config, wrap_with_banner,
decrypt_field, status_label, render_*_badge methods,
preview_status_label, format_date_br, format_time_hm, msg.
Visibility: 5 methods called from the facade (render_uncached, render)
are public — render_section, render_filters_bar, parse_columns_config,
wrap_with_banner, msg. The remaining 14 helpers stay private since
they're only called from sibling renderer methods.
Facade keeps the public API + cache/rate-limit infrastructure:
register, render, render_uncached, wrap_output, enqueue_public_css,
cache_key, invalidate_public_cache, check_rate_limit, client_ip, plus
all SHORTCODE_TAG / CACHE_PREFIX / RATE_PREFIX / CACHE_VERSION_OPTION
/ CACHE_DIRTY_ACTION constants. Inside render_uncached() and render(),
the 5 calls to moved helpers became
RecruitmentPublicShortcodeRenderer::method(...). Unused use imports
for BadgeHtml, DocumentFormatter, and Encryption migrated to the
renderer where they're now used.
Verification:
- php -l: clean on both files.
- grep self:: in facade: only references own methods/constants.
- vendor/bin/phpunit --filter RecruitmentPublicShortcode: 10/10 OK.
- vendor/bin/phpunit: 3870/3870 OK.
- vendor/bin/phpstan analyse includes/recruitment/: no errors.
https://claude.ai/code/session_01HiExaniSqvNBLpVTszCLNx
…tials (S8 of #141) Pure file-system reorganization — the rendered HTML is byte-identical. md5 of the rendered output stream is unchanged (17ec376b4ccef6aeba4b9fe94c977454). The 1474-LOC procedural view `ffc-tab-documentation.php` becomes an 83-LOC shell that emits the wrap div + the intro/TOC card inline, then requires 19 numbered partials in order from a new `includes/settings/views/documentation/` directory: 01-shortcodes.php (72) 11-ficha-pdf.php (140) 02-variables.php (101) 12-geofence-locations.php (50) 03-quiz-variables.php (51) 13-features.php (97) 04-appointment-variables.php(44) 14-security.php (57) 05-qr-code.php (66) 15-examples.php (102) 06-validation-url.php (49) 16-url-shortener.php (114) 07-html-styling.php (135) 17-hooks.php (176) 08-custom-fields.php (39) 18-troubleshooting.php (82) 09-audience-custom-fields.php(65) 19-rest-api-auth.php (127) 10-reregistration.php (109) Each partial has a standard preamble (`<?php` + docblock + ABSPATH guard + `?>`) followed by the section's `<div class="card">…</div>` block lifted byte-identical from the original. No class changes, no autoloader changes (procedural view). TabDocumentation::render() still does `include FFC_PLUGIN_DIR . 'includes/settings/views/ffc-tab-documentation.php'` exactly as before; the cascade of `require __DIR__ . '/documentation/NN-x.php'` inside the shell handles the rest. Verification: - php -l: clean on shell + all 19 partials. - md5 of rendered output: identical before/after. - vendor/bin/phpunit: 3870/3870 OK. https://claude.ai/code/session_01HiExaniSqvNBLpVTszCLNx
WPCS gating failed on the S3 facade (class-ffc-form-editor-metabox-renderer.php) because the 9 private properties wiring sub-renderers and the 10 thin forwarder methods lacked docblocks. Pure documentation fix — no code or behavior change. https://claude.ai/code/session_01HiExaniSqvNBLpVTszCLNx
WPCS gating still failed after the previous PHPDoc pass because @var/@PARAM tags alone don't satisfy "Missing short description in doc comment" — each property and forwarder needed a one-line summary above the tags. Pure documentation fix; no code change. https://claude.ai/code/session_01HiExaniSqvNBLpVTszCLNx
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.
Closes #141.
Summary
Eight commits — one per sprint — splitting eight PHP files (>1000 LOC each) into focused responsibilities. Pure code movement; no behavior changes at any commit. Each commit keeps tests + PHPStan green.
LOC overview (before → after, primary file)
recruitment/class-ffc-recruitment-notice-edit-page.phprecruitment/class-ffc-recruitment-rest-controller.php(deleted)admin/class-ffc-form-editor-metabox-renderer.php(facade)security/class-ffc-rate-limiter.php(facade)frontend/class-ffc-public-csv-download.php(facade)reregistration/class-ffc-reregistration-admin.php(facade)recruitment/class-ffc-recruitment-public-shortcode.php(facade)settings/views/ffc-tab-documentation.php(shell)Per-sprint changes
RecruitmentNoticeEditPageRenderer(1330 LOC) andRecruitmentClassificationFilterManager(169 LOC). Orchestrator keeps register / handle_save / handle_transition / etc.RecruitmentRestController. Replaced by 4 domain controllers (Notices 179, Classifications 509, Adjutancies 420 [incl. Reasons], Candidates 344) + sharedRecruitmentRestSupporttrait (164 LOC) for cap-checks andwp_error_*helpers. Loader registers each.FormEditor*Metabox) each with a singlerender(WP_Post $post). The builder class also exposesrender_field_rowfor the facade's helper forwarder.RateLimitChecker(1073, allcheck_*per spec),RateLimitLogger(99),RateLimitStats(44). 9self::log_attempt→RateLimitLogger::log_attempt.get_user_ippromoted to public so the logger can use it.CACHE_GROUPstays on the facade (used externally).CsvDownloadValidator(291) +CsvDownloadFormInfoBuilder(277). Facade constructor wires them as private properties; validation methods become one-line forwarders;ajax_info()calls the builder inline. Two reflection-based tests repointed to the new home.ReregistrationAjaxHandler(142) +ReregistrationSubmissionDetailsRenderer(123).init()registers the 3 wp_ajax callbacks against the handler instance; three thin public delegators on the facade preserve the test surface that calls AJAX methods directly onReregistrationAdmin.RecruitmentPublicShortcodeRenderer(692) owns all badge / label / format / section / row / pagination helpers. Facade keeps register / render / render_uncached / cache + rate-limit infra.ffc-tab-documentation.phpbecomes an 83-LOC shell that emits the wrap div + TOC card inline, thenrequires 19 numbered partials fromincludes/settings/views/documentation/. md5 of the rendered output is byte-identical before/after.Acceptance criteria
includes/<feature>/structure.check_*method in Checker by design. Hitting <800 would require sub-splitting (Device-fingerprint, Blocklist) the spec did not authorize.render_list,render_form,render_submissions,render_submission_row,render_audience_*,handle_save,handle_deleteto keep on the facade. Hitting <800 would require extracting render helpers the spec did not authorize.If those LOC bars are binding, the path forward is named in each deviation note.
Test plan
vendor/bin/phpunit— 3870 tests / 9810 assertions / 0 failures (verified after every commit).vendor/bin/phpstan analyse <touched dir>— no errors (verified per-sprint).17ec376b4ccef6aeba4b9fe94c977454) unchanged.https://claude.ai/code/session_01HiExaniSqvNBLpVTszCLNx
Generated by Claude Code