gathercontent/laravel-fractal
A Laravel wrapper for the Fractal library that helps you transform Eloquent models, collections, and paginators into clean API responses with support for includes, serializers, and flexible resource transformations.
composer require gathercontent/laravel-fractal
Laravel 5.5+ discovers GatherContent\LaravelFractal\FractalServiceProvider automatically. On older versions register the provider in config/app.php and alias 'Fractal' => GatherContent\LaravelFractal\FractalFacade::class.
Publish the config so you can see (and change) the one setting that determines every response shape:
php artisan vendor:publish --provider="GatherContent\LaravelFractal\FractalServiceProvider" --tag=config
config/fractal.php holds default_serializer (ships as ArraySerializer) and paginator (IlluminatePaginatorAdapter). Change default_serializer to DataArraySerializer or JsonApiSerializer before you write any endpoints — retrofitting it later means touching every controller and every test.
Your first use case: a transformer plus one endpoint.
namespace App\Transformers;
use App\Models\User;
use League\Fractal\TransformerAbstract;
class UserTransformer extends TransformerAbstract
{
public function transform(User $user): array
{
return [
'id' => (int) $user->id,
'name' => $user->name,
'email' => $user->email,
];
}
}
Route::get('/users/{user}', fn (User $user) => fractal($user, new UserTransformer)->respond());
Route::get('/users', fn () => fractal(User::paginate(25), new UserTransformer)->respond());
The helper inspects the data: an iterable becomes a Collection resource, a paginator gets wrapped in the paginator adapter, anything else becomes an Item. respond() returns a JsonResponse (200 by default, ->respond(201, ['X-Foo' => 'bar']) to override). ->toArray() is the escape hatch when the transformed data is only part of a bigger payload.
Where to look first: the README examples (they cover 90% of daily use), then config/fractal.php, then the upstream Fractal docs for transformer semantics. Transformers belong in app/Transformers — treat them as your API contract layer, separate from Eloquent resources.
Includes and eager loading. Declare availableIncludes on the transformer; Fractal silently ignores anything not declared.
class UserTransformer extends TransformerAbstract
{
protected array $availableIncludes = ['posts'];
protected array $defaultIncludes = [];
public function includePosts(User $user): Collection
{
return $this->collection($user->posts, new PostTransformer);
}
}
Drive it from the request and eager-load in parallel — otherwise you get N+1 per include:
$includes = array_filter(explode(',', $request->input('include', '')));
$users = User::with(array_intersect($includes, ['posts', 'posts.comments']))
->paginate(25);
return fractal($users, new UserTransformer)
->parseIncludes($includes)
->respond();
Keep the array_intersect whitelist explicit. It is the only thing standing between a public ?include= parameter and a full table scan.
A base API controller. Every real project ends up with one; write it on day one.
abstract class ApiController extends Controller
{
protected function respondWithItem($item, TransformerAbstract $transformer, int $status = 200)
{
return fractal($item, $transformer)->respond($status);
}
protected function respondWithCollection($collection, TransformerAbstract $transformer)
{
return fractal($collection, $transformer)->respond();
}
}
Chained/fluent form. Use it when you need to configure the manager before transforming:
return fractal()
->collection($books)
->transformWith(new BookTransformer)
->parseIncludes($request->input('include', ''))
->serializer(new JsonApiSerializer)
->withResourceName('books')
->respond();
Per-endpoint serializers. Don't let one global serializer dictate every endpoint. If your legacy endpoints need a bare array and your new v2 endpoints need JSON:API, set ->serializer(...) explicitly on the v2 controllers and let the config default serve the rest.
Nesting. Transformers should return Item/Collection resources from includeX() methods, never raw arrays. For sub-resources built in a controller, use fractal()->item($parent)->transformWith(...) rather than hand-assembling arrays.
Primitives and nulls. fractal()->primitive($value) and fractal()->nullItem() keep the response envelope consistent when the payload is a scalar or a missing record — useful for DELETE responses and 404 bodies under JsonApiSerializer.
ArraySerializer emits no data key; DataArraySerializer adds data; JsonApiSerializer emits data with type/id and moves pagination under meta/links. Tests written against one serializer will fail after switching, often with confusing nesting assertions rather than obvious key mismatches. Pin the serializer in a test helper.JsonApiSerializer needs a resource key. It derives type from $resourceKey on the transformer or ->withResourceName('users'). If both are missing you get a generic/empty type, and clients keyed on type break. Set protected $resourceKey = 'users'; on every transformer you use with JSON:API.availableIncludes is a hard whitelist. Forgetting to list a relation means parseIncludes('posts') is a no-op with no warning. This is the single most common "why is my include empty" bug.includePosts() throws or produces garbage depending on version. Use $this->item(...) / $this->collection(...).ArraySerializer you get meta.pagination; under JsonApiSerializer you get meta + links. Assert on structure in tests, not on whole-array equality.respond() is not toArray(). Inside a controller that composes other keys (['user' => ..., 'token' => ...]), call ->toArray() on the fractal result instead of returning respond(). Chaining respond() mid-array gives you a JsonResponse object embedded in an array.Fractal::serializer(...) in a service provider is fine; calling it in a controller and expecting it not to affect other code in the same request is not — the League\Fractal\Manager binding is a singleton.League\Fractal\Pagination\Cursor with League\Fractal\Resource\Collection::setCursor) or stream outside Fractal entirely; transforming 50k models is a memory incident waiting to happen.Fractal facade proxies to League\Fractal\Manager, so IDEs won't show parseIncludes, setSerializer, etc. Inject League\Fractal\Manager directly when you need real type hints, or add @method docblocks to a local facade subclass.$this->getJson('/users')->assertJsonStructure([...]) beats assertExactJson; Fractal output ordering isn't guaranteed across versions and the data wrapper flips with the serializer. Add one smoke test per serializer if you use more than one.League\Fractal\Manager as a singleton — you can $this->app->extend() it or bind a custom serializer globally in a service provider without touching the package, which is the cleanest way to apply cross-cutting includes (e.g. an include param default) across all endpoints.How can I help you explore Laravel packages today?