Context
Classes/Controller/Backend/TaskController.php has grown to 920
lines carrying eleven public actions, nine private helpers, and three
distinct user-facing pathways:
- List / catalog —
listAction().
- AI wizard (create a Task from a natural-language description) —
wizardFormAction(), wizardGenerateAction(),
wizardGenerateChainAction(), wizardCreateAction().
- Execution (run a stored Task with various input sources) —
executeFormAction(), executeAction(),
refreshInputAction().
- Record picking (browse DB tables to source Task input from a
record) —
listTablesAction(), fetchRecordsAction(),
loadRecordDataAction().
The 2026-04 architecture audit — generated locally and kept under the
gitignored claudedocs/ directory rather than checked in (the
codebase intentionally excludes Claude Code working notes from version
control via .gitignore) — flagged three concrete problems with the
controller as it stands:
- Inline SQL. Eight call sites use
ConnectionPool /
QueryBuilder directly to query sys_log, the picked
record's table, and so on. Repository layer is bypassed.
- Inconsistent response shape. Most backend controllers return
typed
Response/* DTOs (ToggleActiveResponse,
TestConfigurationResponse, etc.) — see ADR-024 widget pattern
and the ConfigurationController precedent. TaskController's
AJAX actions instead return raw new JsonResponse(['success'
=> …, 'error' => …]) literals at sixteen call sites.
- God-class scope. Three independent user pathways (catalog,
wizard, execution + record picking) sharing one class makes
navigation, testability, and per-feature ownership harder than it
needs to be.
Adding any of the planned follow-ups — pre-flight budget gating in the
execute flow (REC #4), a typed exception layer for execute errors
(REC #8), domain-JSON-to-DTO promotion for Task::getInputConfig()
(REC #6) — would each make this class even larger.
The audit explicitly noted that REC #5 should ship behind an ADR
because the change touches backend module routing, the AJAX URL surface
JavaScript depends on, and the boundary between controllers and the
service layer.
Decision
We will adopt a hybrid split: per-pathway controllers + service
extraction + uniform typed responses. Concretely:
Per-pathway controllers
The eleven public actions move into four focused controllers, each
sharing the same dependency-injection patterns we already use for
ConfigurationController / ProviderController /
ModelController:
Controller/Backend/TaskListController — listAction
only.
Controller/Backend/TaskWizardController — the four wizard
actions.
Controller/Backend/TaskExecutionController —
executeFormAction, executeAction,
refreshInputAction.
Controller/Backend/TaskRecordsController —
listTablesAction, fetchRecordsAction,
loadRecordDataAction.
Each controller is #[AsController] and remains thin: parse the
request DTO, delegate to a service, return a typed response.
Repository layer
Inline SQL moves to repository methods on two repositories:
Domain/Repository/TaskRepository gains
fetchSampleRecords(string $table, ...) and
loadRecordRow(string $table, int $uid) for the picker
controller.
- The
sys_log and deprecation-log reads (which are
TYPO3-internal, not Task-domain) move into a small
Service/Task/TaskInputResolver collaborator that wraps the
appropriate ConnectionPool /
Filesystem calls in named methods, then is exposed via an
interface so tests can stub it.
Typed response normalization
Every AJAX action returns a typed Response/* DTO. Five new ones
are introduced where no existing match is good enough:
Response/TableListResponse (record picker — table dropdown).
Response/RecordListResponse (record picker — row results).
Response/RecordDataResponse (record picker — single row
payload).
Response/TaskExecutionResponse (execute success).
Response/TaskInputResponse (refresh-input result).
Existing ErrorResponse covers every error branch; raw
new JsonResponse(['success' => false, ...]) calls go away.