Skip to content
Nav

Extending the model

composer require langsys/laravel-api-keys
Packagist version Monthly downloads License PHP 8.2+ Laravel 10 / 11 / 12

Point api-keys.model at your own subclass to add columns, relations, or application-specific authorization:

config/api-keys.php
'model' => App\Models\ApiKey::class,

Three protected hooks are the supported extension points:

class ApiKey extends \Langsys\ApiKeys\Models\ApiKey
{
// Extra write authorization — a signed grant, device attestation, and so
// on. OR'd into the type decision, so returning true lets a key write
// where its type alone would not. This bypasses the read/write guarantee
// by design; keep it as strict as the check it replaces.
protected function extraWriteAllowances(Request $request): bool
{
return $this->hasValidWriteGrant($request);
}
// Allow-list entries contributed by the application rather than stored on
// the key — e.g. the egress addresses of a trusted internal service.
protected function additionalAllowlistEntries(): array
{
return config('services.renderer.egress_ips', []);
}
// Where the client address comes from, when $request->ip() is not it.
protected function clientIp(Request $request): ?string
{
return $request->header('CF-Connecting-IP') ?: $request->ip();
}
}

Every one of these has caught a real integration:

  • $fillable, $hidden and $casts replace rather than merge. In particular keep 'type' => ApiKeyType::class in $casts — without it the middleware’s type check silently denies every write. Keep key_hash in $hidden or you will serialize hashes. If you must keep your own enum for type, cast to it and set enforce_read_write => false so the package stops making that decision at all.
  • getByKey() does not filter by active. That is deliberate — the middleware checks active separately, and callers may want to distinguish “unknown key” from “revoked key”. But if you call getByKey() yourself as an authentication path, check active too, or override it. isValidKey() already does both.
  • The package registers its model hooks in booted(), not boot(). Key generation, the type/active defaults, default permissions and the lifecycle events all live there. Model::boot() only boots traits; booted() is invoked separately by bootIfNotBooted(). So a test helper that flushes model event listeners and then calls boot() to restore them will not restore these, and the failure is silent — the row saves, it just has no key_hash. If you flush listeners, re-run bootIfNotBooted() or reset the model’s booted state.
  • If you override booted() yourself, call parent::booted(). Eloquent does not chain it for you, and skipping it loses everything in the first bullet’s list at once.
Feedback