Download the PHP package devespresso/laravel-api-kit without Composer
On this page you can find all versions of the php package devespresso/laravel-api-kit. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download devespresso/laravel-api-kit
More information about devespresso/laravel-api-kit
Files in devespresso/laravel-api-kit
Package laravel-api-kit
Short Description A Laravel API kit providing filtering, transformation, repositories, request validation, and authorisation.
License MIT
Informations about the package laravel-api-kit
Laravel API Kit
A Laravel package that provides a complete data filtering, transformation, and API response system. Drop it into any Laravel application to get automatic query filtering, model transformation, pagination, sorting, authorisation, and CRUD repositories — all driven by simple class conventions.
Requirements
- PHP 8.1+
- Laravel 10, 11, 12, or 13
Installation
Publish the config file:
Scaffolding
Generate a full API resource with a single command:
This creates all 7 components at once:
| Component | Generated Class |
|---|---|
| Model | App\Models\Post |
| Repository | App\Repositories\PostRepository |
| Controller | App\Http\Controllers\PostController |
| Transformer | App\Transformers\PostTransformer |
| Request | App\Http\Requests\PostRequest |
| Authorisation | App\Services\Authorisation\PostAuthorisationService |
| Filter Service | App\Services\Filters\PostFilterService |
All paths are driven by the paths config — if you customise them, the scaffold command follows automatically.
Options
Available component names for --only and --except: model, repository, controller, transformer, request, authorisation, filter-service.
Configuration
Publish the config file to config/devespressoApi.php:
pagination.with_pages
Controls the default pagination method used when no pagination_type is passed in the request.
paths
Namespaces used to auto-resolve classes and to determine where the scaffold command places generated files. Change these if your project uses a non-standard structure.
auto_select
When true, the filter service reads the active transformer format and automatically adds a SELECT clause to the query — only fetching columns that are actually needed. Prevents SELECT * without any manual effort.
Set to false to let Eloquent fall back to SELECT *, or when you need full manual control over selected columns.
auto_eager_load
When true, any relation defined as a nested array in the transformer format is automatically eager-loaded with its own scoped SELECT. Eliminates N+1 queries without writing ->with() manually.
Set to false to manage eager loading manually in your filter service or controller.
enable_explicit_filtering
When true, the filter service only dispatches request keys that are explicitly listed via the explicitFilters parameter. Keys not in the list are silently ignored. sort and search are always exempt. $autoApply is unaffected.
See Explicit Filtering for usage.
roles, numeric_roles, role_resolver
Controls role-based method restrictions in the filter service.
role_resolvermust be an invokable class — closures cannot be used because the config file must be cacheable (php artisan config:cache).
See $roleMethods for usage.
transformers.prefixes
Single-character prefixes used in transformer $formats arrays to control how attributes are treated. All three are fully configurable — if any clash with your attribute names, change them here and the entire package will use your values automatically.
| Key | Default | Effect |
|---|---|---|
hidden_attributes |
! |
Attribute is SELECTed but stripped from the response |
custom_attributes |
@ |
Attribute value is resolved via $customAttributes map |
accessor_attributes |
~ |
Attribute is a Laravel model accessor — NOT added to SELECT, but included in the output via $model->attribute |
unmerged_format |
_ |
Format key is not merged with the * wildcard format |
Core Components
1. EnableDatabaseFiltering Trait
Add to any Eloquent model to enable filtering:
Call filter() from a controller:
filter() accepts two optional extra parameters:
Pre-scoping the query with a parent resource ($query):
Passing context into filter methods ($extras):
Using $this->user inside the filter service:
$this->user holds the authenticated user passed as the second argument to filter(). It is available anywhere in the filter service — setConditions(), filter methods, and any custom method you add to the subclass.
2. BaseFilterService
Create a filter service per model by extending BaseFilterService. Each key in the incoming request data is camelCased and dispatched to a matching method on the service.
Available helpers inside filter methods
Method Dispatch and Security
The filter service works by taking each key in the incoming request data, converting it to camelCase, and calling the matching public method on the service if it exists. For example, passing author_id=5 in the request will automatically call $this->authorId(5).
This means any public method on your filter service subclass is callable from request data by default. The package protects against this in four ways:
1. Base class methods are always blocked
All public methods defined on BaseFilterService itself (e.g. setData, setQuery, filter) are automatically guarded and can never be triggered by request data. sort and search are intentionally excluded from this list so they remain dispatchable.
2. Protected methods are automatically blocked
Only public methods can be dispatched. If you define a method as protected on your subclass, it will never be triggered by request data — no configuration needed. Use this as a natural way to write internal helper methods without worrying about accidental exposure:
3. $guardedMethods — block specific public methods on your subclass
Use this to explicitly prevent public methods on your subclass from being triggered by request data:
Any method listed here will be silently skipped even if a matching key is present in the request.
4. $roleMethods — restrict methods to specific roles
Maps role names to the methods that require them. A method is only dispatched if the current user holds that role — or a higher one:
Roles are hierarchical — a higher role automatically inherits access to all methods available to lower roles. Declare the hierarchy and a resolver in config/devespressoApi.php:
The resolver must be an invokable class — closures are not supported because the config file must be cacheable:
An admin user can trigger methods listed under admin, editor, and moderator. An editor can trigger editor and moderator methods, but not admin.
Full example:
Calling from a controller requires no extra work — role checking happens automatically:
Numeric roles — if your roles are numeric (e.g. 1, 2, 3 or 10, 20, 30), set numeric_roles to true and skip the roles list entirely. The hierarchy is derived automatically from the keys in $roleMethods:
A user with role 3 can trigger methods at levels 1, 2, and 3. A user with role 1 can only trigger level 1 methods.
That's all the setup needed — no overrides required on individual filter services.
If you need custom resolution logic for a specific service, override getEffectiveRoles():
Rule of thumb: keep internal helpers
protected. If a public method should not be triggerable from a request key, add it to$guardedMethods. If it should only be available to specific roles, add it to$roleMethods.
Auto-Apply
Methods listed in $autoApply are always dispatched regardless of what is in the request data. They run after the request-driven filters and cannot be skipped by the caller:
Use this for constraints that must always be enforced — scoping to active records, filtering by tenant, etc.
Explicit Filtering
For an extra layer of security, you can restrict which request keys are allowed to drive filter methods on a per-call basis. This is controlled by two things:
-
Config flag — enable it globally in
config/devespressoApi.php: - Allowed list per request — pass it through the model's
filter()call:
When enable_explicit_filtering is true, the restriction always applies — there is no opt-out per call. Only keys in the allowed list are dispatched to filter methods; anything not listed is silently ignored, even if a matching public method exists. sort and search are always exempt.
Not passing an allowed list is treated as an empty list — all request-driven filters are blocked. This means every endpoint that uses filtering must explicitly declare which keys it allows:
$autoApplymethods are unaffected — they always run regardless of the explicit filter list.
Pagination
Control pagination via request data:
pagination_type |
Result |
|---|---|
| (not set) | simplePaginate() or paginate() based on config |
simple |
simplePaginate() — no total count query |
paginate |
paginate() — includes total count |
cursor |
cursorPaginate() — cursor-based, no total count |
none |
get() — returns all results |
Cursor pagination and sorting
Cursor pagination works by encoding the last-seen row position into an opaque token. Laravel reads the cursor query parameter automatically — you do not need to handle it in your filter service.
Because the cursor points to a specific row, the sort order must be stable and unique across the full result set. If two rows can produce the same sort key, Laravel cannot determine a reliable position and will skip or repeat rows between pages.
Rules to follow:
- Always include a unique column (typically
id) as the final sort key. A sort likecreated_at,descis not stable on its own — two posts can share the samecreated_attimestamp. Addidas a tiebreaker:sort[]=created_at,desc&sort[]=id,desc. - Never use
rawSortwith cursor pagination unless the raw expression is fully deterministic and unique per row. - Avoid sorts on nullable columns without a fallback —
NULLvalues make the cursor position ambiguous.
The defaultSortingColumn on BaseFilterService is ['id,desc'], which is safe for cursor pagination out of the box. If you change it in your subclass, make sure to keep id as the final tiebreaker:
Laravel will throw a RuntimeException if it cannot encode a valid cursor from the current sort — which is a clear signal that the ordering is not stable enough.
Sorting
Allowed sort columns are controlled by $sortColumns. You can also define aliases via $customSortColumns:
For complex sorts that can't be expressed as a simple column — such as FIELD(), COALESCE(), or any raw SQL expression — use $rawSort to map an alias to a method on your filter service:
The method returns the raw SQL expression — no need to handle the direction. The framework appends it and calls orderByRaw() for you. Raw sort methods bypass the column allowlist entirely.
Methods listed in
$rawSortare automatically guarded from request data dispatch — they cannot be triggered as filter methods regardless of their visibility.
3. BaseTransformer
Controls which model attributes are included in API responses and how they are formatted. The transformer is resolved automatically from the model name (PostTransformer for Post), or set explicitly via $transformer on the filter service or controller.
Attribute Prefixes
| Prefix | Meaning |
|---|---|
!attribute |
Hidden — excluded from output. On a relation key, still eager-loaded for SELECT purposes but not returned. |
@attribute |
Custom — value resolved via the $customAttributes map instead of reading from the database. |
~attribute |
Accessor — a Laravel model accessor. Not added to the SELECT query, but read from the model and included in the output. |
All prefixes are configurable via
config/devespressoApi.phpundertransformers.prefixes. If your attribute names clash with the defaults, change them there and the entire package will use your values automatically.
Format Key Prefixes
| Format key | Behaviour |
|---|---|
* |
Wildcard — always included, merged with the matched route key |
show, index, etc. |
Merged on top of * for that controller method |
_index |
Returned standalone — does not merge with * |
API Versioning
When your API evolves across versions, the transformer's versioning system lets you describe what changes at each version — without creating separate transformer files.
Enable versioning in the config:
Define your base format and version methods on the transformer:
Resolution chain:
| Request version | Formats applied |
|---|---|
| unversioned / none | baseFormat() only |
v2 |
baseFormat() → v2Format() |
v3 |
baseFormat() → v2Format() → v3Format() |
v445 (unknown) |
baseFormat() → v2Format() → v3Format() (falls back to latest) |
Nested relations — append and remove work at any depth, mirroring the existing format shape:
Standalone versions — use merge: false to replace all accumulated formats and start fresh from that version:
Subsequent versions still build on top of the standalone result. Note that merge: false only resets the accumulated formats — property overrides (renames, formatters, guarded, defaults, customAttributes) always accumulate cumulatively regardless.
Versioned property overrides
Version methods can also override renames, formatters, guarded, defaults, and customAttributes — the same properties you set on the transformer class. The rule is simple:
- Class properties (
$renames,$formatters, etc.) are the base — always applied regardless of version. - Version method keys are additive — merged on top of the base for that call, never touching the class properties.
What the user gets per version:
| Version | Active renames |
|---|---|
| base | created_at → createdAt |
| v2 | + name → fullName, author.name → authorName |
| v3 | + status → userStatus |
The chain accumulates across versions — v3's renames are merged on top of v2's, which are merged on top of the base. Later versions override earlier values for the same key.
Base properties are never mutated — calling resolveVersionedFormats() on the same transformer instance with different versions is safe. Versioned state is isolated per call and reset at the start of each resolution.
Key validation — version methods only accept the following keys. Any typo (e.g. 'appned' instead of 'append') throws an \InvalidArgumentException immediately with a clear message listing both the bad key and the valid ones:
Also, merge: false requires a formats key — omitting it throws as well.
$latestVersion — opt-in strict mode:
When set, any version within this boundary that is missing its format method throws a RuntimeException with a descriptive message. Versions beyond $latestVersion are always skipped silently — not every transformer needs to change at every version.
Driver: route_prefix — detects the version from the start of the route URI. A route registered at v2/posts/{id} resolves to v2. A route whose URI is exactly the version string (e.g. v2 with no trailing path) also matches.
Driver: header — reads the version from the configured request header:
Reading the resolved version — after setData() is called on the controller, the resolved version is available on the controller via $this->version. On the transformer it is available via getResolvedVersion(). Both return null when versioning is disabled or no version was detected.
When an unknown version is requested (e.g. v445) the system falls back to the full chain and both properties reflect the last known version (v3), not the raw requested value — so callers always see the effective version that was actually applied.
$wrapper
The $wrapper property controls the key name used to wrap the transformed data in the response. If not set, it defaults to 'data':
The wrapper can also be overridden per-call from the controller via setData($post, 'post').
Transformer-Driven Query
When auto_select and auto_eager_load are enabled, the filter service reads your transformer's $formats definition and automatically builds an optimised query — no SELECT *, no N+1.
Given this transformer:
Important: always include the foreign key that connects the relation (e.g.
user_idon posts) in your transformer format. Without it, the column won't be selected and the eager-loaded relation will return empty. Use the plain key to include it in the response, or prefix it with!to select it silently.
Calling Post::filter($request->validated(), $request->user()) generates exactly:
@word_count and ~reading_time are both excluded from SELECT — the difference is how their values are resolved: @ calls a method on the transformer, while ~ calls the model accessor directly ($model->reading_time).
And the JSON response includes only what was declared as visible — ! prefixed fields are fetched but stripped from the output:
team_idand!— they never appear in the response.user_idis selected and visible since it was declared without a prefix. If you changed it to'!user_id'in the transformer, it would still be selected but would disappear from the response.
The Eloquent equivalent you would otherwise write by hand:
With the package, that query is derived automatically from the transformer — you never write it, and it stays in sync with your response format as the transformer evolves.
4. BaseRepository
Provides standard CRUD with lifecycle hooks. Automatically resolves the model from the repository class name (PostRepository → Post).
Available methods:
To skip hooks for a single operation, chain withoutHooks() before the call. The skip list resets automatically after each operation.
5. ApiController
Base controller for JSON API responses. Automatically resolves a transformer and repository from the controller class name.
Response shortcuts
respondCreated() returns a 201 Created response. respondNoContent() returns a 204 No Content response:
setRawData() — bypass the transformer
Use setRawData() to add data to the response without going through the transformer. Defaults to the 'data' key:
This is especially useful when autoResolveTransformer is disabled, or when the data doesn't come from a model.
appendTo() — accumulate multiple values under a key
Use appendTo() to push values onto a response key rather than replacing it. Each call appends to the array. Defaults to the 'data' key:
Use a custom key to keep different datasets separate:
setMeta() and addMeta() — response metadata
Attach metadata (permissions, roles, feature flags, etc.) to the response via the meta key:
setMeta() replaces the entire meta array. addMeta() adds a single key-value pair. The meta key is only included in the response when non-empty.
respond() — merging extra data
You can pass an array to respond() to merge additional data into the response, or override existing keys entirely:
setData() optional parameters
setData() accepts two optional arguments that give you finer control over the response shape:
-
$wrapper— overrides the key name used to wrap the data in the response. By default the transformer's own$wrappervalue is used. Passing a string replaces it for that call: $format— selects a specific format key from the transformer's$formatsarray instead of auto-detecting from the current route action:
Overriding the transformer at runtime
Use setTransformer() to swap out the auto-resolved transformer for a specific call. Useful when one controller needs to serve multiple models or formats:
setCode() and error responses
setCode() automatically sets status to "error" for any code >= 400. An optional second argument sets a custom message:
Disabling auto-resolution
Both $autoResolveRepository and $autoResolveTransformer can be set to false on the subclass to disable auto-resolution when you want full manual control:
Default response format:
metais only present when metadata has been set viasetMeta()oraddMeta().
6. BaseRequest
Auto-dispatches validation rules and authorization per controller method. Includes built-in rules for pagination and sorting on all list endpoints.
Built-in rules available on all requests (from indexRules()):
| Key | Rule |
|---|---|
sort |
string |
per_page |
integer, min:1, max:100 |
with_pages |
boolean |
pagination_type |
in:paginate,none,simple,cursor |
7. BaseAuthorisationService
Property-based authorisation checks, usable standalone or from filter services.
Use skipExceptions() to collect errors instead of throwing:
Running Tests
License
MIT
All versions of laravel-api-kit with dependencies
illuminate/support Version ^10.0|^11.0|^12.0|^13.0
illuminate/database Version ^10.0|^11.0|^12.0|^13.0
illuminate/http Version ^10.0|^11.0|^12.0|^13.0