refactor: Use form requests for transactions - #232
Conversation
Code Review Summary✨ This PR moves request validation for the transaction endpoints out of 🚀 Key Improvements
📉 Regressions
🚨 Critical Issues
|
|
Coverage Report |
f4e8e7c to
8407052
Compare
|
@iMercyvlogs the pull request title does not follow the patterns we use |
kofimokome
left a comment
There was a problem hiding this comment.
This is good. However, failure responses have a structure. See
$this->failure()It should be such that If the form request validation fails, the response structure should follow the same structure as $this->failure()
|
Merged The rules had gone stale. The branch carried rules from another PR.
Three tests cover it: a named-field failure returns 422, a nested-field failure reports under One correction to something said in review: the 400 path is not reachable through a wildcard rule. |
|
|
||
| private function expectsBoolean($rule): bool | ||
| { | ||
| $rules = is_array($rule) ? $rule : explode('|', (string) $rule); |
There was a problem hiding this comment.
expectsBoolean() casts non-array rules to string unconditionally. validationData() runs for every subclass of ApiFormRequest (including the four import requests, whose rules() we cannot see here). If any rule is declared as a bare object/Closure (e.g. 'field' => new MyRule() or 'field' => function () {}), (string) $rule throws Object of class ... could not be converted to string, crashing the request before validation even runs. Custom ValidationRule implementations generally do not define __toString(). Guard the cast so only string rules are exploded.
| $rules = is_array($rule) ? $rule : explode('|', (string) $rule); | |
| + $rules = is_array($rule) ? $rule : (is_string($rule) ? explode('|', $rule) : []); |
17527ab to
76dc581
Compare
| use Illuminate\Http\Exceptions\HttpResponseException; | ||
|
|
||
| abstract class ApiFormRequest extends FormRequest | ||
| { |
There was a problem hiding this comment.
This duplicates the API validation-failure envelope that already lives on the base controller (ApiController::validateRequest() / failure()), which produced the same two messages and was consumed by the removed controller code via $validationResult['isValidated'] / $this->failure(...). Two independent implementations of the same contract can now diverge (message text, 422/400 status, payload shape), and because TransactionController still extends ApiController, other endpoints keep using the old path. Extract the envelope into one reusable method so both call sites share it instead of each owning a copy.
| { | |
| protected function failedValidation(Validator $validator): void | |
| { | |
| throw new HttpResponseException($this->validationErrorResponse( | |
| $validator->errors(), | |
| array_keys($this->rules()) | |
| )); | |
| } | |
| /** | |
| * Single source of truth for the API validation-error envelope. | |
| * | |
| * ApiController::failure()/validateRequest() previously produced these same | |
| * messages and status codes; they should delegate here so the API exposes | |
| * one contract instead of two that can drift apart. | |
| */ | |
| protected function validationErrorResponse($errors, array $knownAttributes) | |
| { | |
| $named = $errors->hasAny($knownAttributes); | |
| return response()->json([ | |
| 'success' => false, | |
| 'message' => $named | |
| ? __('Server failed to validate request.') | |
| : __('Server unable to process request.'), | |
| 'errors' => $errors->toArray(), | |
| ], $named ? 422 : 400); | |
| } |
|
This repository is not connected to any of your workspaces. Please connect it at https://app.sourceant.ai to get reviews on it. |
The extracted rules had fallen behind the controller, dropping the intent field and the cap on categories. Textual booleans are cast again before validation, and a failure that names no known field answers the way it used to.
76dc581 to
6e1a5e2
Compare
Superseded: the branch was rebased onto dev and the code these reviews point at no longer exists.
|
|
||
| protected function failedValidation(Validator $validator): void | ||
| { | ||
| $errors = $validator->errors(); |
There was a problem hiding this comment.
MessageBag stores messages under their flat, dotted attribute name (the same keys Laravel emits in JSON validation payloads, e.g. categories.0). array_keys($this->rules()) only contains the declared keys — categories and categories.* — never categories.0. Therefore hasAny(...) returns false for a failure that only occurs on a nested/wildcard rule, and the request falls through to the 400 branch. The new test test_validation_failure_on_a_nested_field_is_reported_under_its_own_key posts ['categories' => [999999]], whose only error key is categories.0, yet asserts 422 — with the current check that request returns 400 and the test fails. Match nested keys against the declared top-level attributes as well.
| $errors = $validator->errors(); | |
| $errors = $validator->errors(); | |
| $rules = array_keys($this->rules()); | |
| $named = $errors->hasAny($rules) | |
| || collect($errors->keys())->contains( | |
| fn (string $key) => in_array(explode('.', $key)[0], $rules, true) | |
| ); |
| protected function failedValidation(Validator $validator): void | ||
| { | ||
| $errors = $validator->errors(); | ||
| $named = $errors->hasAny(array_keys($this->rules())); |
There was a problem hiding this comment.
MessageBag::hasAny() compares keys exactly, so declared wildcard rules such as categories.* never match a concrete error key such as categories.0. Nested-field failures will therefore be classified as unrecognised and returned as 400 / "Server unable to process request." instead of 422, contradicting the added test test_validation_failure_on_a_nested_field_is_reported_under_its_own_key and the API's named-field contract. Expand the check so any error key matching a declared rule (including via Str::is wildcards) counts as named.
| $named = $errors->hasAny(array_keys($this->rules())); | |
| $ruleKeys = array_keys($this->rules()); | |
| $named = $errors->hasAny($ruleKeys) | |
| || collect($errors->keys())->contains( | |
| fn ($errorKey) => collect($ruleKeys)->contains( | |
| fn ($ruleKey) => \Illuminate\Support\Str::is($ruleKey, $errorKey) | |
| ) | |
| ); |
Use FormRequest in store and update methods in the transactionController