Skip to content

fix: declare relation return types the console requests and filter resource expansions through core - #392

Merged
roncodes merged 2 commits into
release/v0.6.72from
fix/relation-return-types
Oct 13, 2026
Merged

roncodes merged 2 commits into
release/v0.6.72from
fix/relation-return-types

Conversation

@roncodes

Copy link
Copy Markdown
Member

Needs human review. This is the security companion to fleetbase/core-api#303. The repo has no needs-human-review label, so I'm flagging it here.

Today the API passes every name in a client's with, expand or with_count straight to Eloquent. Eloquent loads a name by calling $model->newInstance()->$name(), so any public method that takes no required arguments runs on a blank model. For example, GET /int/v1/orders?with[]=dispatch runs Order::dispatch(), which saves a blank order and queues OrderDispatched and its webhooks.

core-api#303 restricts these names to methods that declare an Eloquent Relation return type, or that the model lists in $withable. Union types like BelongsTo|Builder count. This PR keeps fleetops working under that rule and closes one fleetops-side bypass.

1. Relation return types (e4a85d74)

These relations are untyped today. The console requests them, or they are documented public expansions, so they would stop loading under #303:

Model Methods → type Requested by
Fleet photo, serviceArea, zone, vendor, parentFleet → BelongsTo; subFleets → HasMany; drivers, vehicles → HasManyThrough fleets list, details and sidebar, order-list overlay; public v1 fleets?with= (all eight are documented in Postman and fleetbase.io)
Issue reporter, assignee, vehicle, driver → BelongsTo issues list and edit
FuelReport driver, vehicle, reporter → BelongsTo fuel reports
ServiceRate rateFees, parcelFees → HasMany; serviceArea, zone → BelongsTo service rates
Driver vehicle → BelongsTo scheduler (driverAssigned.vehicle); public v1 drivers?with=vehicle
Payload entities → HasMany; pickup, dropoff, return → BelongsTo; waypoints → HasManyThrough fleetops-data order model
UserExpansion closures driver, currentDriverSession, customer, contact → HasOne; driverProfiles → HasMany Expansion relations on User
CompanyExpansion closure drivers → HasMany cosmetic: core Company::drivers() already takes precedence

I checked every entry in the public allowlists (FleetController::EXPANDABLE, DriverController, VehicleController, TrailerController). Each target is now typed or already was. Vehicle and Trailer were already typed, and the union-typed Order and Driver relations (driverAssigned, vehicleAssigned, facilitator, currentJob, jobs) already pass.

Fatal-error check: adding a return type is fatal for any subclass that overrides the method without one. I scanned every package and followed subclass chains (82 subclasses). No src class overrides a typed method. The only subclass outside fleetops is storefront's test ServiceRateQuoteStub, which overrides none of them. Several fleetops test fakes did override these methods untyped. They now declare the matching type, and their fake relations extend the real relation class.

2. Resource bypass (45fab851)

ResolvesPublicRelationFields::requestedRelations() checks the request's with names with only method_exists before Fleet and Vehicle call loadMissing($with). That path also renders nested inside other responses, so ?with=saveInstance reached the loader there. When core's RequestRelations exists, Eloquent models now go through RequestRelations::eagerLoads(), which returns names as the model's methods. Older core releases and non-Eloquent fixtures keep the previous check.

A new test in VehicleResourceContractsTest requests [driver, saveInstance, untypedDriver, delete, driver.saveInstance, parent_vehicle, payload] and gets back ['driver', 'parentVehicle'], without ever calling saveInstance. It is skipped when RequestRelations is absent.

Merge order

Merge this with core-api#303 or before it. On today's core, part 1 changes nothing at runtime and part 2 falls back to the old check.

Docs / API spec

Every documented expansion stays requestable, and unknown names are still ignored, so no Postman change is needed. fleetbase.io extension-development/backend/expansions-and-observers.mdx should say that an Expansion relation needs a declared closure return type to be requestable through with. That page is tracked in core-api#303.

Validation

  • php -l on all 16 changed files: clean.
  • php-cs-fixer dry-run on the changed files: clean.
  • Pest, per file (502 files):
    • 489 pass.
    • 13 fail on core classes missing from the local vendored core-api 1.6.65: SocketCluster\SocketToken, SocketChannelRegistry, VerificationCode::CHECK_VALID. None of them touch the changed files.
  • Against release: v0.6.61 #303's RequestRelations: I copied the class into the vendored core temporarily and reran the 99 resource and with-related files. All pass, including the new test, except 3 with the same SocketToken failure.
  • phpstan: not run.

Core's RequestRelations now loads a client-requested `with`/`expand`/`with_count`
name only when the method (or Expandable closure) declares an Eloquent Relation
return type, or the model lists it in `$withable`. These relations are requested
by the console and the public v1 expansions but were untyped, so they would stop
loading:

- Fleet: photo, serviceArea, zone, vendor, parentFleet (BelongsTo); subFleets
  (HasMany); drivers, vehicles (HasManyThrough)
- Issue: reporter, assignee, vehicle, driver (BelongsTo)
- FuelReport: driver, vehicle, reporter (BelongsTo)
- ServiceRate: rateFees, parcelFees (HasMany); serviceArea, zone (BelongsTo)
- Driver: vehicle (BelongsTo)
- Payload: entities (HasMany); dropoff, pickup, return (BelongsTo); waypoints
  (HasManyThrough)
- UserExpansion closures: driver, currentDriverSession, customer, contact
  (HasOne); driverProfiles (HasMany). CompanyExpansion: drivers (HasMany)

Test fakes that override these methods now declare the same return type and
extend the matching relation class, since an untyped override of a typed parent
method is a fatal error at class load.
ResolvesPublicRelationFields::requestedRelations() kept any `with` name whose
root was a method on the model, and the Fleet and Vehicle resources pass the
result to loadMissing(). Eloquent loads a name by calling it on a blank model,
so `?with=saveInstance` ran that method.

When Fleetbase\Support\RequestRelations is available, an Eloquent resource now
keeps only the paths RequestRelations::eagerLoads() accepts, named as the
model's methods, which also matches the canonical names publicRelationObject()
compares against. The method_exists check stays as the fallback for core
releases without RequestRelations and for non-Eloquent resources.
@roncodes roncodes added the bug Something isn't working label Oct 13, 2026
@roncodes
roncodes merged commit 7357a55 into release/v0.6.72 Oct 13, 2026
4 of 6 checks passed
@roncodes
roncodes deleted the fix/relation-return-types branch October 13, 2026 06:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant