refactor(action-context): remove unused fragment aggregator + building block

actionFragmentContext was built spec-driven in step 1 to handle the
anticipated drawer-fragment pattern: authorize + finder → status DTO.
The action-context rollout (steps 2-11) found that none of the three
real drawer fragments (users/view-fragment, addressbook/view-fragment,
helpdesk/ticket-fragment) match that pattern — all three delegate to
domain services that own their own status models. Step 11 declared
them structural exceptions; the helper code remained unused.

This commit removes the dead spec:
* core/Support/helpers/action_context.php drops actionFragmentContext
  (~9 LOC) and its building block actionFragmentResolveOrStatus
  (~30 LOC) plus their docblocks. The MUST-call-actionRequireCsrf
  warning, present in three aggregator docblocks before, now appears
  twice (one per remaining aggregator).
* tests/Support/Helpers/ActionContextHelperTest.php drops the six
  unit tests that exercised these functions (~83 LOC).
* tests/Architecture/ActionContextHelperContractTest.php drops the
  fragment building block from the buildingBlocks() data provider
  (5 entries instead of 6) and removes testFragmentResolveReturnDocblockIsFrozen.
  The CSRF-warning expectation is updated from 3 to 2 with a code
  comment explaining the rollback.

Verified:
* No production caller exists in pages/ or modules/.
* All 9 aggregator callers (5 actionEditContext + 4 actionCreateContext)
  remain unchanged.
* ActionContextCsrfPairingContractTest and DetailDrawerFragmentContractTest
  are deliberately left untouched: their allowlists/recognizer regexes
  still mention actionFragmentContext, but the patterns now match an
  empty set — harmless dead text. Documented as open items in the run
  report; future cleanup is optional and orthogonal to this removal.
* QGs all green (PHPUnit 2088 tests, PHPStan level 5, CS-fixer 0 diffs).

Net: 3 files, +31/-198 LOC, behavior unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 17:34:53 +02:00
parent 95723cebd9
commit 9ec10f5c02
3 changed files with 31 additions and 198 deletions

View File

@@ -1,13 +1,15 @@
<?php
/**
* Step 1: helpers introduced without production callers; produced by run
* 2026-04-25-action-context-helper. The 6 orthogonal building blocks plus
* the 3 cluster aggregators centralize the ~40-80 line pre-render preamble
* that every Edit/Create/View/Drawer-Fragment action repeats today.
*
* No page action or module page is allowed to call these yet — Step 2 is the
* Departments-Edit pilot and Step 3 is the cluster-wide rollout.
* Action-context helpers introduced by run 2026-04-25-action-context-helper
* (Step 1) and pruned by run 2026-04-25-action-fragment-context-removal after
* Cluster 10 (Drawer-Fragments) was declared a structural exception in run
* 2026-04-25-action-context-rollout-step11-fragments-exception. The remaining
* 5 orthogonal building blocks plus the 2 cluster aggregators
* (actionEditContext, actionCreateContext) centralize the ~40-80 line
* pre-render preamble that Edit/Create actions repeat. Drawer-fragment actions
* delegate to domain-services with their own status models and are NOT served
* by these helpers — see the Step 11 decision record for rationale.
*/
use MintyPHP\Router;
@@ -180,55 +182,6 @@ function actionBuildViewAuth(array $capabilities, array $whitelistedFlags): arra
];
}
/**
* Drawer-fragment variant of actionResolveModelOrFail + Authorize.
*
* Drawer fragments must NOT redirect (CLAUDE.md). They return HTTP status
* codes 400/403/404 instead. This helper produces a status DTO that the
* caller translates into http_response_code() + early return.
*
* Usage:
* $r = actionFragmentResolveOrStatus($finder, $rawId, ABILITY_VIEW, [...]);
* if ($r['status'] !== 'ok') { http_response_code($r['http_code']); return; }
*
* @param callable(string): mixed $finder Returns the resolved model or null.
* @param string $rawId Raw id from the URL.
* @param string $abilityKey Ability constant for authorize().
* @param array<string, mixed> $context Authorize context payload.
*
* @return array{status: 'ok'|'forbidden'|'not_found'|'invalid_id', model?: mixed, capabilities?: array<string, mixed>, http_code?: int}
*/
function actionFragmentResolveOrStatus(
callable $finder,
string $rawId,
string $abilityKey,
array $context
): array {
Guard::requireLogin();
$trimmedId = trim($rawId);
if ($trimmedId === '') {
return ['status' => 'invalid_id', 'http_code' => 400];
}
$decision = app(AuthorizationService::class)->authorize($abilityKey, $context);
if (!$decision->isAllowed()) {
return ['status' => 'forbidden', 'http_code' => 403];
}
$model = $finder($trimmedId);
if ($model === null) {
return ['status' => 'not_found', 'http_code' => 404];
}
$capabilities = $decision->attribute('capabilities', []);
return [
'status' => 'ok',
'model' => $model,
'capabilities' => is_array($capabilities) ? $capabilities : [],
];
}
/**
* CALLERS MUST call actionRequireCsrf() BEFORE this aggregator for POST requests. This helper does NOT verify CSRF (GR-SEC-001). For GET-only edit-render flows, CSRF is not required.
*
@@ -356,29 +309,3 @@ function actionCreateContext(array $args): array
'viewAuth' => $viewAuth,
];
}
/**
* CALLERS MUST call actionRequireCsrf() BEFORE this aggregator for POST requests. This helper does NOT verify CSRF (GR-SEC-001). Drawer fragments are typically GET, but the warning is intentionally defensive and consistent across aggregators.
*
* Cluster-10 Drawer-Fragment aggregator: thin wrapper around
* actionFragmentResolveOrStatus. Returns the status DTO unchanged so the
* caller can translate it to http_response_code(...) + early return.
*
* Required keys in $args:
* - finder: callable(string): mixed
* - rawId: string
* - abilityKey: string
* - context: array<string, mixed>
*
* @param array<string, mixed> $args
* @return array{status: 'ok'|'forbidden'|'not_found'|'invalid_id', model?: mixed, capabilities?: array<string, mixed>, http_code?: int}
*/
function actionFragmentContext(array $args): array
{
$finder = $args['finder'];
$rawId = (string) $args['rawId'];
$abilityKey = (string) $args['abilityKey'];
$context = is_array($args['context'] ?? null) ? $args['context'] : [];
return actionFragmentResolveOrStatus($finder, $rawId, $abilityKey, $context);
}