sinemacula / laravel-api-toolkit
A comprehensive Laravel toolkit for streamlined development of RESTful APIs
Requires
- php: ^8.3
- aws/aws-sdk-php: ^3.342
- illuminate/database: *
- illuminate/http: *
- illuminate/notifications: *
- illuminate/routing: *
- illuminate/support: ^12.9
- illuminate/validation: *
- phpnexus/cwh: ^3.0
- sinemacula/laravel-repositories: ^1.0
- sinemacula/laravel-resource-exporter: ^1.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.85
- laravel/framework: ^12.9
- phpstan/phpdoc-parser: ^2.2
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.0
- squizlabs/php_codesniffer: ^3.13
This package is auto-updated.
Last update: 2026-07-30 20:06:57 UTC
README
The Laravel API Toolkit is a comprehensive package designed to simplify the development of RESTful APIs in Laravel. It provides tools to enhance API functionality, improve error handling, and ensure consistent data output, making API development faster and more reliable.
Features
- Exception Handling: Implements a custom exception handler that captures and formats all exceptions for consistent API error responses, preserving the intended HTTP status codes.
- Queryable Resources: Resource schemas give fine-tuned control over which fields, filters, relations, and orderings are exposed via your API endpoints under a fail-closed allowlist posture, enhancing security and customization.
- Data Repositories: Abstracts database interactions into repositories to promote a cleaner and more maintainable codebase, with safe-by-default deferred writes (failed flushes retain records rather than dropping them) and per-query caching (each query is cached against its own fingerprint, so a cache hit performs zero database queries and a filtered read never returns the full table).
- Data Resources: Schema-driven resources ensure consistent presentation of data across different API endpoints, simplifying client-side data integration.
- Services: A composable service layer with immutable configuration, cross-cutting concerns (transactions, locking), and self-describing results.
Installation
To install the Laravel API Toolkit, run the following command in your project directory:
composer require sinemacula/laravel-api-toolkit
Configuration
After installation, publish the package configuration to customize it according to your needs:
php artisan vendor:publish --provider="SineMacula\ApiToolkit\ApiServiceProvider" --tag=config
This publishes config/api-toolkit.php to your application's config directory. The file is documented inline
and covers exception rendering strategy, sensitive-key redaction, query-access posture, query parser limits,
deferred write behaviour, middleware toggles, and more. Per-query repository caching is configured in the
sinemacula/laravel-repositories package under repositories.cache.
Usage
API Query Parser
The ApiQueryParser sits behind the ApiQuery facade and is populated automatically by the ParseApiQuery
middleware, which the service provider registers globally by default (controlled via api-toolkit.parser.register_middleware).
Sparse fieldsets - request only the fields you need for a given resource type:
GET /users?fields[user]=id,name,email
use SineMacula\ApiToolkit\Facades\ApiQuery; $fields = ApiQuery::getFields('user'); // ['id', 'name', 'email']
Filtering - apply column-level filters by passing a URL-encoded JSON object of operator tokens:
GET /users?filters={"status":{"$eq":"active"},"created_at":{"$ge":"2024-01-01"}}
The filters value must be a JSON string; requests carrying a non-JSON value are rejected with a validation
error.
Available built-in operator tokens: $eq, $neq, $gt, $lt, $ge, $le, $like, $in, $between,
$contains, $null, $notNull.
Sorting - sort by one or more columns, with optional direction:
GET /users?order=last_name,first_name:desc
Limit clamping - client-supplied ?limit values are silently clamped to the api-toolkit.parser.max_limit
ceiling (default 100). Values exceeding the ceiling are reduced; the request is never rejected:
GET /users?limit=200 // clamped to 100 GET /users?limit=25 // honoured as-is
Relation aggregates - request counts, sums, or averages over declared relations:
GET /users?counts[user]=memberships,posts GET /accounts?sums[account][transaction]=amount GET /accounts?averages[account][order]=total
ApiQuery::getCounts('user'); // ['memberships', 'posts'] ApiQuery::getSums('account'); // ['transaction' => ['amount']] ApiQuery::getAverages('account'); // ['order' => ['total']]
Cursor-based pagination is enabled by adding ?pagination=cursor (or including a ?cursor token); offset
pagination is used otherwise.
Schema-Driven ApiResource
Extend ApiResource and declare a schema() method using the Field, Relation, Count, Sum, and Average
schema helpers. The compiled schema drives field resolution, guard evaluation, and eager-load planning automatically.
use App\Models\User; use SineMacula\ApiToolkit\Attributes\ForModel; use SineMacula\ApiToolkit\Http\Resources\ApiResource; use SineMacula\ApiToolkit\Schema\Count; use SineMacula\ApiToolkit\Schema\Field; use SineMacula\ApiToolkit\Schema\Relation; use SineMacula\ApiToolkit\Schema\Sum; #[ForModel(User::class)] class UserResource extends ApiResource { const RESOURCE_TYPE = 'user'; protected static array $default = ['id', 'name', 'email', 'created_at']; public static function schema(): array { return Field::set( Field::scalar('name')->filterable()->sortable(), Field::scalar('email')->filterable(), Field::timestamp('created_at')->sortable(), Field::compute('full_name', 'getFullName'), Relation::to('organization', OrganizationResource::class)->traversable(), Count::of('memberships'), Sum::of('orders', 'total'), ); } }
Fields marked filterable() or sortable() are exposed under the allowlist posture. Relations marked
traversable() can be targeted by nested filters. The $default static property declares the fields returned
when the client sends no ?fields parameter.
Model binding and discovery - the #[ForModel(...)] attribute binds a resource to its model, and may be
repeated to bind one resource to several models. By default (api-toolkit.resources.paths left null) resources
are discovered at boot from the application's own Http/Resources directory plus each module's, resolved from
app_path() so a modular application is covered with no configuration, and compiled into the model-to-resource
map automatically so no central map needs maintaining. Set paths to an explicit array to override the scanned
roots, or an empty array to disable discovery. An explicit api-toolkit.resources.resource_map entry always
wins over a discovered binding - use it as the canonical-resource tiebreak when a model has more than one
resource, or to bind resources living outside the scanned paths. When two discovered resources claim the same model and no
explicit entry resolves it, the first (in sorted file order) wins and a warning is logged; with
validate_schemas enabled the conflict fails the boot instead.
Eager-load planning - the resource builds with()/withCount()/withSum()/withAvg() maps from the
resolved field set, so relations are loaded precisely and automatically:
// Build a with() map for the active field set $with = UserResource::eagerLoadMapFor(UserResource::resolveFields());
Field-set control at instantiation:
new UserResource($user, loadMissing: true); // eager-loads missing relations new UserResource($user, included: ['id', 'email']); // explicit field set new UserResource($user, excluded: ['email']); // field set minus exclusions (new UserResource($user))->withAll(); // all schema fields
Schema validation - api-toolkit.resources.validate_schemas has all registered schemas validated
during application boot. It defaults to enabled outside production (set VALIDATE_SCHEMAS=false to opt
out) and off in production, where the boot cost is not worth paying. The api-toolkit:validate-schemas
Artisan command runs the same validation on demand - independently of the flag - so it can also gate CI.
Repositories
Extend ApiRepository to get a repository wired to the API query parser, eager-load planning, and
pagination out of the box:
use SineMacula\ApiToolkit\Repositories\ApiRepository; class UserRepository extends ApiRepository { public function model(): string { return User::class; } }
Call withApiCriteria() before any read to apply the parsed filters, sorts, eager loads, and limit from the
current request automatically:
$users = $repository->withApiCriteria()->paginate();
Allowlist posture - by default (api-toolkit.repositories.query_posture = 'allowlist') only schema fields
declared filterable(), sortable(), or traversable() are accepted. Undeclared keys are rejected with a
validation error (controlled by api-toolkit.repositories.reject_undeclared). Switch to 'blocklist' to
restore the opt-out behaviour and exclude specific columns via api-toolkit.repositories.searchable_exclusions.
Cacheable trait - add per-query transparent caching to any ApiRepository subclass:
use SineMacula\Repositories\Concerns\Cacheable; class UserRepository extends ApiRepository { use Cacheable; protected int $cacheTtl = 3600; protected ?string $cacheStoreName = null; // uses app default protected bool $cacheReferenceTable = false; // whole-table reference mode }
Read results are keyed by query fingerprint; write operations invalidate the table automatically. Call
withoutCache() to bypass the cache for a single read, or flushCache() to invalidate immediately.
ReferenceCache is enabled by setting protected bool $cacheReferenceTable = true on a Cacheable
repository. In reference mode the full table is loaded once and memoised in-process; single-record lookups
resolve in O(1) without touching the database. Use this only for small, rarely-changing lookup tables.
Deferrable trait - buffer insert operations in memory and flush them as bulk INSERT statements at the
lifecycle boundary (RequestHandled, CommandFinished, JobProcessed, or JobFailed):
use SineMacula\ApiToolkit\Repositories\Concerns\Deferrable; class AuditRepository extends ApiRepository { use Deferrable; } // In your service: $auditRepository->defer(['user_id' => 1, 'action' => 'login']);
The default on_failure = 'collect' strategy retains failed records for the next boundary flush rather than
dropping them. The 'throw' strategy raises a WritePoolFlushException for callers that own an explicit flush
site. The 'log' strategy is best-effort only - use it solely for genuinely disposable writes such as
telemetry.
The WritePool is a scoped singleton, so after a deferred write you can call
app(WritePool::class)->lastAutoFlushResult() to observe the outcome of the most recent automatic flush
(null if none has occurred). Under the non-throwing 'collect' and 'log' strategies this accessor is the
only in-process signal that an auto-flush failed.
Filter-Operator Registry
The OperatorRegistry singleton maps token strings to FilterOperator handler instances. Register custom
operators in a service provider's boot() method:
use SineMacula\ApiToolkit\Repositories\Criteria\OperatorRegistry; use SineMacula\ApiToolkit\Contracts\FilterOperator; class AppServiceProvider extends ServiceProvider { public function boot(OperatorRegistry $registry): void { $registry->register('$regex', new RegexOperator); // Override an existing operator $registry->override('$like', new CaseInsensitiveLikeOperator); } }
A FilterOperator is any class implementing SineMacula\ApiToolkit\Contracts\FilterOperator, or a closure
with the same signature. Operators registered via register() throw InvalidArgumentException if the token
is already taken; use override() to replace unconditionally.
Exception Handling
Register the exception handler once in bootstrap/app.php:
use SineMacula\ApiToolkit\Exceptions\ApiExceptionHandler; ->withExceptions(function (Exceptions $exceptions): void { ApiExceptionHandler::handles($exceptions); })
All exceptions are mapped to typed ApiException subclasses and rendered as consistent JSON error responses
with appropriate HTTP status codes. The rendering strategy is configurable:
'auto'(default) - renders JSON unless the request does not expect JSON and debug mode is on.'always_json'- always renders JSON.'json_when_expected'- renders JSON only when the request expects a JSON response (Laravel'sexpectsJson(), e.g.Accept: application/json).
Sensitive-key redaction - request data written to the exception log is automatically scanned and values
whose keys match any substring in api-toolkit.exceptions.sensitive_keys (default: password, token,
secret, authorization) are replaced with [redacted] before being written to the log. Add application-specific
keys to that array in your published config to extend coverage.
Middleware
The service provider registers the following middleware automatically. Each registration can be disabled
independently in api-toolkit.middleware:
maintenance_mode_swap- JSON503maintenance responses with anexceptURI allowlist.json_pretty_print- opt-in pretty-printed JSON responses via a query parameter.throttle- API-friendly rate-limit responses; auto-selects the Redis variant when Redis is the cache driver.
json_pretty_print accepts 'scope': 'global' (default, pushed to the global stack) or 'scope': 'api'
(appended to the api middleware group only). maintenance_mode_swap is always prepended to the global
stack when enabled, and throttle is registered as the router's throttle alias, so neither takes a scope.
Typed request capabilities (soft-delete visibility via includeTrashed() / onlyTrashed()) are available
on demand through SineMacula\ApiToolkit\Http\RequestCapabilities::fromRequest($request), which resolves
and caches them lazily on first access - no middleware registration is required.
Request throttling and rate-limit keying - each request is keyed by method, host, path, and caller
identity. Authenticated requests are keyed by the user identifier; guests are keyed by their client IP
($request->ip()), matching Laravel's stock ThrottleRequests. Guests are deliberately not pooled into a
single shared bucket, which would let one anonymous caller exhaust the rate limit for every other guest.
Behind a shared-IP proxy, load balancer, CDN, or NAT, per-IP guest keying can over-throttle many distinct
callers that share one egress IP. Configure Laravel's TrustProxies middleware so that $request->ip()
resolves the real client IP rather than the proxy's.
To key guests by an application-specific identifier (for example an API key) instead of their IP, set
api-toolkit.middleware.throttle.class to your own middleware that uses the ThrottleRequestsTrait and
overrides resolveRequestSignature(). That config option is the supported customisation point.
Schema Introspection and OpenAPI Export
The schema compiler resolves filterable columns, sortable columns, traversable relations, and all field keys
for any registered resource without instantiating it. A complementary database-schema introspector resolves
model columns, searchable columns, and relations; it is used internally by ApiCriteria and is available
for injection:
use SineMacula\ApiToolkit\Contracts\SchemaIntrospectionProvider; public function __construct(private SchemaIntrospectionProvider $introspector) {}
An OpenAPI 3.1 components document can be generated from the registered resource map and operator grammar:
php artisan api-toolkit:export-openapi php artisan api-toolkit:export-openapi --output=openapi.json
Upgrading
See UPGRADE.md for version-by-version migration guides, including breaking changes and the steps required to move from 1.x to 2.x.
Requirements
- PHP ^8.3
- Laravel 12+
Testing
composer test
composer test:coverage
composer check
composer format
composer smells
Changelog
See CHANGELOG.md for a list of notable changes, and UPGRADE.md for version upgrade guides.
Contributing
Contributions are welcome. Please read CONTRIBUTING.md for guidelines on branching, commits, code quality, and pull requests.
Security
If you discover a security vulnerability, please report it responsibly. See SECURITY.md for the disclosure policy and contact details.
License
Licensed under the Apache License, Version 2.0.