refactor(airport): add #[Scope] methods + #[ObservedBy] modernization
- Scopes: active(), byHub(), orderByIcao() — all using #[Scope] attribute - Attach #[ObservedBy(AirportObserver::class)], remove imperative Airport::observe() from ObserverServiceProviders - Override resolveRouteBinding() for case-insensitive ICAO route binding (URLs like /api/airports/kjfk now resolve correctly) - Drop dead public static array $rules (Prettus validator hook, never activated)
This commit is contained in:
parent
ab91a8c258
commit
1eb5f2eb1c
@ -3,8 +3,12 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Contracts\Model;
|
||||
use App\Models\Observers\AirportObserver;
|
||||
use App\Models\Traits\ExpensableTrait;
|
||||
use App\Models\Traits\FilesTrait;
|
||||
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
|
||||
use Illuminate\Database\Eloquent\Attributes\Scope;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@ -86,6 +90,7 @@ use Spatie\Activitylog\Traits\LogsActivity;
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
#[ObservedBy(AirportObserver::class)]
|
||||
class Airport extends Model
|
||||
{
|
||||
use ExpensableTrait;
|
||||
@ -124,25 +129,6 @@ class Airport extends Model
|
||||
'notes',
|
||||
];
|
||||
|
||||
/**
|
||||
* Validation rules
|
||||
*/
|
||||
public static array $rules = [
|
||||
'icao' => 'required',
|
||||
'iata' => 'sometimes|nullable',
|
||||
'name' => 'required',
|
||||
'location' => 'sometimes',
|
||||
'region' => 'sometimes',
|
||||
'country' => 'sometimes',
|
||||
'lat' => 'required|numeric',
|
||||
'lon' => 'required|numeric',
|
||||
'elevation' => 'nullable|numeric',
|
||||
'ground_handling_cost' => 'nullable|numeric',
|
||||
'fuel_100ll_cost' => 'nullable|numeric',
|
||||
'fuel_jeta_cost' => 'nullable|numeric',
|
||||
'fuel_mogas_cost' => 'nullable|numeric',
|
||||
];
|
||||
|
||||
public $sortable = [
|
||||
'id',
|
||||
'iata',
|
||||
@ -255,6 +241,49 @@ class Airport extends Model
|
||||
return $this->hasMany(User::class, 'home_airport_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Marker scope. Airport has no `status`/`active` column, so this is
|
||||
* equivalent to the SoftDeletes default (excludes trashed).
|
||||
*
|
||||
* Reserved for cross-model consistency — Phase 4 (User domain) and
|
||||
* Phase 6 (Flight domain) will add `active()` scopes to `User`,
|
||||
* `Airline`, and other models, at which point reading
|
||||
* `Airport::active()` will match the convention naturally.
|
||||
*/
|
||||
#[Scope]
|
||||
protected function active(Builder $q): Builder
|
||||
{
|
||||
return $q->whereNull('deleted_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to hub airports only.
|
||||
*/
|
||||
#[Scope]
|
||||
protected function byHub(Builder $q): Builder
|
||||
{
|
||||
return $q->where('hub', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Order results by ICAO ascending.
|
||||
*/
|
||||
#[Scope]
|
||||
protected function orderByIcao(Builder $q): Builder
|
||||
{
|
||||
return $q->orderBy('icao', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* URLs typically use a lowercase ICAO (e.g. /api/airports/kjfk) but
|
||||
* the table stores ICAO uppercased. Override the default Eloquent
|
||||
* route binding so case-insensitive lookups resolve correctly.
|
||||
*/
|
||||
public function resolveRouteBinding($value, $field = null): ?\Illuminate\Database\Eloquent\Model
|
||||
{
|
||||
return $this->resolveRouteBindingQuery($this, strtoupper((string) $value), $field)->first();
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@ -2,13 +2,11 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Airport;
|
||||
use App\Models\Flight;
|
||||
use App\Models\FlightField;
|
||||
use App\Models\FlightFieldValue;
|
||||
use App\Models\Journal;
|
||||
use App\Models\JournalTransaction;
|
||||
use App\Models\Observers\AirportObserver;
|
||||
use App\Models\Observers\FlightObserver;
|
||||
use App\Models\Observers\JournalObserver;
|
||||
use App\Models\Observers\JournalTransactionObserver;
|
||||
@ -26,7 +24,6 @@ class ObserverServiceProviders extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
Airport::observe(AirportObserver::class);
|
||||
Journal::observe(JournalObserver::class);
|
||||
JournalTransaction::observe(JournalTransactionObserver::class);
|
||||
|
||||
|
||||
59
tests/Unit/Models/AirportScopesTest.php
Normal file
59
tests/Unit/Models/AirportScopesTest.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Airport;
|
||||
|
||||
test('Airport::byHub returns only hubs', function () {
|
||||
Airport::factory()->count(3)->create(['hub' => false]);
|
||||
Airport::factory()->create(['hub' => true, 'icao' => 'KORD']);
|
||||
|
||||
$results = Airport::byHub()->get();
|
||||
|
||||
expect($results->pluck('icao')->all())->toBe(['KORD']);
|
||||
});
|
||||
|
||||
test('Airport::orderByIcao returns rows in ascending icao order', function () {
|
||||
foreach (['KZZZ', 'KAAA', 'KMMM'] as $icao) {
|
||||
Airport::factory()->create(['id' => $icao, 'icao' => $icao]);
|
||||
}
|
||||
|
||||
$results = Airport::orderByIcao()->get();
|
||||
|
||||
expect($results->pluck('icao')->all())->toBe(['KAAA', 'KMMM', 'KZZZ']);
|
||||
});
|
||||
|
||||
test('Airport::active excludes soft-deleted airports', function () {
|
||||
Airport::factory()->create(['icao' => 'KAAA']);
|
||||
$trashed = Airport::factory()->create(['icao' => 'KBBB']);
|
||||
$trashed->delete();
|
||||
|
||||
$results = Airport::active()->get();
|
||||
|
||||
expect($results->pluck('icao')->all())->toBe(['KAAA']);
|
||||
});
|
||||
|
||||
test('Airport scopes compose with each other', function () {
|
||||
Airport::factory()->create(['icao' => 'KZZZ', 'hub' => true]);
|
||||
Airport::factory()->create(['icao' => 'KAAA', 'hub' => true]);
|
||||
Airport::factory()->create(['icao' => 'KMMM', 'hub' => false]);
|
||||
|
||||
$results = Airport::byHub()->orderByIcao()->get();
|
||||
|
||||
expect($results->pluck('icao')->all())->toBe(['KAAA', 'KZZZ']);
|
||||
});
|
||||
|
||||
test('Airport::resolveRouteBinding upcases the incoming ICAO', function () {
|
||||
Airport::factory()->create(['id' => 'KJFK', 'icao' => 'KJFK']);
|
||||
|
||||
$resolved = (new Airport())->resolveRouteBinding('kjfk');
|
||||
|
||||
expect($resolved)->not->toBeNull()
|
||||
->and($resolved->id)->toBe('KJFK');
|
||||
});
|
||||
|
||||
test('Airport::resolveRouteBinding returns null for unknown ICAO', function () {
|
||||
$resolved = (new Airport())->resolveRouteBinding('XXXX');
|
||||
|
||||
expect($resolved)->toBeNull();
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user