jcfrane/laravel-resource-scope

Symfony-like serialization scoping for Laravel API Resources. Define scopes to control which fields are returned per context.

Maintainers

Package info

github.com/jcfrane/laravel-resource-scope

pkg:composer/jcfrane/laravel-resource-scope

Statistics

Installs: 52

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v0.0.1 2026-02-18 15:20 UTC

This package is auto-updated.

Last update: 2026-02-18 15:26:48 UTC


README

Control which fields your Laravel API Resources return based on context. Define scopes like listing, detail, or summary and let the frontend request only the data it needs.

Inspired by Symfony's serialization groups.

Requirements

  • PHP 8.2+
  • Laravel 11 or 12

Installation

composer require jcfrane/laravel-resource-scope

The package auto-discovers its service provider. No manual registration needed.

Publish Config (Optional)

php artisan vendor:publish --tag=resource-scope-config

Quick Start

1. Add the trait to your resource

use JCFrane\ResourceScope\Concerns\HasResourceScope;

class UserResource extends JsonResource
{
    use HasResourceScope;

    protected function scopeDefinitions(): array
    {
        return [
            'listing' => ['id', 'name', 'email', 'avatar'],
            'detail'  => ['id', 'name', 'email', 'avatar', 'bio', 'created_at', 'settings'],
        ];
    }

    public function toArray(Request $request): array
    {
        return $this->scoped([
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'avatar' => $this->avatar_url,
            'bio' => $this->bio,
            'created_at' => $this->created_at,
            'settings' => $this->whenLoaded('settings'),
        ]);
    }
}

2. Register the middleware

In bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->api(append: [
        \JCFrane\ResourceScope\Middleware\SetResourceScope::class,
    ]);
})

Or apply it to specific route groups:

Route::middleware('resource.scope')->group(function () {
    Route::apiResource('users', UserController::class);
});

3. Request scoped data

# Listing — returns only id, name, email, avatar
GET /api/users?scope=listing

# Detail — returns all detail fields
GET /api/users/1?scope=detail

# No scope — returns everything (backwards compatible)
GET /api/users

You can also pass the scope via header:

X-Resource-Scope: listing

Defining Scopes

Method-based (recommended)

Define a scopeDefinitions() method that returns scope name => allowed field keys:

protected function scopeDefinitions(): array
{
    return [
        'listing' => ['id', 'name', 'email'],
        'detail'  => ['id', 'name', 'email', 'bio', 'skills', 'documents'],
    ];
}

Attribute-based

Use PHP 8 attributes on the resource class:

use JCFrane\ResourceScope\Attributes\ResourceScope;

#[ResourceScope('listing', fields: ['id', 'name', 'email'])]
#[ResourceScope('detail', fields: ['id', 'name', 'email', 'bio', 'skills', 'documents'])]
class UserResource extends JsonResource
{
    use HasResourceScope;

    public function toArray(Request $request): array
    {
        return $this->scoped([
            // ...
        ]);
    }
}

If both are present, scopeDefinitions() takes priority.

Scope Cascading

When a resource contains nested resources, you can control how scopes propagate. There are two ways to define mappings:

Method-based mappings

class PostResource extends JsonResource
{
    use HasResourceScope;

    protected function scopeDefinitions(): array
    {
        return [
            'listing' => ['id', 'title', 'author', 'created_at'],
        ];
    }

    protected function scopeMappings(): array
    {
        return [
            'listing' => [
                UserResource::class => 'summary',
            ],
        ];
    }

    public function toArray(Request $request): array
    {
        return $this->scoped([
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->body,
            'author' => new UserResource($this->whenLoaded('author')),
            'created_at' => $this->created_at,
        ]);
    }
}

Attribute-based mappings

You can also define mappings directly in the #[ResourceScope] attribute using the mappings parameter:

use JCFrane\ResourceScope\Attributes\ResourceScope;

#[ResourceScope('listing', fields: ['id', 'title', 'author', 'created_at'], mappings: [
    UserResource::class => 'summary',
])]
#[ResourceScope('detail', fields: ['id', 'title', 'body', 'author', 'created_at'])]
class PostResource extends JsonResource
{
    use HasResourceScope;

    public function toArray(Request $request): array
    {
        return $this->scoped([
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->body,
            'author' => new UserResource($this->whenLoaded('author')),
            'created_at' => $this->created_at,
        ]);
    }
}

When the listing scope is active on PostResource, the nested UserResource will automatically use the summary scope.

If both scopeMappings() method and attribute mappings are present, the method takes priority. If no mapping is defined, the parent's scope name passes through to nested resources. If the nested resource doesn't define that scope, it returns all fields.

Works with Laravel's Conditional Fields

Scoping works alongside whenLoaded(), whenHas(), and when(). Both conditions apply — Laravel's conditional checks run first, then scoping filters the keys:

return $this->scoped([
    'id' => $this->id,
    'name' => $this->name,
    'documents' => DocumentResource::collection($this->whenLoaded('documents')),
    'is_admin' => $this->when($user->isAdmin(), true),
]);

Backwards Compatible

  • No scope parameter = all fields returned (existing behavior)
  • Scope not defined on a resource = all fields returned
  • Unknown scope name = all fields returned

No breaking changes to existing API responses.

Configuration

Published config file (config/resource-scope.php):

return [
    // Query parameter name (default: 'scope')
    'query_param' => 'scope',

    // HTTP header name (default: 'X-Resource-Scope')
    'header' => 'X-Resource-Scope',

    // Query param takes priority over header (default: true)
    'query_param_priority' => true,
];

Testing

composer test

License

MIT