riddle/rest-api-bundle
A Symfony bundle for building REST APIs, giving you a structured foundation for controllers, request handling, and JSON responses. It helps you expose resources quickly, keep endpoints consistent, and cut boilerplate so your API stays clean and maintainable across your Symfony application.
Run the framework check before anything else. Open the package's composer.json. It declares "type": "symfony-bundle" and constrains against Symfony framework components and Symfony\Component\HttpKernel\Bundle\Bundle. There is no Illuminate\Support\ServiceProvider, no facade, no config/ published by vendor:publish, and nothing that Laravel's package auto-discovery (bootstrap/cache/packages.php) will ever register. composer require riddle/rest-api-bundle into a Laravel app either resolves a pile of Symfony\Component\* transitives you will never wire into the container, or fails resolution on PHP/version constraints. Treat that as a hard stop until proven otherwise.
That means "getting started" branches into two paths.
Path A — you're actually maintaining a legacy Symfony 2/3 app (the only context this bundle runs in):
composer require riddle/rest-api-bundle:dev-master, then pin to a specific commit hash once it resolves.// app/AppKernel.php
$bundles = [
// ...
new Riddle\RestApiBundle\RiddleRestApiBundle(),
];
Resources/config/services.yml (this is the real spec: which listeners and helpers get wired and under what IDs) → DependencyInjection/Configuration.php (what is actually configurable) → tests, if any shipped.GET /api/items returning a consistent JSON envelope (data, pagination meta, structured errors) rather than Symfony's default HTML exception page. If that isn't working in an afternoon, close the evaluation.Path B — you're a Laravel developer, which is the realistic case here: don't install it. Read it as a reference implementation for response envelopes, exception-to-JSON listeners, and request parsing, then reimplement the handful of ideas you like using JsonResource, FormRequest, and App\Exceptions\Handler. That is a two-hour exercise and leaves your dependency tree untouched. Start with the bundle's base controller/trait (if present) and its kernel.exception listener — those two files contain essentially all the reusable ideas.
The bundle's only real value is as a catalogue of thin REST plumbing. Map each piece to its Laravel equivalent, which is what you will actually maintain day-to-day.
Response envelope → ApiResponse helper or resource layer. The pattern worth copying is a single, documented shape that every endpoint — success and failure — obeys:
{ "data": { }, "meta": { "page": 1, "total": 42 }, "errors": [] }
In Laravel, emit it from JsonResource subclasses or a small trait on your base controller:
trait RespondsWithEnvelope
{
protected function ok($data, array $meta = [], int $status = 200): JsonResponse
{
return response()->json(['data' => $data, 'meta' => $meta, 'errors' => []], $status);
}
protected function fail(array $errors, int $status = 422): JsonResponse
{
return response()->json(['data' => null, 'meta' => [], 'errors' => $errors], $status);
}
}
Exception-to-JSON listener → App\Exceptions\Handler. The bundle hooks kernel.exception to convert exceptions into JSON with correct status codes. In Laravel this is already render() on your exception handler; the lesson is to be explicit per exception type rather than letting everything fall through to a 500:
public function render($request, Throwable $e)
{
if ($request->is('api/*')) {
return match (true) {
$e instanceof ModelNotFoundException => response()->json(['errors' => ['Not found']], 404),
$e instanceof ValidationException => response()->json(['errors' => $e->errors()], 422),
default => response()->json(['errors' => ['Server error']], 500),
};
}
return parent::render($request, $e);
}
Serialization of entities/DTOs → API Resources. Where the bundle normalises entities to JSON, use JsonResource for single records and ResourceCollection for lists. Keep the transformation out of controllers so the contract lives in one class per resource.
Pagination and filtering → first-party paginators and scopes. Laravel's paginate()/cursorPaginate() plus spatie/laravel-query-builder (or hand-rolled query scopes) cover everything the bundle's "simple pagination and filtering helpers" advertise, with better documentation and ongoing maintenance.
Routing conventions → routes/api.php. The bundle's routing assumptions are Symfony router config. In Laravel, Route::apiResource() gives you the same REST conventions with api middleware, model binding, and automatic JSON 404s already applied.
Request body parsing → FormRequest. Any parameter parsing or validation the bundle performs around request bodies maps directly to a FormRequest class with rules() and authorize(), keeping controllers to a couple of lines.
If you are forced to bridge into an existing Symfony consumer, define an internal interface (ApiResponder with ok()/fail()) and implement it once against the bundle and once against Laravel's JsonResponse. That keeps the integration swappable and makes the eventual extraction a one-class change rather than an audit.
vendor/. Never ship that. Run the dependency spike in a throwaway branch and treat any need for --ignore-platform-reqs as a hard no.services.yml is the actual API surface. There is effectively no user documentation. The service definitions (listener IDs, priorities, tagged controllers) tell you what the bundle does; Configuration.php tells you what is configurable. Read those before believing the README.assessment-pm.md bottom line — do not adopt as a production dependency — is the correct default; this is a pattern source, not a dependency.kernel.exception listener priority and whether it re-throws; that is the usual cause of a generic 500 replacing a useful error.config/api.php. Do not look for Laravel-style configuration files, and do not expect vendor:publish to expose anything.composer require, and move on.How can I help you explore Laravel packages today?