refactor: extract DbDialect helper, inline ProfileValidationRules trait

Audit items #17 and #21.

#17 — DayOfWeekSignal and StickinessSignal each had their own
isSqlite ternary picking between SQLite (strftime/julianday) and
MySQL (DAYOFWEEK/DATEDIFF) date expressions. Centralised in
App\Services\Prediction\Signals\DbDialect.

#21 — ProfileValidationRules was a trait with one consumer
(CreateNewUser); inlined the rules into the action and deleted the
trait. Also dropped PasswordValidationRules::currentPasswordRules()
which was unused. PasswordValidationRules trait stays (two consumers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ovidiu U
2026-04-29 20:00:01 +01:00
parent 4d9df1ee19
commit 7f64c42a23
6 changed files with 46 additions and 71 deletions

View File

@@ -3,14 +3,14 @@
namespace App\Actions\Fortify; namespace App\Actions\Fortify;
use App\Concerns\PasswordValidationRules; use App\Concerns\PasswordValidationRules;
use App\Concerns\ProfileValidationRules;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\CreatesNewUsers; use Laravel\Fortify\Contracts\CreatesNewUsers;
class CreateNewUser implements CreatesNewUsers class CreateNewUser implements CreatesNewUsers
{ {
use PasswordValidationRules, ProfileValidationRules; use PasswordValidationRules;
/** /**
* Validate and create a newly registered user. * Validate and create a newly registered user.
@@ -20,7 +20,8 @@ class CreateNewUser implements CreatesNewUsers
public function create(array $input): User public function create(array $input): User
{ {
Validator::make($input, [ Validator::make($input, [
...$this->profileRules(), 'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', Rule::unique(User::class)],
'password' => $this->passwordRules(), 'password' => $this->passwordRules(),
])->validate(); ])->validate();

View File

@@ -16,14 +16,4 @@ trait PasswordValidationRules
{ {
return ['required', 'string', Password::default(), 'confirmed']; return ['required', 'string', Password::default(), 'confirmed'];
} }
/**
* Get the validation rules used to validate the current password.
*
* @return array<int, Rule|array<mixed>|string>
*/
protected function currentPasswordRules(): array
{
return ['required', 'string', 'current_password'];
}
} }

View File

@@ -1,50 +0,0 @@
<?php
namespace App\Concerns;
use App\Models\User;
use Illuminate\Validation\Rule;
trait ProfileValidationRules
{
/**
* Get the validation rules used to validate user profiles.
*
* @return array<string, array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>>
*/
protected function profileRules(?int $userId = null): array
{
return [
'name' => $this->nameRules(),
'email' => $this->emailRules($userId),
];
}
/**
* Get the validation rules used to validate user names.
*
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
*/
protected function nameRules(): array
{
return ['required', 'string', 'max:255'];
}
/**
* Get the validation rules used to validate user emails.
*
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
*/
protected function emailRules(?int $userId = null): array
{
return [
'required',
'string',
'email',
'max:255',
$userId === null
? Rule::unique(User::class)
: Rule::unique(User::class)->ignore($userId),
];
}
}

View File

@@ -10,10 +10,7 @@ final class DayOfWeekSignal extends AbstractSignal
public function compute(SignalContext $context): array public function compute(SignalContext $context): array
{ {
$isSqlite = DB::connection()->getDriverName() === 'sqlite'; $dowExpr = DbDialect::dayOfWeekExpr('price_effective_at');
$dowExpr = $isSqlite
? "(CAST(strftime('%w', price_effective_at) AS INTEGER) + 1)"
: 'DAYOFWEEK(price_effective_at)';
$rows = DB::table('station_prices') $rows = DB::table('station_prices')
->where('fuel_type', $context->fuelType->value) ->where('fuel_type', $context->fuelType->value)

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Services\Prediction\Signals;
use Illuminate\Support\Facades\DB;
/**
* SQL dialect helpers for the small set of MySQL/SQLite differences the
* signal classes care about. Centralises the isSqlite ternaries that were
* duplicated across DayOfWeekSignal and StickinessSignal.
*/
final class DbDialect
{
private static function isSqlite(): bool
{
return DB::connection()->getDriverName() === 'sqlite';
}
/**
* Day-of-week expression returning 1=Sun..7=Sat (MySQL DAYOFWEEK convention).
* Targets a column on the queried table.
*/
public static function dayOfWeekExpr(string $column): string
{
return self::isSqlite()
? "(CAST(strftime('%w', {$column}) AS INTEGER) + 1)"
: "DAYOFWEEK({$column})";
}
/**
* Whole-day difference between MAX and MIN of a datetime column, suitable
* for use in an aggregate selectRaw.
*/
public static function maxMinDayDiffExpr(string $column): string
{
return self::isSqlite()
? "CAST((julianday(MAX({$column})) - julianday(MIN({$column}))) AS INTEGER)"
: "DATEDIFF(MAX({$column}), MIN({$column}))";
}
}

View File

@@ -8,10 +8,7 @@ final class StickinessSignal extends AbstractSignal
{ {
public function compute(SignalContext $context): array public function compute(SignalContext $context): array
{ {
$isSqlite = DB::connection()->getDriverName() === 'sqlite'; $diffExpr = DbDialect::maxMinDayDiffExpr('price_effective_at');
$diffExpr = $isSqlite
? 'CAST((julianday(MAX(price_effective_at)) - julianday(MIN(price_effective_at))) AS INTEGER)'
: 'DATEDIFF(MAX(price_effective_at), MIN(price_effective_at))';
$rows = DB::table('station_prices') $rows = DB::table('station_prices')
->where('fuel_type', $context->fuelType->value) ->where('fuel_type', $context->fuelType->value)