Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Fractal Bundle Laravel Package

somnambulist/fractal-bundle

A Laravel bundle that integrates League Fractal, adding service providers, configuration and helpers so you can transform API responses with Fractal serializers, includes and pagination using familiar framework conventions.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the framing: somnambulist/fractal-bundle is a Symfony bundle, not a Laravel package. It has no service provider, no config/fractal.php, no facade. If you are in a Laravel codebase, you will not install this package — you will install league/fractal directly (optionally spatie/laravel-fractal) and replicate the handful of things this bundle wires up. Where this package is useful to a Laravel developer is (a) you maintain a Symfony service alongside a Laravel app and need consistency in output shape, or (b) you want a reference implementation of how to container-manage Fractal managers, serializers, and paginators — which is exactly what the bundle's DependencyInjection/ directory shows.

If you're actually in Symfony:

composer require somnambulist/fractal-bundle
// config/bundles.php
return [
    // ...
    Somnambulist\FractalBundle\FractalBundle::class => ['all' => true],
];

Then add a config file. Do not guess the keys — read DependencyInjection/Configuration.php first; the bundle is a port of samj/fractal-bundle, so the original's README is usually accurate for the config tree (default serializer, default paginator, recursion limit).

Where to look first, in this order:

  1. Resources/config/services.* — the actual service IDs you'll inject. Fractal bundles typically expose a League\Fractal\Manager service and a transformation helper; confirm the ID before type-hinting.
  2. The compiler pass — most Fractal bundles collect tagged transformers into a registry so transformers can have constructor dependencies. This is the single most important thing to understand; without it, Fractal instantiates transformers with new and you lose DI.
  3. DependencyInjection/Configuration.php — serializer and paginator defaults.

First use case — the canonical one:

use League\Fractal\TransformerAbstract;

final class UserTransformer extends TransformerAbstract
{
    protected array $availableIncludes = ['posts'];

    public function transform(User $user): array
    {
        return [
            'id'    => $user->getId(),
            'name'  => $user->getName(),
            'email' => $user->getEmail(),
        ];
    }

    public function includePosts(User $user): Item
    {
        return $this->collection($user->getPosts(), new PostTransformer());
    }
}
// controller
$resource = new Collection($users, $userTransformer);
$resource->setMetaValue('total', count($users));

$manager->parseIncludes($request->query->get('include', ''));

return new JsonResponse($manager->createData($resource)->toArray());

The Laravel equivalent, which is what you'll actually write:

// app/Providers/FractalServiceProvider.php
public function register(): void
{
    $this->app->singleton(Manager::class, function () {
        $manager = new Manager();
        $manager->setSerializer(new ArraySerializer());
        return $manager;
    });
}

Implementation Patterns

One transformer per resource, always. The moment you add if ($request->user()->isAdmin()) inside a transformer you have lost. Split into UserTransformer and AdminUserTransformer, or use $this->availableIncludes / $this->availableIncludes guards. Fractal transformers are cheap; conditionals inside them are not.

DI-aware transformers via tags. The bundle's compiler pass is the pattern to copy. In Laravel:

// register tagged transformers
$this->app->singleton(TransformerRegistry::class);

foreach (glob(app_path('Transformers/*Transformer.php')) as $file) { /* or explicit list */ }

Simpler and more honest: just app(UserTransformer::class) at the call site rather than a registry, unless you genuinely need string-based resolution (e.g. transformers chosen by an API version parameter).

Serializer choice is a product decision, not a config detail. Set it once, globally.

Serializer Output shape Use when
ArraySerializer {"data": [...]} Standard REST APIs, matches Laravel response()->json() conventions
DataArraySerializer {"data": {"id": 1, ...}} Single-item responses need the same wrapper as collections
JsonApiSerializer {"data": {"type": "users", "id": "1", "attributes": {...}}} You're committed to JSON:API tooling

The gotcha nobody mentions: pagination metadata only appears with serializers that support it. Switch to JsonApiSerializer and your hand-rolled setMetaValue('total', ...) calls vanish into meta.pagination. Decide before you ship.

Includes are parsed from the request, not the transformer. $manager->parseIncludes('posts.comments') is the integration point. In Laravel, put this in a middleware or a small FractalRequest value object so every controller doesn't repeat it:

$includes = $request->string('include')->explode(',')->filter()->all();
$manager->parseIncludes($includes);

Whitelist them. Unvalidated include params let a client request arbitrary traversals of your object graph — a cheap DoS on nested relations.

Pagination adapters bridge the two ecosystems. Fractal's PaginatorInterface is not Laravel's LengthAwarePaginator and not Pagerfanta. You write a ~20-line adapter:

final class LaravelPaginatorAdapter implements PaginatorInterface
{
    public function __construct(private LengthAwarePaginator $paginator) {}

    public function getCurrentPage(): int { return $this->paginator->currentPage(); }
    public function getLastPage(): int { return $this->paginator->lastPage(); }
    public function getTotal(): int { return $this->paginator->total(); }
    public function getCount(): int { return $this->paginator->count(); }
    public function getPerPage(): int { return $this->paginator->perPage(); }
    // ...
}

In Symfony this comes from the bundle's paginator config; in Laravel you write it once and forget it.

Test the transformer, not the HTTP layer. $transformer->transform($user) returns an array — assert on that. Reserve full Manager round-trips for the two or three cases where includes or serialization actually matter. This turns slow feature tests into fast unit tests and is the single biggest day-to-day quality-of-life win with Fractal.

Reuse in jobs and commands. Anything that returns an array for an external consumer (webhook payload, queue message, CSV export) should go through the same transformer. Consistency beats a second hand-written mapping.

Gotchas and Tips

Manager is stateful. parseIncludes(), setRecursionLimit(), and serializer swaps mutate the instance. As a singleton — which is how both the bundle and the obvious Laravel provider register it — includes will leak between requests under Octane, RoadRunner, or Symfony's long-running runtimes. Reset it, or bind it as a scoped/transient service:

$this->app->scoped(Manager::class, ...); // Laravel: per-request lifecycle

This is the number-one production bug with every Fractal integration, not just this one.

N+1 queries are entirely your problem. Fractal calls $user->getPosts() and expects them loaded. Eager-load in the query, not the transformer. If you can't, at least add Model::preventLazyLoading() in non-production so tests catch it.

Transformers resolved by string/registry can't take constructor args. If the container is the one constructing them, fine; if Fractal is doing new $class, your UserTransformer(Formatter $f) dies. Verify which path the bundle uses before writing transformers with dependencies — check whether the compiler pass registers a factory/resolver or just a tag list.

Recursion limit. Deep includes (posts.comments.author.posts...) silently produce {"data": null} past setRecursionLimit(). Symptom: a relationship that "just disappears" with no error. Set it explicitly and log when includes get truncated.

Version drift is a real risk here. 3 stars, 0 dependents, and it's a port of a bundle that itself has moved on. Pin the exact version in composer.json, and read the bundle's composer.json constraints against your PHP/Symfony version before committing. Budget for vendoring the DI wiring into your own app if it goes stale — it's ~200 lines of container config, not a framework.

Debugging output: call ->toArray() on the resource before it hits JsonResponse. Fractal returns a ResourceInterface; if you json_encode() it directly and get {}, you forgot createData(). Also dd($manager->getSerializer()) — "wrong wrapper shape" is almost always a serializer you didn't set.

Extension points worth knowing: custom SerializerAbstract subclass for bespoke envelopes, a decorator around Manager for cross-cutting concerns (include whitelisting, timing), and the tagged-transformer registry if you need polymorphic transformation. Everything else in the bundle is wiring you can replace in an afternoon.

When to skip this package entirely: if you're in Laravel, skip it. league/fractal plus ~40 lines of service provider gives you the same result with no third-party bundle risk. The value of somnambulist/fractal-bundle is entirely in the Symfony container integration — outside that context it has nothing to offer.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource