friendsofsymfony/rest
FOSRestBundle provides tools for rapidly building RESTful APIs with Symfony, including format handling, view listeners, routing conventions, and request body decoding. It simplifies controller setup for JSON, XML, and other response formats while staying flexible and configurable.
First: confirm you are not about to install the wrong thing. friendsofsymfony/rest (the repo you linked) is the legacy, pre-1.0 home of the bundle — not the Composer package anyone actually used. The package you would have required in a real Symfony 2.x app is friendsofsymfony/rest-bundle. That bundle itself has since been superseded by Symfony's native #[Route], MapRequestPayload, and #[Serialize]. So there are three distinct things to keep straight before you write a single line:
| What you're looking at | Composer name | Status | Use for |
|---|---|---|---|
| This repo | friendsofsymfony/rest |
Archived, 2013 | Reading the original design vocabulary only |
| The bundle | friendsofsymfony/rest-bundle |
Deprecated (in favor of Symfony native) | Maintaining a legacy Symfony 2.x service |
| Framework native | Symfony #[Route]/MapRequestPayload, Laravel resources |
Current | Anything you're building today |
For a Laravel codebase there is no install step. There is no service provider, no middleware, no container binding, no PSR-4 namespace that plays with Laravel's router or Illuminate\Http\Request. composer require friendsofsymfony/rest in a Laravel project gets you dead code and, if you're unlucky, an autoload conflict. Do not do it.
Where to look first (as reference material): the 1.x docs under Resources/doc/, specifically the listener chapters:
5-the-view-listener.md — turning controller return values into responses6-the-format-listener.md — Accept header → _format negotiation7-the-body-listener.md — decoding JSON bodies into the request so forms work8-the-param-fetcher-listener.md — mapping route params onto request attributesThose four listeners are the entire mental model of the package. Read them as a spec for what you should be doing natively in Laravel, because every concept has a first-class Laravel counterpart.
If you are maintaining a legacy Symfony 2.x app that already has it, the era-appropriate minimal setup looks like this:
composer require friendsofsymfony/rest-bundle:~1.0 jms/serializer-bundle
// app/AppKernel.php
new FOS\RestBundle\FOSRestBundle(),
new JMS\SerializerBundle\JMSSerializerBundle(),
# app/config/config.yml
fos_rest:
view:
view_response_listener: true
formats:
json: true
format_listener: true
body_listener: true
# app/config/routing.yml
api:
type: rest
resource: Acme\ApiBundle\Controller\ArticleController
First use case (and its Laravel translation). The canonical demo is "return a domain object from a controller and get JSON back with no json_encode, no Content-Type wiring":
// Symfony 2.x + FOSRestBundle (legacy)
/**
* @View(statusCode=200, serializerGroups={"list"})
*/
public function getArticlesAction()
{
return ['articles' => $this->repo->findAll()];
}
In Laravel today that same outcome is:
// routes/api.php
Route::get('/articles', [ArticleController::class, 'index']);
// app/Http/Controllers/Api/ArticleController.php
public function index(): ArticleCollection
{
return new ArticleCollection(Article::all());
}
No library needed — ArticleCollection handles serialization, the framework sets Content-Type: application/json, and content negotiation is a middleware decision you own. That is the entire migration story in one before/after.
1. Content negotiation — middleware, not a listener.
In FOSRest, the format listener rewrites _format on the request, and everything downstream keys off it. If you want the same behaviour in Laravel, write a small middleware that reads Accept and normalizes a format onto the request:
class NegotiateFormat
{
public function handle(Request $request, Closure $next): Response
{
$supported = ['json', 'xml']; // keep it short
$accept = $request->header('Accept', 'application/json');
$format = 'json';
foreach ($supported as $candidate) {
if (str_contains($accept, "application/$candidate")) {
$format = $candidate;
break;
}
}
$request->attributes->set('_format', $format);
return $next($request);
}
}
Register it on your api group and have a thin response factory pick the representation. The FOSRest takeaway is rule shape: it exposed fos_rest.format_listener.rules with fallback_format, prefer_extension, and priority — replicate exactly those three knobs and you have parity.
2. Read paths — prefer resources over return-value magic.
The @View annotation let controllers return plain arrays. In Laravel the same idea is JsonResource / ResourceCollection. The FOSRest annotation options map cleanly:
FOSRest @View option |
Laravel equivalent |
|---|---|
serializerGroups={"list"} |
Dedicated ArticleListResource class |
statusCode=201 |
Return the resource with ->response()->setStatusCode(201) |
templateVar="articles" |
Wrapping in ['data' => ...] via ResourceCollection |
@View(template=...) |
Blade/JSON view; in Laravel prefer a resource |
Never return a raw Traversable or an Eloquent Collection unkeyed — FOSRest had to warn about this because JMSSerializer would emit {"0":{...},"1":{...}}. Eloquent's toJson() does the right thing, but if you hand a Cursor or generator to json_encode you resurrect the same bug. Always go through a resource.
3. Write paths — body decoding + validation as one step.
The body listener's job was to decode application/json into $request->request so $form->bind($request) worked untouched. Laravel gives you this for free via $request->json(), but you should still bundle decoding with validation using a FormRequest:
class StoreArticleRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
];
}
}
public function store(StoreArticleRequest $request): ArticleResource
{
$article = Article::create($request->validated());
return (new ArticleResource($article))
->response()
->setStatusCode(201);
}
FormRequest is the direct spiritual successor to "body listener decodes → form validates → controller persists." The validation failure path also replaces the FOSRest pattern of returning the invalid form and letting the view listener turn it into a 400 with errors.
4. Error envelope — centralize in the exception handler.
FOSRest normalized exceptions to HTTP status codes through config (fos_rest.exception.codes mapped class names to codes). In Laravel you do that in bootstrap/app.php or a custom Handler:
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (ApiException $e, Request $request) {
if (! $request->is('api/*')) {
return null; // let the default handler run
}
return response()->json([
'error' => [
'code' => $e->errorCode(),
'message' => $e->getMessage(),
'details' => $e->details(),
],
], $e->statusCode());
});
})
The contract that matters here — and the one worth writing down for the team — is the mapping: validation → 422, not found → 404, auth → 401/403, throttle → 429, unhandled → 500 with a correlation id. That checklist is the durable artifact; the FOSRest code is not.
5. Versioning — pick one and stay boring.
FOSRest implied choices between URI versioning and content-type versioning (Accept: application/vnd.yourapi.v2+json). In Laravel, URI versioning via route prefix is almost always the right call because it's visible in logs, cache keys, and gateway rules:
Route::prefix('v1')->group(base_path('routes/api/v1.php'));
Route::prefix('v2')->group(base_path('routes/api/v2.php'));
If you must do header versioning, do it in the same negotiation middleware from pattern 1 and treat the version as another attribute. Do not split the two across layers.
6. Workflow when auditing a legacy Symfony service that has this bundle.
type: rest — those are the bundled endpoints.@View, @Rest\Get, @QueryParam — that's the surface being shaped by the bundle.fos_rest.* in config — that's your behavioural contract (fallback format, body listener on/off, exception codes).GET /articles returns 200, Accept: application/xml returns 406 if you drop XML support) and then reimplement behind a gateway. Endpoint-group by endpoint-group, not listener by listener.Package identity confusion will burn you. friendsofsymfony/rest ≠ friendsofsymfony/rest-bundle. Searching "FOSRest" documentation online returns the bundle docs, which reference APIs this repo doesn't have (or has under different namespaces). If someone tells you "we use FOSRest 1.0", ask which package name is in composer.json. If they can't answer, treat it as the archived repo until proven otherwise.
Zero dependents, 51 stars, archived since 2013. That means: no PHP 7/8 support, no PSR-7, no security releases, no issues answered, no StackOverflow answers for the specific version you're on. Any bug you hit is now your bug. If you are not on a frozen Symfony 2.x service, this is a hard stop, not a "we'll deal with it" — it's not a dependency, it's a liability.
$form->bind($request) is the trap in every old tutorial. It works only because the body listener has already populated $request->request. The moment the body listener is disabled, misconfigured for the content type (application/vnd.api+json won't match application/json by default without a rule), or you're behind a proxy that strips the header, you get an empty form and a "validation failed" error with no explanation. Debug tip: log $request->request->all() before the form binds — if it's empty, the body listener didn't fire, and the fix is in fos_rest.body_listener config, not your controller.
Format listener ordering is load-bearing. In Symfony it had to run early enough that _format existed before routing/param fetchers, but late enough that the request was populated. If you replicate it as Laravel middleware, register it in the api group before any middleware that reads _format, and remember Laravel runs middleware in declaration order within a group. Silent 406s are almost always ordering.
@View annotations require sensio/framework-extra-bundle. If you inherit a Symfony app that returns plain arrays from controllers and gets a "cannot convert array to Response" error, the annotation driver isn't loaded. That's a AppKernel registration, not a fos_rest config toggle — and it's a common breakage on upgrades.
statusCode in @View beats whatever you set on a Response. If a controller sets a header on a Response and also has @View(statusCode=201), the annotation can win. Don't mix the two styles within one action; pick annotation-driven or Response-driven.
XML negotiation is a footgun, not a feature. FOSRest's format listener happily negotiated application/xml. JMSSerializer then emitted XML that clients struggled to parse (attributes vs elements, namespaces, encoding declarations). If you port this pattern to a Laravel middleware, support JSON only until a client explicitly contracts for XML — and if that ever happens, use a dedicated XML resource layer, not a generic serializer.
Exception codes config drifts from reality. fos_rest.exception.codes maps exception FQCNs to HTTP codes, but if a parent exception is mapped and a child isn't, the child inherits the wrong code. The same hazard exists in Laravel's withExceptions. Write a table-driven test that asserts every domain exception your app throws resolves to an expected status — it catches both inherited-code bugs and newly added exceptions nobody mapped.
When you're debugging a legacy endpoint, check the _format route default first. Symfony's type: rest routing injected _format into route defaults. If a route was manually defined and the format listener was on, you got two sources of truth, and the manual default silently shadowed the Accept header. Look at bin/console debug:router for the endpoint before you touch any code.
Extension point that actually matters: a response factory. FOSRest's real value was a single choke point where the format, status, and body came together. Whatever you build natively, keep one class (an ApiResponse builder or a responds() helper) that every controller goes through. The moment serialization is scattered across controllers, your error envelope and Content-Type handling will drift per endpoint — which is precisely the boilerplate this bundle was invented to eliminate.
Bottom line for a Laravel developer: don't install it, don't fork it, don't vendor it. Use its four-listener vocabulary as a design checklist — negotiation, view shaping, body decoding, exception mapping — and satisfy each item with a middleware, a FormRequest, an ApiResource, and a handler. That's a one-page standard, and it costs you a day rather than the life of the service.
How can I help you explore Laravel packages today?