m6web/fos-rest-extra-bundle
A Symfony bundle that extends FOSRestBundle with extra utilities for building REST APIs. It provides convenient configuration and helpers to reduce boilerplate around request handling and responses, integrating cleanly into existing FOSRestBundle-based applications.
Install and register
composer require m6web/fos-rest-extra-bundle:^4.0
v4.0.0 is the Symfony 6-compatible line. That is the headline of the release and effectively the whole of it — there are no new features, no new config keys, and no documented deprecations, so the day-to-day usage is identical to v1. What changed is the framework floor: expect PHP 8.1+ and symfony/* ^6.x constraints. Verify the actual matrix rather than trusting the one-line changelog:
composer show m6web/fos-rest-extra-bundle --all # requires / conflicts
Registration is unchanged, and there is still no Flex recipe, so do it by hand:
// config/bundles.php
M6Web\Bundle\FOSRestExtraBundle\M6WebFOSRestExtraBundle::class => ['all' => true],
The bundle sits on top of FOSRestBundle, so friendsofsymfony/rest-bundle (and typically jms/serializer-bundle or the Symfony serializer) must already be installed and working. If your API is not yet returning FOSRest View responses, stop and fix that first.
Where to look first
Documentation is still thin and the repo is the source of truth. Read these three files in the vendor directory in this order:
README.md — the feature list and any usage examples.DependencyInjection/Configuration.php — the entire configuration tree, including the real config key (usually m6web_fos_rest_extra) and every option.Resources/config/services.xml — the service ids and, crucially, the priority attributes on any tagged event listeners. Listener priority is where most integration bugs come from.Confirm what actually got wired after the v4 bump — this is the fastest way to catch a service that silently stopped being registered under the new Symfony version:
bin/console debug:container --tag=kernel.event_subscriber | grep -i m6web
bin/console debug:event-dispatcher kernel.request
First use case
The realistic entry point is unchanged: you have a working FOSRest controller returning View::create($entity, 200) and you want request-driven serialization context (groups/fields supplied by the client) plus the listener plumbing around it. Enable the bundle with defaults, hit one existing endpoint, and diff the response against what you had before. With v4 specifically, do this immediately after the Symfony 6 upgrade and before any other refactoring — you want the bundle's delta isolated in the diff.
Keep FOSRest as the skeleton. Don't rewrite controllers to "use" this bundle. Continue with AbstractFOSRestController and View objects — on Symfony 6 that means PHP 8 attributes, #[Rest\Get('/api/pages/{id}')], not the old docblock annotations, which FOSRest no longer reads. The bundle's listeners observe the request/response cycle, so a controller that produces a well-formed View gets the extra behaviour for free. A controller that hand-rolls new JsonResponse(json_encode(...)) bypasses the serializer context entirely and the bundle will appear to do nothing.
Request-driven serialization context. The core workflow is letting the client narrow the payload:
#[Rest\Get('/api/pages/{id}')]
public function show(Page $page): View
{
$view = $this->view($page, 200);
$view->setContext($view->getContext() + [
'groups' => ['page:read'],
]);
return $view;
}
Treat the bundle's contribution as additive and untrusted: your controller's own groups define the maximum surface, and the request may only select a subset. Whitelist the group names the client is allowed to name (a const ALLOWED_GROUPS in a base controller or a config array) rather than passing the query string straight into the context.
A shared base controller pays off. Because the logic is per-response, put it in one place:
abstract class ApiController extends AbstractFOSRestController
{
protected function apiView($data, int $status = 200, array $groups = []): View
{
$view = $this->view($data, $status);
$view->setContext($view->getContext() + [
'groups' => array_merge($this->defaultGroups(), $groups),
]);
return $view;
}
}
All your resources then inherit consistent behaviour, and if the bundle lags behind Symfony 7 the way it lagged behind 6, you have exactly one edit to make.
Integration with the format listener. Keep fos_rest.format_listener and view_response_listener enabled and let this bundle's subscribers run alongside them. Order matters: format negotiation should resolve _format before serialization context is computed, so if you see an Accept header being ignored, check debug:event-dispatcher kernel.request for who runs first and adjust priority in your own subscriber rather than patching vendor code. Re-check this explicitly after the v4 upgrade — a priority that worked on Symfony 4/5 is not guaranteed to be the effective order under 6.
Testing. Two levels, both cheap:
// functional — asserts the whole listener chain
$client = static::createClient();
$client->request('GET', '/api/pages/1?groups=page:read');
$client->assertResponseIsSuccessful();
$this->assertArrayNotHasKey('internalNotes', $client->getResponse()->toArray());
// unit — the listener in isolation, fast and stable across Symfony upgrades
$listener = new YourListener(/* ... */);
$listener->onKernelRequest($this->createRequestEvent('/api/pages?groups=a,b'));
$this->assertSame(['a', 'b'], $request->attributes->get('_groups'));
The unit test is what saves you when you upgrade Symfony and the functional test fails for unrelated reasons — exactly the situation v4 just created. Note that Symfony 6's KernelTestCase/WebTestCase API (assertResponseIsSuccessful, getResponse()->toArray()) differs from the 3.x/4.x idioms you'll find in older tutorials.
Compose with your serializer. If you use JMS, groups must exist in #[Groups] metadata or they are silently ignored. If you use the Symfony serializer, they must match #[Groups] attributes. A typo produces a full, unfiltered response with no error — add an assertion in a test that a nonexistent group still returns the safe default, so you notice when filtering breaks.
^4.0, commit composer.lock, and run composer why-not symfony/framework-bundle ^7.0 before any framework upgrade. If you're starting a new API today, still weigh a 20-line custom listener in your own codebase against the dependency — for a single-purpose bundle this small, inlining removes an upgrade liability for free, and v4 arriving years after Symfony 6 shipped is the evidence for why that matters.repositories entry, or copy the listener into src/EventListener/ and drop the dependency — the code volume is small enough that a copy is a legitimate strategy.body_listener, format_listener, or your own security subscriber runs before the bundle's subscriber and the request attributes it expects aren't there yet. Confirm with debug:event-dispatcher, then set an explicit priority on your overrides.?groups= or a custom header, your HTTP cache must know:$response->setVary(['Accept', 'X-Serialization-Groups']);
Without it, a cached narrow response gets served to a client asking for the full one, or worse, a full response is served where filtering was expected. If you use Symfony HttpCache or Varnish, verify the Vary header in a test.
View flow, so your group filtering may not apply to 4xx/5xx payloads. Symfony 6 makes this more visible because the error renderer changed; assert on an error response in a test and, if needed, normalise it via a kernel.exception subscriber that produces a View like everything else.Configuration.php says, not what the README implies. Dump the resolved tree with bin/console config:dump-reference m6web_fos_rest_extra (adjust the key using the bundle's DI alias) before assuming an option is on — and re-dump after the v4 upgrade, since a Configuration.php rewritten for Symfony 6's config component can change node defaults silently.services.yaml decorates: to wrap behaviour without forking; (2) add your own subscriber at a chosen priority; (3) fork. Avoid editing vendor/ — even with an active v4 release, your patch will be lost on the next composer update, and a compat-only maintainer is unlikely to carry it upstream.How can I help you explore Laravel packages today?