Download the PHP package adonyarik/consistent-api without Composer
On this page you can find all versions of the php package adonyarik/consistent-api. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download adonyarik/consistent-api
More information about adonyarik/consistent-api
Files in adonyarik/consistent-api
Package consistent-api
Short Description Laravel toolkit for lean REST APIs: modular structure, JSON middleware, pagination and sorting.
License MIT
Informations about the package consistent-api
Consistent API
Laravel toolkit for lean, consistent REST APIs: modular route structure, CRUD controller, advanced filtering (including relations) and sorting (including relations), pagination, JSON/multipart middleware, debug responses, and PostgreSQL ENUM helpers.
| Requirement | Version |
|---|---|
| PHP | ^8.1 |
| Laravel | ^10 / ^11 / ^12 / ^13 |
Package: adonyarik/consistent-api
Namespace: Adonyarik\ConsistentApi
Table of contents
- Consistent API
- Table of contents
- Installation
- Configuration
config/consistentapi.phpconfig/pagination.phpconfig/filters.php- Modular structure
- Rate limiting
- Artisan commands
consistent:crudconsistent:rebuild- Models (
CrudModel) - Disable pagination for a model
- CRUD controller
- Methods
- Filtering
- Simple columns
- Nested operators
- Filtering through relations
- Validation of filter input
- Sorting
- Simple columns
- Sorting through relations
- Search request (
BaseSearchRequest) - Startup validation of
#[Filterable]/#[Sortable] - Pagination and responses
- Middleware
- Example usage
- Debugger
- Route macro
development - PostgreSQL ENUM
- DB macros
- Blueprint macros
- Extra traits
EnumHelpersCredibility- Package structure
- Quick start checklist
- License
Installation
The service provider is registered via Laravel package discovery:
Adonyarik\ConsistentApi\ConsistentApiProvider
It automatically boots:
ModuleServiceProvider— module routes and theapirate limiterMacroServiceProvider—Route::development()PostgresEnumServiceProvider— PostgreSQL ENUM macros (migration/test context)
Publish the config files:
This creates:
config/consistentapi.phpconfig/pagination.phpconfig/filters.php
Configuration
config/consistentapi.php
| Key | Default | Description |
|---|---|---|
modules_folder |
Modules |
Modules directory relative to app/ |
request_limit |
60 |
Max requests per minute for the api rate limiter |
api_url_prefix |
api |
URL prefix for module routes |
middlewares |
['api'] |
Middleware stack applied to module routes |
debugger_enabled |
env('DEBUGGER_ENABLED', false) |
Enable the debug block in JSON responses |
Example .env:
config/pagination.php
| Key | Default | Description |
|---|---|---|
data_container_name |
items |
Data array key in the response |
meta_container_name |
meta |
Pagination metadata key |
per_page |
sm/default/md/lg/xl → 10/15/25/50/100 |
Allowed perpage values |
config/filters.php
| Key | Default | Description |
|---|---|---|
operators |
['eq', 'like', 'from', 'to', 'in'] |
Global whitelist of nested filter operators the package will accept. Uncomment not_eq / min / max / not_in / null to enable them. |
validate_on_boot |
env('FILTERS_VALIDATE_ON_BOOT', true) |
Scan models on boot and throw if #[Filterable] declares an operator that isn't enabled above, or a broken relation. |
model_paths |
[app_path('Models'), app_path('Modules/*/Models')] |
Directories scanned for models during startup validation. Supports a single * wildcard segment (e.g. modular apps). |
An operator listed in operators must also be implemented in CanFilter::applyArrayFilter(), otherwise it is silently ignored at the trait level even if it passes validation.
Startup validation only runs in local / testing environments by default (see Startup validation); it never runs on every production request, so scanning the filesystem is not a performance concern there.
Modular structure
The package loads modules from app/{modules_folder} (default: app/Modules).
Example layout:
Module folders use the plural StudlyCase name of the model (Post → Posts, Company → Companies).
Each module Routes.php (and the optional root Routes.php) is loaded with:
- prefix from
consistentapi.api_url_prefix(e.g.api) - middleware from
consistentapi.middlewares(e.g.api)
A folder named Middleware inside the modules directory is skipped.
If the modules directory does not exist, the provider does not fail — routes are simply not loaded.
Rate limiting
On boot, the package registers a limiter named api:
This may override your application's default api limiter. Adjust request_limit, or redefine the limiter in your AppServiceProvider / bootstrap/app.php if needed.
Artisan commands
Both commands use the consistent:{action} naming format.
| Command | Purpose |
|---|---|
php artisan consistent:crud {model} |
Scaffold a full CRUD module |
php artisan consistent:rebuild |
Move existing Laravel API classes into modules |
consistent:crud
Creates a ready-to-extend module for the given model name:
Generated layout for Post:
- Model extends
CrudModelwith empty#[Fillable([])]/#[Hidden([])]/#[Filterable([])]/#[Sortable([])]declarations (Laravel 13+ for Eloquent attributes) - Controller extends
CrudControllerwithindex/show/store/update/destroy - Search request extends
BaseSearchRequestand points$modelat the generated model, so relation/operator validation rules are built automatically Routes.phpregisters REST routes under the plural URI (posts) with{post}route-model binding- Without
--force, existing target files cause the command to fail
consistent:rebuild
Migrates a conventional Laravel layout into the same plural module structure:
- Models from
app/Models - Controllers from
app/Http/Controllers(andapp/Controllers) - Requests from
app/Http/Requests(andapp/Requests) - Resources from
app/Http/Resources(andapp/Resources)
Also:
- Rewrites namespaces and class references under
app/,routes/,database/, andtests/ - Moves matching route statements from
routes/api.phpinto each module'sRoutes.php
Example:
Post + PostController become App\Modules\Posts\Models\Post and App\Modules\Posts\Controllers\PostController.
Models (CrudModel)
Base API model:
Features:
CanFilterandCanSorttraitsHasFactorywith lookup forDatabase\Factories\{Model}FactoryleftJoinOnce()— left join without duplicatesgetAllColumns()— table column listing
Disable pagination for a model
Implement the contract:
Then, with paginate=false (or 0), indexLogic returns the full list without pagination meta.
CRUD controller
Extend Adonyarik\ConsistentApi\Controllers\CrudController and set:
$resourceClass— API Resource class$relationFunctions— relations forwith/load(optional)
Methods
| Method | Purpose | Response |
|---|---|---|
indexLogic |
List + filter/sort/paginate | PaginatedJsonResponse or non-paginated JSON |
selectLogic |
Single record | JsonResource |
storeLogic |
Create | 201 + resource |
updateLogic |
Update | JsonResource |
destroyLogic |
Delete | 204 No Content |
Models passed into these methods must extend CrudModel.
indexLogic validates filter/sort against the model's #[Filterable] / #[Sortable] declarations before the query is built. Any disallowed column, disallowed nested operator, or nesting on a column that doesn't support it returns a 422 with Laravel-style validation errors — the query is never executed with unverified input.
Filtering
Declare allowed columns on the model with #[Filterable]:
Simple columns
A plain string ('name') allows only scalar filtering, applied as LIKE '%value%' (ILIKE on PostgreSQL):
Nested operators
A column can be declared with an array of allowed operators, optionally paired with validation rules for that operator's value:
Supported operators (must also be enabled in config('filters.operators')):
| Operator | Behaviour |
|---|---|
eq |
column = value |
not_eq |
column != value |
like |
column LIKE/ILIKE '%value%' |
from |
column >= start of day(value) (date-aware) |
to |
column <= end of day(value) (date-aware) |
min |
column >= value |
max |
column <= value |
in |
column IN (values) — comma string or array |
not_in |
column NOT IN (values) — comma string or array |
null |
whereNull / whereNotNull based on boolean value |
A plain list of scalar values without operator keys is treated as in:
Attempting to nest a column that was declared as a plain string, or using an operator that isn't in its declared list, returns a 422.
Filtering through relations
Use RelationFilter for columns backed by a foreign key or a relationship, instead of the raw column:
Internally this runs whereHas($relation, ...) matching $column with LIKE/ILIKE (default) or = when operator: 'eq' is set — this works for any relation type (BelongsTo, HasMany, BelongsToMany, etc.) without duplicating rows. Nested operators (filter[user][eq]=...) on relation columns are not yet supported and return a 422.
Validation of filter input
BaseSearchRequest automatically builds validation rules from the model's #[Filterable]:
- Any operator not enabled in
config('filters.operators')→422. - Any per-operator rules declared on the model (e.g.
numeric,date,gte:filter.price.min) are applied to the correspondingfilter.column.operatorinput. - Columns declared as
RelationFilterare excluded from value-rule generation (there is no scalar value shape to validate beyond the base type checks).
Sorting
Declare allowed columns on the model with #[Sortable]:
Simple columns
Sorting through relations
RelationSort inspects the relationship type and picks the right strategy automatically:
BelongsTo/HasOne— a single related record, resolved with aleftJoinOnce()join, thenORDER BY {related_table}.{column}.BelongsToMany/HasMany— potentially multiple related records. To avoid row duplication from aJOIN, the values are aggregated into a single comma-separated string per parent row via a correlated subquery (STRING_AGGon PostgreSQL,GROUP_CONCATon MySQL), and the main query orders by that subquery — no join on the outer query at all.
Sorting on a relation that isn't BelongsTo / HasOne / BelongsToMany / HasMany is silently ignored.
MySQL note:
GROUP_CONCATtruncates atgroup_concat_max_len(1024 bytes by default). If a parent row can have many related values, consider raising this session/server variable.
Search request (BaseSearchRequest)
Setting $model lets BaseSearchRequest build the filter.* value-validation rules straight from that model's #[Filterable] declaration (see Validation of filter input). This is done automatically by the consistent:crud stub.
Base rules (always applied, regardless of $model):
| Parameter | Rules |
|---|---|
perpage |
numeric value from config('pagination.per_page') |
paginate |
true / false / 0 / 1 |
sort |
array |
sort.* |
asc or desc |
filter |
array |
filter.* |
nullable; if an array, its keys must all be enabled in config('filters.operators') |
Example request:
Behaviour at the controller level:
- Filtering:
LIKE/ILIKE(PostgreSQL) on columns allowed by#[Filterable], or the operator/relation logic described above - Sorting:
orderByon columns allowed by#[Sortable], or the relation logic described above - Disallowed columns, disallowed nested operators, or filter/sort on a non-filterable/non-sortable model →
422with Laravel-style validation errors
Simple array-based whitelisting (without the attribute) still works:
An empty array means filtering/sorting is disabled.
Startup validation of #[Filterable] / #[Sortable]
When config('filters.validate_on_boot') is true and the app is running in local or testing, ConsistentApiProvider::boot() scans every model under config('filters.model_paths') and eagerly checks:
- Every operator declared in a
#[Filterable]column exists inconfig('filters.operators')→ otherwiseInvalidFilterableOperatorException. - Every
RelationFilterpoints to a method that exists on the model and actually returns an EloquentRelation→ otherwiseInvalidRelationFilterException.
This catches typos (min used but not enabled in config, relation: 'usre', pointing a RelationFilter at a non-relation method) at boot time instead of on the first matching request.
model_paths supports a single * wildcard segment, which makes it work with both conventional (app/Models) and modular (app/Modules/*/Models) project layouts out of the box.
Disable this check entirely (e.g. to skip the filesystem scan) with:
Pagination and responses
PaginatedJsonResponse produces JSON like:
The items / meta keys are configurable in config/pagination.php.
Without pagination (contract + paginate=false):
Middleware
Aliases are registered automatically:
| Alias | Class | Purpose |
|---|---|---|
consistent.api-json |
ApiJsonMiddleware |
Sets Accept: application/json for API-prefixed URLs |
consistent.ensure-json |
EnsureJsonMiddleware |
Requires JSON Content-Type for POST / PUT / PATCH |
consistent.ensure-multipart |
EnsureMultipartMiddleware |
Requires multipart/form-data for POST |
consistent.debugger |
DebuggerMiddleware |
Appends a debugger block to JSON responses |
Example usage
Laravel 11+:
Or in routes:
Attach EnsureMultipartMiddleware only to upload endpoints: any non-POST request or missing multipart Content-Type returns 415.
Debugger
- Set
DEBUGGER_ENABLED=true - Apply the
consistent.debuggermiddleware to the routes/group you need
JSON responses will include:
Do not enable the debugger in production unless you intend to expose SQL, bindings, and request input.
Route macro development
Routes available only in the local environment:
In production / staging the callback is not executed.
PostgreSQL ENUM
Macros are active during migrations (artisan migrate*) and tests (pest / phpunit).
DB macros
Blueprint macros
Failures throw Adonyarik\ConsistentApi\Exceptions\PostgresEnumException (e.g. typeAlreadyExists, typeMissing, columnHasInvalidValues, valueNotAllowed, typeStillReferenced, typeSharedAcrossTables).
Extra traits
EnumHelpers
For PHP backed enums:
Credibility
Assert that a related model "belongs" to the current one (matching IDs):
Package structure
Quick start checklist
composer require adonyarik/consistent-apiphp artisan vendor:publish --tag=consistent-api-configphp artisan consistent:crud Post(orconsistent:rebuildfor an existing app)- Fill in
#[Fillable(...)]/#[Hidden(...)]/#[Filterable(...)]/#[Sortable(...)]and request validation rules (Eloquent attributes require Laravel 13+) - For foreign-key/relation columns, use
RelationFilter/RelationSortinstead of plain column names - Enable any extra nested operators you need in
config/filters.php - Optionally add middleware aliases to your
apigroup - For debugging:
DEBUGGER_ENABLED=true+consistent.debugger
License
MIT © Yaroslav Tyrchenko