outlandish/rest-bundle
Outlandish REST Bundle is a Symfony bundle that streamlines REST API development, providing JSON request/response handling, serialization, error handling, and routing conveniences to reduce boilerplate in Symfony applications.
First, a reality check that changes everything about "day-to-day" usage: outlandish/rest-bundle is a Symfony 2 bundle, not a Laravel package. It targets symfony/symfony ~2.3, Doctrine ORM, and the Symfony Form/Serializer stack of 2013–2014. It is archived, the last tag was 2014-02-21, it has 2 stars and 0 dependents. You will not composer require this into a Laravel app; on PHP 8 the constraint resolution fails before you get to the interesting part.
So the realistic getting-started path is one of two:
A. You are maintaining a legacy Symfony 2.3 app that already has it. Then:
DependencyInjection/*Extension.php) — that is where the config tree lives: which entities are exposed, which fields are writable, and the route prefix.Resources/config/routing.yml or the @RestBundle/... route annotation on the generic controller). That tells you the URL shape — typically /{prefix}/{entity} and /{prefix}/{entity}/{id}.ClassMetadata from the EntityManager, binds the request into a form, and serializes the result.app/console doctrine:mapping:info and app/console router:debug | grep -i rest to confirm what is actually exposed. The config-to-route mapping is the thing that surprises people.B. You are a Laravel developer evaluating it as a design reference. Then clone it into a scratch directory, read the generic controller and the config tree, and stop. The value is the pattern — "one controller, metadata-driven, all entities" — not the code.
First real use case in either case: read-only CRUD for a small set of back-office Doctrine entities (a Country, a Tag, a Category) where you want GET /api/tag and GET /api/tag/7 with almost no code. Anything beyond that — auth rules, computed fields, nested writes — is where it stops paying for itself.
The bundle embodies an "entity == resource" philosophy. If you are reproducing it in Laravel (which is what you should actually do), map each piece deliberately:
Generic resource controller, not per-model controllers. The bundle ships one controller that derives the entity from the route and resolves everything at runtime. In Laravel the equivalent is an abstract base controller or a single invokable controller plus a resource registry:
// config/rest.php
return [
'tag' => ['model' => Tag::class, 'read' => ['id','name','slug'], 'write' => ['name']],
'category' => ['model' => Category::class, 'read' => ['id','name'], 'write' => ['name']],
];
Then Route::apiResource('{resource}', GenericRestController::class) with a whereIn constraint. You get the bundle's ergonomics without its coupling.
Metadata-driven field exposure. The bundle reads Doctrine ClassMetadata so it never needs to know your entities. The Laravel analogue is $fillable/$hidden plus a per-resource allow-list. Do not rely on $guarded = [] here — the whole point of the registry is that write fields are declared explicitly, mirroring read/write separation in the config.
Form binding as the write pipeline. Symfony Forms gave you type coercion, validation and the entity hydration in one step. Laravel's FormRequest is the direct analogue, but you have to remember that FormRequest validates — it does not hydrate into a model with type casting. Pair it with a DTO or explicit fillable filtering; do not Model::create($request->all()).
Serialization groups per operation. The bundle leans on JMSSerializer-era conventions; in Laravel that is JsonResource with different classes or whenLoaded/when conditionals for index vs show. Keep index payloads thin — the generic approach tempts you to return the whole entity graph and it will bite on any hasMany.
Query-parameter filtering with an explicit whitelist. Whatever query-parsing the bundle does, replicate it only over the read keys. In Laravel:
$query = $model->newQuery();
foreach (array_intersect_key($request->only($allowedFilters), array_flip($allowedFilters)) as $field => $value) {
$query->where($field, $value); // field names come from config, never from the request
}
Pagination always on, with a hard cap. Generic list endpoints are the classic place where ?limit=100000 takes the DB down.
Version and mount. Everything under /api/v1, behind auth:sanctum (or your guard), with write routes behind a policy/Gate. The bundle deliberately leaves auth to the Symfony firewall; that decision is correct and you should make it explicitly rather than inheriting "the bundle doesn't do it."
Extension points in the original: override the generic controller service definition, decorate the serializer, add a custom form type, or hook the config tree. In practice, most shops forked the controller — a strong signal that the config surface was too narrow for real use.
composer update on anything will attempt to move Symfony past the ~2.3 constraint and fail. Pin the lockfile, and do not expect composer audit to be meaningful.ClassMetadata.?sort=name,(SELECT ...) is your problem. Whitelist fields and directions.GET /api/user returning 404 vs a list is an information-disclosure signal. Restrict the registry to resources the caller may know about, and return 404 uniformly.cache:clear. Expect a "why is my new entity not there" fifteen minutes.scope closure to the registry entry.If you're migrating: the checklist is (1) enumerate exposed entities and fields from the config tree, (2) snapshot every route shape and response body with real requests, (3) rebuild as a resource registry + generic controller + FormRequest + JsonResource in Laravel, (4) diff responses on the snapshot, (5) add auth, throttling, and a pagination cap that the original never had. The bundle is small enough to read end-to-end in an afternoon — do that, then leave it in 2014.
How can I help you explore Laravel packages today?