# API Platform for Laravel API Platform for Laravel (`api-platform/laravel`) is a full-featured REST and GraphQL framework that integrates the [API Platform](https://api-platform.com) ecosystem directly into Laravel applications. It auto-generates CRUD endpoints, OpenAPI/Swagger documentation, JSON-LD/Hydra hypermedia, JSON:API, and HAL responses from plain Eloquent models or plain PHP classes decorated with PHP attributes. The package wires itself into Laravel's service container via three service providers (`ApiPlatformProvider`, `ApiPlatformDeferredProvider`, `ApiPlatformEventProvider`) and requires PHP ≥ 8.2 with Laravel 11, 12, or 13. The core design centers on two extension points: **State Providers** (read data) and **State Processors** (write/delete data). Eloquent-aware default implementations ship out of the box (`CollectionProvider`, `ItemProvider`, `PersistProcessor`, `RemoveProcessor`), so annotating a model with `#[ApiResource]` is sufficient to get a fully-functional REST API with pagination, filtering, validation, content-negotiation, and OpenAPI docs. Optional packages enable GraphQL (via `api-platform/graphql`), HTTP cache invalidation (`api-platform/http-cache`), and Model Context Protocol support for AI agents (`api-platform/mcp`). --- ## Installation Install the package and publish assets/config. ```bash composer require api-platform/laravel # Publish configuration to config/api-platform.php and public assets php artisan api-platform:install ``` --- ## Configuration — `config/api-platform.php` Central configuration file controlling formats, pagination, GraphQL, Swagger UI, caching, and more. ```php // config/api-platform.php (published after install) return [ 'title' => 'My API', 'description' => 'The best API ever', 'version' => '1.0.0', // Directories scanned for #[ApiResource] classes 'resources' => [ app_path('Models'), app_path('ApiResource'), ], // Enabled response formats 'formats' => [ 'jsonld' => ['application/ld+json'], 'json' => ['application/json'], 'jsonapi' => ['application/vnd.api+json'], // opt-in 'csv' => ['text/csv'], // opt-in ], 'patch_formats' => [ 'json' => ['application/merge-patch+json'], ], // When true, PATCH replaces 'required' rules with 'sometimes' 'partial_patch_validation' => false, 'defaults' => [ 'pagination_enabled' => true, 'pagination_items_per_page' => 30, 'pagination_maximum_items_per_page' => 30, 'pagination_client_items_per_page' => false, 'pagination_client_enabled' => false, 'route_prefix' => '/api', 'middleware' => [], ], 'pagination' => [ 'page_parameter_name' => 'page', 'items_per_page_parameter_name' => 'itemsPerPage', 'enabled_parameter_name' => 'pagination', 'partial_parameter_name' => 'partial', ], 'graphql' => [ 'enabled' => false, // set true + require api-platform/graphql 'nesting_separator' => '__', 'introspection' => ['enabled' => true], 'max_query_complexity' => 500, 'max_query_depth' => 200, ], 'swagger_ui' => ['enabled' => true], 'scalar' => ['enabled' => true], 'redoc' => ['enabled' => true], // 'file' or 'apcu' recommended for production 'cache' => 'file', 'name_converter' => \Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter::class, 'exception_to_status' => [ \Illuminate\Auth\AuthenticationException::class => 401, \Illuminate\Auth\Access\AuthorizationException::class => 403, ], 'error_handler' => [ 'extend_laravel_handler' => true, // integrates with Laravel's exception handler ], // HTTP Cache (requires api-platform/http-cache) // 'http_cache' => [ // 'etag' => true, // 'max_age' => 60, // 'shared_max_age' => 3600, // ], ]; ``` --- ## `#[ApiResource]` — Exposing an Eloquent Model Attach `#[ApiResource]` to any Eloquent model (or plain PHP class) to generate full CRUD REST operations automatically. ```php 'boolean']; public function author(): BelongsTo { return $this->belongsTo(Author::class); } } // Generated REST routes (prefix /api by default): // GET /api/books → collection with pagination // POST /api/books → create // GET /api/books/{id} → single item // PUT /api/books/{id} → full replace // PATCH /api/books/{id} → partial update // DELETE /api/books/{id} → delete // Example requests: // curl https://example.com/api/books // curl "https://example.com/api/books?title=laravel&order[title]=asc&price[gte]=10&price[lte]=50" // curl -X POST https://example.com/api/books \ // -H 'Content-Type: application/ld+json' \ // -d '{"title":"Clean Code","author":"Robert Martin","isbn":"9780132350884","price":39.99}' ``` --- ## `#[ApiResource]` with a Separate DTO (ObjectMapper) Use a plain PHP DTO class as the API surface while keeping the Eloquent model as the persistence layer, via `stateOptions` and `#[Map]`. ```php inner->process($data, $operation, $uriVariables, $context); // Custom side-effect after persistence Mail::to('admin@example.com')->send(new BookCreatedMail($result)); return $result; } } // app/Models/Book.php — wire the processor: use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Post; use App\State\BookPersistProcessor; #[ApiResource( operations: [new Post(processor: BookPersistProcessor::class)] )] class Book extends Model {} ``` --- ## Artisan Generator Commands Scaffold boilerplate for custom state providers, processors, and filters using Artisan. ```bash # Generate a state provider skeleton in app/State/ php artisan make:state-provider # Generates: app/State/YourNameProvider.php # implementing ApiPlatform\State\ProviderInterface # Generate a state processor skeleton in app/State/ php artisan make:state-processor # Generates: app/State/YourNameProcessor.php # implementing ApiPlatform\State\ProcessorInterface # Generate a custom Eloquent filter skeleton in app/Filter/ php artisan make:filter # Generates: app/Filter/YourNameFilter.php # implementing ApiPlatform\Laravel\Eloquent\Filter\FilterInterface # AND automatically tags it in AppServiceProvider for DI # Export the full OpenAPI specification as JSON php artisan api:openapi:export php artisan api:openapi:export --output=openapi.json ``` --- ## `ApiPlatformMiddleware` — Operation Resolution The middleware resolves the current API Platform operation from the route and sets the response format from the URL extension (e.g., `.json`, `.jsonld`). ```php middleware(ApiPlatformMiddleware::class . ':_api_my_operation_name'); // The middleware: // 1. Reads the operation name from the route parameter // 2. Fetches the OperationMetadata and stores it in $request->attributes->get('_api_operation') // 3. Parses the format suffix (e.g., /books.json → format = 'json') and sets '_format' attribute // 4. Calls $next($request) — fully transparent to the controller // Accessing the resolved operation inside a controller or provider: public function provide(Operation $operation, array $uriVariables = [], array $context = []): mixed { $request = $context['request']; // Illuminate\Http\Request $resolvedOp = $request->attributes->get('_api_operation'); // HttpOperation instance $format = $request->attributes->get('_format'); // e.g. 'json', 'jsonld', '' } ``` --- ## Eloquent Filters ### `EqualsFilter` — Exact match filter Filters a collection by an exact value on a property, supports nested relation joins. ```php use ApiPlatform\Laravel\Eloquent\Filter\EqualsFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; use Illuminate\Database\Eloquent\Model; #[ApiResource] #[QueryParameter(key: 'status', filter: EqualsFilter::class)] #[QueryParameter(key: 'author', filter: EqualsFilter::class, property: 'author_id')] class Book extends Model {} // GET /api/books?status=published // GET /api/books?author=42 ``` --- ### `PartialSearchFilter` — LIKE %value% search Performs a case-insensitive partial string match (`LIKE '%value%'`) on the target column, including across relations. ```php use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; use Illuminate\Database\Eloquent\Model; #[ApiResource] #[QueryParameter(key: 'title', filter: PartialSearchFilter::class)] #[QueryParameter(key: 'isbn', filter: PartialSearchFilter::class, constraints: 'min:2')] class Book extends Model {} // GET /api/books?title=laravel // → WHERE title LIKE '%laravel%' // GET /api/books?isbn=978 // → WHERE isbn LIKE '%978%' ``` --- ### `StartSearchFilter` / `EndSearchFilter` — Prefix and suffix search `StartSearchFilter` matches `LIKE 'value%'`; `EndSearchFilter` matches `LIKE '%value'`. ```php use ApiPlatform\Laravel\Eloquent\Filter\EndSearchFilter; use ApiPlatform\Laravel\Eloquent\Filter\StartSearchFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; use Illuminate\Database\Eloquent\Model; #[ApiResource] #[QueryParameter(key: 'nameStart', filter: StartSearchFilter::class, property: 'name')] #[QueryParameter(key: 'nameEnd', filter: EndSearchFilter::class, property: 'name')] class Author extends Model {} // GET /api/authors?nameStart=Jo → WHERE name LIKE 'Jo%' // GET /api/authors?nameEnd=son → WHERE name LIKE '%son' ``` --- ### `BooleanFilter` — Boolean column filter Accepts `true`, `false`, `1`, or `0` as the query value and applies a `WHERE` on the target boolean column. ```php use ApiPlatform\Laravel\Eloquent\Filter\BooleanFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\TypeIdentifier; use Illuminate\Database\Eloquent\Model; #[ApiResource] #[QueryParameter( key: 'published', filter: BooleanFilter::class, nativeType: new BuiltinType(TypeIdentifier::BOOL) // advertised in OpenAPI schema )] class Book extends Model {} // GET /api/books?published=true → WHERE published = 'true' // GET /api/books?published=0 → WHERE published = '0' ``` --- ### `RangeFilter` — Numeric range filter Filters by `gt`, `lt`, `gte`, `lte` operators on a numeric column. Exposes four OpenAPI parameters automatically. ```php use ApiPlatform\Laravel\Eloquent\Filter\RangeFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; use Illuminate\Database\Eloquent\Model; #[ApiResource] #[QueryParameter(key: 'price', filter: RangeFilter::class)] #[QueryParameter(key: 'isbn', filter: RangeFilter::class, property: 'isbn')] class Book extends Model {} // GET /api/books?price[gte]=10&price[lte]=50 // → WHERE price >= 10 AND price <= 50 // GET /api/books?price[gt]=0 // → WHERE price > 0 ``` --- ### `DateFilter` — Date/datetime range filter Filters on date columns using `eq`, `gt`, `lt`, `gte`, `lte` operators. Supports an `include_nulls` context option to also return rows where the column is NULL. ```php use ApiPlatform\Laravel\Eloquent\Filter\DateFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; use Illuminate\Database\Eloquent\Model; #[ApiResource] #[QueryParameter( key: 'publishedAfter', filter: DateFilter::class, property: 'publication_date' )] #[QueryParameter( key: 'publishedAfterOrUnknown', filter: DateFilter::class, property: 'publication_date', filterContext: ['include_nulls' => true] )] class Book extends Model {} // GET /api/books?publishedAfter[gte]=2020-01-01&publishedAfter[lte]=2023-12-31 // → WHERE DATE(publication_date) >= '2020-01-01' AND DATE(publication_date) <= '2023-12-31' // GET /api/books?publishedAfterOrUnknown[gte]=2020-01-01 // → WHERE (DATE(publication_date) >= '2020-01-01' OR publication_date IS NULL) ``` --- ### `OrderFilter` — Sort filter Sorts the collection by a column ASC or DESC. Supports nested relation sorting via LEFT JOINs. ```php use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; use Illuminate\Database\Eloquent\Model; #[ApiResource] #[QueryParameter(key: 'orderByTitle', filter: OrderFilter::class, property: 'title')] #[QueryParameter( key: 'order[:property]', // wildcard for multi-column ordering filter: OrderFilter::class, properties: ['title' => 'title', 'price' => 'price'] )] class Book extends Model {} // GET /api/books?orderByTitle=asc // → ORDER BY title ASC // GET /api/books?order[price]=desc&order[title]=asc // → ORDER BY price DESC, title ASC ``` --- ### `OrFilter` — OR-condition wrapper Wraps any other filter so that multiple values are combined with `OR` instead of `AND`. ```php use ApiPlatform\Laravel\Eloquent\Filter\EqualsFilter; use ApiPlatform\Laravel\Eloquent\Filter\OrFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; use Illuminate\Database\Eloquent\Model; #[ApiResource] #[QueryParameter( key: 'name[]', filter: new OrFilter(new EqualsFilter()), property: 'name' )] class Book extends Model {} // GET /api/books?name[]=Clean+Code&name[]=Refactoring // → WHERE (name = 'Clean Code' OR name = 'Refactoring') ``` --- ### JSON:API `SortFilter` — JSON:API compliant sorting Implements the [JSON:API sort parameter](https://jsonapi.org/format/#fetching-sorting) (`sort=field,-otherField`). Requires `SortFilterParameterProvider` to parse the sort string into an ordered map. ```php use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; use Illuminate\Database\Eloquent\Model; #[ApiResource] #[QueryParameter(key: 'sort', filter: SortFilter::class)] class Book extends Model {} // GET /api/books?sort=title,-price // → ORDER BY title ASC, price DESC ``` --- ## `CollectionProvider` — Eloquent Collection State Provider Reads a paginated or full list of Eloquent models, applies query extensions (filters), handles relation links, and returns an API Platform `Paginator` or raw `Collection`. ```php where('active', true); } } // Register in AppServiceProvider: // $this->app->tag(ActiveOnlyExtension::class, QueryExtensionInterface::class); // CollectionProvider respects pagination config: // GET /api/books → paginate(30) by default // GET /api/books?page=2 → paginate page 2 // GET /api/books?itemsPerPage=5 → paginate(5) if clientItemsPerPage enabled // GET /api/books?pagination=false → get() — no pagination (if client-enabled) ``` --- ## `ItemProvider` — Eloquent Single-Item State Provider Resolves one Eloquent model by its URI variables (primary key or custom identifier), supporting relation constraints and query extensions. ```php where('tenant_id', auth()->user()->tenant_id) ->where('id', $uriVariables['id'] ?? null); } } // GET /api/books/01J3K9QW00A1B2C3D4E5F6G7H8 // → SELECT * FROM books WHERE tenant_id = ? AND id = ? LIMIT 1 // Returns 404 automatically if null (no result) ``` --- ## `PersistProcessor` — Eloquent Save Processor Handles `POST`, `PUT`, and `PATCH` write operations. Automatically manages `BelongsTo`, `HasMany`, and `MorphMany` relations by saving related models and associating/dissociating as needed. ```php saveOrFail() then $book->refresh() // For PUT (full replacement), PersistProcessor preserves the primary key from the // existing record ($previousData) to prevent key conflicts: // PUT /api/books/42 → $data->id is forced to 42 even if omitted in body // RemoveProcessor (for DELETE) is equally simple: // DELETE /api/books/42 → $book->delete() → returns null (204 No Content) ``` --- ## `ValidateProvider` — Laravel Validation Integration Wraps the deserialization pipeline to run Laravel validation rules (array rules or a `FormRequest` class) before the data reaches the processor. ```php 'required|string|max:255', 'isbn' => 'required|string|unique:books,isbn', 'price' => 'required|numeric|min:0', ]) ] )] class Book extends Model {} // Validation via FormRequest class (preferred — supports authorization too): // app/Http/Requests/BookFormRequest.php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class BookFormRequest extends FormRequest { public function authorize(): bool { return true; } public function rules(): array { return [ 'title' => 'required|string|max:255', 'isbn' => 'required|string', 'price' => 'numeric|min:0', ]; } } // Wire to the resource: #[ApiResource(rules: BookFormRequest::class)] class Book extends Model {} // On validation failure the API returns HTTP 422 with application/problem+json: // { // "type": "https://tools.ietf.org/html/rfc2616#section-10", // "title": "An error occurred", // "detail": "title: This value should not be blank.", // "violations": [{"propertyPath": "title", "message": "This value should not be blank."}] // } ``` --- ## `ResourceAccessChecker` — Laravel Gate / Policy Integration Bridges API Platform's security expressions (`security`, `securityPostDenormalize`) with Laravel's `Gate::allows()` / policies. ```php id === $book->user_id; } public function delete(User $user, Book $book): bool { return $user->is_admin; } } // Register in AuthServiceProvider: // protected $policies = [Book::class => BookPolicy::class]; // Use security expressions on operations: use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Patch; use Illuminate\Database\Eloquent\Model; #[ApiResource( operations: [ new Patch(security: 'is_granted("update", object)'), new Delete(security: 'is_granted("delete", object)'), ] )] class Book extends Model {} // is_granted("update", object) calls Gate::allows('update', $book) // → resolves to BookPolicy::update($currentUser, $book) // Returns HTTP 403 if denied ``` --- ## HTTP Routes — Auto-Generated Endpoints All API Platform routes are registered automatically via `routes/api.php` during the `boot()` phase. The route prefix, domain, and middleware are all configurable. ```php // All routes are generated at boot time; these are the built-in special routes: // (assuming route_prefix = '/api') // GET /api/ → Hydra API entrypoint (list of all resource IRIs) // GET /api/docs{.format?} → OpenAPI documentation (JSON / HTML / Swagger UI) // GET /api/contexts/{shortName} → JSON-LD context document // GET /api/.well-known/genid/{id} → Skolem IRI (blank node support) // When GraphQL is enabled: // GET|POST /api/graphql → GraphQL endpoint // GET /api/graphiql → GraphiQL browser IDE // MCP endpoint (when api-platform/mcp is installed): // GET|POST|DELETE|OPTIONS /mcp → Model Context Protocol for AI agents // Customise the prefix and add global middleware in config: // 'defaults' => ['route_prefix' => '/v1'], // 'routes' => ['middleware' => ['auth:sanctum']], // curl examples: // curl https://example.com/api/ // curl https://example.com/api/docs.json // curl -H 'Accept: application/ld+json' https://example.com/api/books // curl https://example.com/api/books.jsonld // curl https://example.com/api/books.json?page=2&itemsPerPage=10 ``` --- ## GraphQL Support Enable GraphQL by installing `api-platform/graphql` and setting `graphql.enabled = true`. Operations are declared with GraphQL-specific attributes. ```php ['enabled' => true] use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Query; use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\QueryParameter; use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter; use Illuminate\Database\Eloquent\Model; #[ApiResource( graphQlOperations: [ new Query(), // book(id: ID!): Book new QueryCollection( // books(...): BookConnection paginationItemsPerPage: 5, parameters: [ new QueryParameter(key: 'order[:property]', filter: OrderFilter::class), ] ), new QueryCollection( paginationItemsPerPage: 3, name: 'simplePagination', paginationType: 'page', // cursor-free page-based pagination ), new Mutation(name: 'create'), // createBook(input: CreateBookInput!): CreateBookPayload ] )] class Book extends Model {} // GraphQL query example: // POST /api/graphql // Content-Type: application/json // { // "query": "{ books { edges { node { id title price } } totalCount } }" // } // Mutation example: // { // "query": "mutation { createBook(input: {title: \"DDD\", price: 49.99}) { book { id title } } }" // } ``` --- ## MCP (Model Context Protocol) Support Expose API resources as AI-agent tools via the Model Context Protocol by installing `api-platform/mcp`. ```php ['enabled' => true] // All #[ApiResource] classes are automatically registered as MCP tools. // The /mcp endpoint handles GET/POST/DELETE/OPTIONS using the Symfony MCP bundle controller. // To mark a specific resource as an MCP tool with custom metadata: use ApiPlatform\Mcp\Attribute\McpTool; use ApiPlatform\Metadata\ApiResource; use Illuminate\Database\Eloquent\Model; #[ApiResource] #[McpTool(description: 'Manages the book catalog for the library system')] class Book extends Model {} // AI agent interaction (MCP JSON-RPC 2.0): // POST /mcp // { // "jsonrpc": "2.0", // "method": "tools/call", // "params": { // "name": "listBooks", // "arguments": {"page": 1, "itemsPerPage": 5} // }, // "id": 1 // } ``` --- ## Custom Eloquent Filter via `make:filter` Implement `FilterInterface` to create reusable, tagged, auto-wired Eloquent query modifiers. ```php $builder */ public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder { if ('true' === $values || '1' === $values) { return $builder->where('deleted_at', null)->where('status', 'active'); } return $builder; } public function getSchema(Parameter $parameter): array { return ['type' => 'boolean']; } } // The make:filter command also auto-tags the filter in AppServiceProvider. // Use it in a resource: use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\QueryParameter; use App\Filter\ActiveFilter; use Illuminate\Database\Eloquent\Model; #[ApiResource] #[QueryParameter(key: 'active', filter: ActiveFilter::class)] class Book extends Model {} // GET /api/books?active=true → WHERE deleted_at IS NULL AND status = 'active' ``` --- ## Summary API Platform for Laravel provides a convention-over-configuration approach to building hypermedia-driven REST and GraphQL APIs on top of Laravel and Eloquent. The primary workflow is to annotate Eloquent models (or plain PHP classes) with `#[ApiResource]` and optional `#[QueryParameter]` filter attributes; the framework then auto-generates routes, OpenAPI/Swagger documentation, JSON-LD/Hydra/HAL/JSON:API serialization, pagination, and input validation with zero boilerplate. Developers can customise every layer through custom State Providers, Processors, and Eloquent Query Extensions, all of which are auto-discovered and tagged in Laravel's service container via the deferred `ApiPlatformDeferredProvider`. Integration patterns typically follow one of three paths: (1) **direct Eloquent exposure** — attach `#[ApiResource]` directly to a Model for the fastest path to a working CRUD API; (2) **DTO pattern** — separate the API surface from persistence using a plain PHP DTO with `#[Map]` and `stateOptions: new Options(modelClass: ...)`, ideal for data transformation or security boundaries; (3) **fully custom providers/processors** — implement `ProviderInterface` / `ProcessorInterface` for non-Eloquent data sources, microservice aggregation, or complex write workflows. All three patterns support the same filter, validation, security, pagination, serialization, and documentation features, and can be incrementally composed using Laravel's service container decoration capabilities.