refactor(requests): extract user-owned exists rules to trait (#477)

## What

The `Rule::exists('x', 'id')->where(fn => user_id)` ownership closure
was repeated **19×** across 10 Form Requests (categories, labels,
accounts, typed accounts).

New `app/Http/Requests/Concerns/ValidatesUserOwnedResources` trait:

- `$this->userOwned('categories' | 'labels' | 'accounts')`
- `$this->userOwnedAccountOfType(AccountType::Loan | RealEstate)`

`UpdateAutomationRuleRequest` intentionally untouched — #476 makes it
extend Store.

## Stats

- **-110 / +63 lines**

## Checks

- `php artisan test
--filter="Transaction|Budget|Account|AutomationRule|RealEstate|Loan"` —
641 passed (3052 assertions), 1 skipped
- `vendor/bin/pint --dirty` — pass

Part of duplication-removal series (#475, #476).
This commit is contained in:
Víctor Falcón 2026-06-03 17:33:54 +02:00 committed by GitHub
parent 65acab4512
commit cc63a86a1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 75 additions and 96 deletions

View File

@ -2,11 +2,13 @@
namespace App\Http\Requests;
use App\Http\Requests\Concerns\ValidatesUserOwnedResources;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class BulkUpdateTransactionsRequest extends FormRequest
{
use ValidatesUserOwnedResources;
public function authorize(): bool
{
return true;
@ -23,44 +25,20 @@ class BulkUpdateTransactionsRequest extends FormRequest
'filters.amount_min' => ['nullable', 'numeric'],
'filters.amount_max' => ['nullable', 'numeric'],
'filters.category_ids' => ['nullable', 'array'],
'filters.category_ids.*' => [
'string',
'uuid',
Rule::exists('categories', 'id')->where(function ($query) {
$query->where('user_id', $this->user()->id);
}),
],
'filters.category_ids.*' => ['string', 'uuid', $this->userOwned('categories')],
'filters.account_ids' => ['nullable', 'array'],
'filters.account_ids.*' => ['string', 'uuid'],
'filters.label_ids' => ['nullable', 'array'],
'filters.label_ids.*' => [
'string',
'uuid',
Rule::exists('labels', 'id')->where(function ($query) {
$query->where('user_id', $this->user()->id);
}),
],
'filters.label_ids.*' => ['string', 'uuid', $this->userOwned('labels')],
'filters.creditor_name' => ['nullable', 'string'],
'filters.debtor_name' => ['nullable', 'string'],
'filters.search' => ['nullable', 'string'],
'filters.search_text' => ['nullable', 'string'],
'category_id' => [
'nullable',
Rule::exists('categories', 'id')->where(function ($query) {
$query->where('user_id', $this->user()->id);
}),
],
'category_id' => ['nullable', $this->userOwned('categories')],
'notes' => ['nullable', 'string'],
'notes_iv' => ['nullable', 'string', 'size:16'],
'label_ids' => ['nullable', 'array'],
'label_ids.*' => [
'required',
'string',
'uuid',
Rule::exists('labels', 'id')->where(function ($query) {
$query->where('user_id', $this->user()->id);
}),
],
'label_ids.*' => ['required', 'string', 'uuid', $this->userOwned('labels')],
];
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Concerns;
use App\Enums\AccountType;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Exists;
trait ValidatesUserOwnedResources
{
/**
* Rule asserting the value references a record on the given table owned by the authenticated user.
*/
protected function userOwned(string $table): Exists
{
return Rule::exists($table, 'id')->where('user_id', $this->user()->id);
}
/**
* Rule asserting the value references an account of the given type owned by the authenticated user.
*/
protected function userOwnedAccountOfType(AccountType $type): Exists
{
return $this->userOwned('accounts')->where('type', $type->value);
}
}

View File

@ -2,11 +2,13 @@
namespace App\Http\Requests\OpenBanking;
use App\Http\Requests\Concerns\ValidatesUserOwnedResources;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class MapAccountsRequest extends FormRequest
{
use ValidatesUserOwnedResources;
public function authorize(): bool
{
return $this->route('connection')->user_id === $this->user()->id;
@ -25,7 +27,7 @@ class MapAccountsRequest extends FormRequest
'nullable',
'uuid',
'required_if:mappings.*.action,link',
Rule::exists('accounts', 'id')->where('user_id', $this->user()->id),
$this->userOwned('accounts'),
],
];
}

View File

@ -4,6 +4,7 @@ namespace App\Http\Requests\Settings;
use App\Enums\AccountType;
use App\Enums\PropertyType;
use App\Http\Requests\Concerns\ValidatesUserOwnedResources;
use App\Models\Account;
use App\Services\CurrencyOptions;
use Illuminate\Contracts\Validation\ValidationRule;
@ -12,6 +13,8 @@ use Illuminate\Validation\Rule;
class StoreAccountRequest extends FormRequest
{
use ValidatesUserOwnedResources;
/**
* Determine if the user is authorized to make this request.
*/
@ -64,10 +67,7 @@ class StoreAccountRequest extends FormRequest
'linked_loan_account_id' => [
'nullable',
'string',
Rule::exists('accounts', 'id')->where(function ($query) {
$query->where('user_id', $this->user()->id)
->where('type', AccountType::Loan->value);
}),
$this->userOwnedAccountOfType(AccountType::Loan),
],
'notes' => ['nullable', 'string', 'max:2000'],
'revaluation_percentage' => ['nullable', 'numeric', 'min:-100', 'max:100'],
@ -85,10 +85,7 @@ class StoreAccountRequest extends FormRequest
'linked_real_estate_account_id' => [
'nullable',
'string',
Rule::exists('accounts', 'id')->where(function ($query) {
$query->where('user_id', $this->user()->id)
->where('type', AccountType::RealEstate->value);
}),
$this->userOwnedAccountOfType(AccountType::RealEstate),
function (string $attribute, mixed $value, \Closure $fail): void {
if (! is_string($value)) {
return;

View File

@ -2,12 +2,14 @@
namespace App\Http\Requests\Settings;
use App\Http\Requests\Concerns\ValidatesUserOwnedResources;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreAutomationRuleRequest extends FormRequest
{
use ValidatesUserOwnedResources;
/**
* Determine if the user is authorized to make this request.
*/
@ -35,9 +37,7 @@ class StoreAutomationRuleRequest extends FormRequest
'action_category_id' => [
'nullable',
'string',
Rule::exists('categories', 'id')->where(function ($query) {
$query->where('user_id', auth()->id());
}),
$this->userOwned('categories'),
],
'action_note' => ['nullable', 'string'],
'action_note_iv' => ['nullable', 'string', 'required_with:action_note'],
@ -46,9 +46,7 @@ class StoreAutomationRuleRequest extends FormRequest
'required',
'string',
'uuid',
Rule::exists('labels', 'id')->where(function ($query) {
$query->where('user_id', auth()->id());
}),
$this->userOwned('labels'),
],
];
}

View File

@ -4,6 +4,7 @@ namespace App\Http\Requests\Settings;
use App\Enums\AccountType;
use App\Enums\PropertyType;
use App\Http\Requests\Concerns\ValidatesUserOwnedResources;
use App\Services\CurrencyOptions;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
@ -11,6 +12,8 @@ use Illuminate\Validation\Rule;
class UpdateAccountRequest extends FormRequest
{
use ValidatesUserOwnedResources;
/**
* Determine if the user is authorized to make this request.
*/
@ -59,10 +62,7 @@ class UpdateAccountRequest extends FormRequest
'linked_loan_account_id' => [
'nullable',
'string',
Rule::exists('accounts', 'id')->where(function ($query) {
$query->where('user_id', $this->user()->id)
->where('type', AccountType::Loan->value);
}),
$this->userOwnedAccountOfType(AccountType::Loan),
],
'notes' => ['nullable', 'string', 'max:2000'],
'revaluation_percentage' => ['nullable', 'numeric', 'min:-100', 'max:100'],

View File

@ -4,11 +4,14 @@ namespace App\Http\Requests;
use App\Enums\BudgetPeriodType;
use App\Enums\RolloverType;
use App\Http\Requests\Concerns\ValidatesUserOwnedResources;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreBudgetRequest extends FormRequest
{
use ValidatesUserOwnedResources;
public function authorize(): bool
{
return true;
@ -16,16 +19,14 @@ class StoreBudgetRequest extends FormRequest
public function rules(): array
{
$userId = $this->user()->id;
return [
'name' => ['required', 'string', 'max:255'],
'period_type' => ['required', Rule::enum(BudgetPeriodType::class)],
'period_start_day' => ['nullable', 'integer', 'min:0', 'max:31'],
'category_ids' => ['nullable', 'array'],
'category_ids.*' => [Rule::exists('categories', 'id')->where('user_id', $userId)],
'category_ids.*' => [$this->userOwned('categories')],
'label_ids' => ['nullable', 'array'],
'label_ids.*' => [Rule::exists('labels', 'id')->where('user_id', $userId)],
'label_ids.*' => [$this->userOwned('labels')],
'rollover_type' => ['required', Rule::enum(RolloverType::class)],
'allocated_amount' => ['required', 'integer', 'min:0'],
];

View File

@ -4,12 +4,15 @@ namespace App\Http\Requests;
use App\Enums\AccountType;
use App\Enums\PropertyType;
use App\Http\Requests\Concerns\ValidatesUserOwnedResources;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreRealEstateDetailRequest extends FormRequest
{
use ValidatesUserOwnedResources;
/**
* Determine if the user is authorized to make this request.
*/
@ -39,10 +42,7 @@ class StoreRealEstateDetailRequest extends FormRequest
'linked_loan_account_id' => [
'nullable',
'string',
Rule::exists('accounts', 'id')->where(function ($query) {
$query->where('user_id', $this->user()->id)
->where('type', AccountType::Loan->value);
}),
$this->userOwnedAccountOfType(AccountType::Loan),
],
'notes' => ['nullable', 'string', 'max:2000'],
];

View File

@ -3,11 +3,14 @@
namespace App\Http\Requests;
use App\Enums\TransactionSource;
use App\Http\Requests\Concerns\ValidatesUserOwnedResources;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreTransactionRequest extends FormRequest
{
use ValidatesUserOwnedResources;
public function authorize(): bool
{
return true;
@ -17,18 +20,8 @@ class StoreTransactionRequest extends FormRequest
{
return [
'id' => ['sometimes', 'uuid'],
'account_id' => [
'required',
Rule::exists('accounts', 'id')->where(function ($query) {
$query->where('user_id', $this->user()->id);
}),
],
'category_id' => [
'nullable',
Rule::exists('categories', 'id')->where(function ($query) {
$query->where('user_id', $this->user()->id);
}),
],
'account_id' => ['required', $this->userOwned('accounts')],
'category_id' => ['nullable', $this->userOwned('categories')],
'description' => ['required', 'string'],
'description_iv' => ['nullable', 'string', 'size:16'],
'transaction_date' => ['required', 'date'],
@ -40,13 +33,7 @@ class StoreTransactionRequest extends FormRequest
'debtor_name' => ['nullable', 'string', 'max:255'],
'source' => ['required', Rule::enum(TransactionSource::class)],
'label_ids' => ['nullable', 'array'],
'label_ids.*' => [
'string',
'uuid',
Rule::exists('labels', 'id')->where(function ($query) {
$query->where('user_id', $this->user()->id);
}),
],
'label_ids.*' => ['string', 'uuid', $this->userOwned('labels')],
];
}

View File

@ -4,12 +4,15 @@ namespace App\Http\Requests;
use App\Enums\AccountType;
use App\Enums\PropertyType;
use App\Http\Requests\Concerns\ValidatesUserOwnedResources;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateRealEstateDetailRequest extends FormRequest
{
use ValidatesUserOwnedResources;
/**
* Determine if the user is authorized to make this request.
*/
@ -40,10 +43,7 @@ class UpdateRealEstateDetailRequest extends FormRequest
'linked_loan_account_id' => [
'nullable',
'string',
Rule::exists('accounts', 'id')->where(function ($query) {
$query->where('user_id', $this->user()->id)
->where('type', AccountType::Loan->value);
}),
$this->userOwnedAccountOfType(AccountType::Loan),
],
'notes' => ['nullable', 'string', 'max:2000'],
'revaluation_percentage' => ['nullable', 'numeric', 'min:-100', 'max:100'],

View File

@ -2,11 +2,13 @@
namespace App\Http\Requests;
use App\Http\Requests\Concerns\ValidatesUserOwnedResources;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateTransactionRequest extends FormRequest
{
use ValidatesUserOwnedResources;
public function authorize(): bool
{
return true;
@ -15,12 +17,7 @@ class UpdateTransactionRequest extends FormRequest
public function rules(): array
{
return [
'category_id' => [
'nullable',
Rule::exists('categories', 'id')->where(function ($query) {
$query->where('user_id', $this->user()->id);
}),
],
'category_id' => ['nullable', $this->userOwned('categories')],
'description' => ['sometimes', 'string'],
'description_iv' => ['nullable', 'string', 'size:16'],
'notes' => ['nullable', 'string'],
@ -28,14 +25,7 @@ class UpdateTransactionRequest extends FormRequest
'creditor_name' => ['nullable', 'string', 'max:255'],
'debtor_name' => ['nullable', 'string', 'max:255'],
'label_ids' => ['nullable', 'array'],
'label_ids.*' => [
'required',
'string',
'uuid',
Rule::exists('labels', 'id')->where(function ($query) {
$query->where('user_id', $this->user()->id);
}),
],
'label_ids.*' => ['required', 'string', 'uuid', $this->userOwned('labels')],
];
}