samj/fractal-bundle
Symfony bundle integrating League\Fractal for transforming API data with transformers, serializers, and includes. Fractal's manager, serializers, and transformers are wired through Symfony's DI container, so you can produce consistent, well-structured JSON responses for your REST API.
First, the reality check. samj/fractal-bundle is a Symfony 2 bundle, not a Laravel package. It cannot be installed, auto-discovered, or booted in a Laravel app — composer require will fail or the bundle will simply never register (there is no AppKernel, no Bundle class loader). It is archived, last released 2020-06-19, and declares Symfony 2.x constraints that conflict with any modern symfony/* or laravel/framework install.
So there are two "getting started" paths, and you should pick deliberately:
Path A — you are actually on a legacy Symfony 2 project. Then:
composer require samj/fractal-bundle (pin to the 2020 tag; expect conflicts with PHP 8).app/AppKernel.php: new SamJ\FractalBundle\SamJFractalBundle().DependencyInjection/ — the extension + config tree (serializer choice, default includes).AppBundle\Transformer\UserTransformer as a service, tag it, inject a repository into its constructor, and call the manager from a controller.// services.yml
app.transformer.user:
class: AppBundle\Transformer\UserTransformer
arguments: ['@app.repository.user']
tags: [{ name: samj_fractal.transformer }]
Path B — you are a Laravel developer evaluating this. Don't use the bundle. Use league/fractal directly (or spatie/laravel-fractal), and steal the one genuinely useful idea: transformers as container-resolved services with constructor injection instead of transformers that app() their dependencies internally.
First use case worth building: a UserTransformer that injects an AvatarUrlGenerator (or a repository) rather than calling a global helper. That single dependency forces you to confront the container wiring, which is the entire value proposition of this package.
1. Transformers as services, resolution by tag. The bundle's contract is "register your transformer, let the container build it." This means the transformer never news up its collaborators:
class UserTransformer extends TransformerAbstract
{
protected $availableIncludes = ['posts'];
public function __construct(private AvatarUrlGenerator $avatars) {}
public function transform(User $user): array
{
return [
'id' => (int) $user->id,
'name' => $user->name,
'avatar'=> $this->avatars->for($user),
];
}
public function includePosts(User $user): Item
{
return $this->collection($user->posts, new PostTransformer());
}
}
The problem this instantly exposes: nested transformers (new PostTransformer()) bypass the container. In the bundle you resolve those through the manager too, or inject them. Either way, decide on one convention and enforce it, because mixing container-resolved and new-ed transformers is where teams drift.
2. Container-aware resolver in Laravel. If you are porting the pattern, the equivalent is a small binding plus a resolver that prefers the container:
// AppServiceProvider::register()
$this->app->bind(UserTransformer::class, fn ($app) => new UserTransformer(
$app->make(AvatarUrlGenerator::class)
));
// A tiny helper the bundle's compiler pass replaces
function transformer(string $class): TransformerAbstract
{
return app($class);
}
3. Serializer configuration lives in config, not code. The bundle exposes serializer selection (array, JSON-API, data-array). Set it once globally, override per-response only when you genuinely need a different shape. In Laravel the analogue is config/fractal.php or an explicit ->setSerializer(new JsonApiSerializer()) at the manager call site.
4. Controller integration. The typical flow is: fetch models → hand to manager → createData/createDataWithPagination → return. Keep the manager call thin; version negotiation (Accept header, media type) is not provided by the bundle, so if you need API versioning you wire it yourself in a kernel listener or middleware.
5. Includes and eager loading are your responsibility. Fractal's $request->query('include') parsing (parseIncludes) does not eager-load. Pair it with an explicit include→relation map:
$manager->parseIncludes($request->query('include', ''));
$users = User::with($this->includeMap($manager->getRequestedIncludes()))->paginate();
6. Feature-test the transformer, not the bundle. Instantiate the transformer with mocks for its injected collaborators and assert the array shape. The DI design makes this possible — that is the payoff for the wiring overhead.
new and loses its injected dependencies — you get an "undefined property" or a null collaborator deep inside transform(), not at wiring time. Verify tags with debug:container --tag=<tag>.$this->get('...')) defeats the purpose and makes the transformer untestable. If you find a container reference inside a transformer, that's the bug.user → posts → author → posts will blow up. Fractal has $maxDepth/recursion guards, but they are per-transformer and easy to forget. Set them deliberately on any bidirectional relationship.data/meta envelopes; JSON-API adds type and id requirements that your transformer output must satisfy. Pick one at project start — retrofitting a serializer after 30 endpoints exist is a rewrite.includeX return types are strict. Returning a plain array where an Item/Collection is expected fails deep in the manager with a confusing error. Return $this->item(...) / $this->collection(...) always.spatie/laravel-fractal's convention-only resolution when transformers have real dependencies.league/fractal directly (or to Symfony's built-in serializer) is a one-file change rather than a hunt-and-replace.How can I help you explore Laravel packages today?