Skip to main content

Request Lifecycle

Every API request flows through Rhino's pipeline — a series of layers that handle authentication, authorization, scoping, querying, and response formatting. Understanding this flow helps you debug issues and know exactly where to customize behavior.

1. Middleware Layer​

The first layer your request hits. Middleware runs before any controller logic and can reject requests early.

Rhino applies middleware in this order:

  1. Global middleware — Laravel's default stack (CORS, authentication, etc.)
  2. Model middleware — Defined via $middleware on your model (applies to all actions)
  3. Action middleware — Defined via $middlewareActions (applies to specific actions only)
app/Models/Post.php
class Post extends Model
{
// Applied to ALL routes for this model
public static array $middleware = ['auth:sanctum'];

// Applied only to specific actions
public static array $middlewareActions = [
'store' => ['verified'],
'destroy' => ['can:admin'],
];
}

If middleware rejects the request, the pipeline stops here and returns the appropriate error (401, 403, 429, etc.).

2. Policy Layer​

After middleware passes, Rhino checks the ResourcePolicy to determine if the authenticated user can perform the requested action.

Each CRUD action maps to a policy method:

HTTP MethodActionPolicy Method
GET /postsindexviewAny($user)
GET /posts/{id}showview($user, $post)
POST /postsstorecreate($user)
PUT /posts/{id}updateupdate($user, $post)
DELETE /posts/{id}destroydelete($user, $post)

For member actions (show, update, destroy, restore, force-delete), Rhino first resolves the record by matching the {id} URL segment against the model's route key: $routeKey on the model if set, otherwise the global route_key config, otherwise the primary key. Unless configured, nothing changes — the lookup uses the primary key as always. See Models — Route Key.

The policy checks the user's roles and permissions for the current organization. If the user lacks the required permission, a 403 Forbidden response is returned immediately.

app/Policies/PostPolicy.php
class PostPolicy extends ResourcePolicy
{
// Permission format: posts.viewAny, posts.create, etc.
// Wildcards supported: posts.* or just *
}

See Policies for full details on permission configuration.

3. Scope Layer​

Once authorized, Rhino determines which records the user can see. This is the multi-tenancy boundary.

Scoping ensures users only access data belonging to their current organization:

  • Models with organization_id are filtered directly
  • Nested models are auto-detected — Rhino introspects BelongsTo relationships to find the path to the organization
  • Custom scopes (via HasAutoScope) can add additional filtering
app/Models/Blog.php
// Direct: WHERE organization_id = ?
class Blog extends Model
{
use BelongsToOrganization;
}

// Nested: auto-detected via Comment -> post() -> blog() -> organization_id
class Comment extends Model
{
use BelongsToOrganization;

public function post()
{
return $this->belongsTo(Post::class);
}
}

See Multi-Tenancy for full details.

Two kinds of "scope"

This layer is the always-on scope — the framework enforces it on every request, and no query parameter can bypass it. It is distinct from a client-selected named scope (?scope=name), which is applied in the Query Builder step below. A named scope runs after organization/authorization scoping and can only narrow the authorized set, never widen it.

4. Query Builder​

With the scope applied, Rhino builds the database query using parameters from the request URL:

FeatureQuery ParameterExample
Scope selection?scope=name?scope=availableForDrivers
Filtering?filter[field]=value?filter[status]=published
Sorting?sort=field?sort=-created_at
Searching?search=term?search=laravel
Pagination?page=N&per_page=N?page=2&per_page=25
Includes?include=relation?include=user,tags
Fields?fields[model]=f1,f2?fields[posts]=id,title

Only fields declared in $allowedFilters, $allowedSorts, $allowedSearch, $allowedIncludes, and $allowedFields on your model are accepted. Anything else is silently ignored.

Scope selection is the exception. A ?scope= value that is not in $allowedScopes (and is not the declared $defaultScope) returns a 403, rather than being silently ignored — matching the include-authorization contract. When no ?scope= is given, the model's $defaultScope is applied. The selected scope narrows the already organization-scoped, authorized set and receives the current authenticated user server-side. See Querying — Named Scopes.

See Querying for full details.

5. Response Serialization​

The query results are serialized into JSON. For index endpoints, Rhino adds pagination headers:

HeaderDescription
X-Current-PageCurrent page number
X-Last-PageTotal number of pages
X-Per-PageItems per page
X-TotalTotal number of records

6. Attribute Permissions via Policy​

Before sending the response, Rhino checks the policy's permittedAttributesForShow() and hiddenAttributesForShow() methods to determine which columns are visible based on the user's role:

app/Policies/PostPolicy.php
class PostPolicy extends ResourcePolicy
{
public function hiddenAttributesForShow(?Authenticatable $user): array
{
if ($user?->hasPermission('posts.*')) {
return []; // Admin sees everything
}

return ['internal_notes', 'cost_price']; // Regular users can't see these
}
}

This provides column-level security — different users see different fields in the same response, all controlled by permissions.

7. JSON Response​

The final JSON response is returned to the client. For a single resource:

Response
{
"id": 1,
"title": "My Post",
"status": "published",
"created_at": "2025-01-15T10:30:00Z"
}

For a collection (index), the response includes the data array with pagination headers in the HTTP response.

Summary​

Request → Middleware → Policy → Scope → Query → Serialize → Hide Columns → Response

Each layer is independently configurable through your model properties and policy methods. If something isn't working as expected, trace the request through these layers to identify where the issue occurs.