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

Rest Presenter Laravel Package

xtend-packages/rest-presenter

Lightweight Laravel presenter for REST APIs that transforms Eloquent models and collections into clean, consistent JSON responses using reusable presenter classes, with support for relationships, field filtering and pagination—keeping controllers thin and API output predictable.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require xtend-packages/rest-presenter

The service provider is auto-discovered on Laravel 9+. There is nothing to register manually unless you have disabled package discovery.

Orient yourself first

Before writing code, spend two minutes in vendor/xtend-packages/rest-presenter/:

  • src/ — the base Presenter class and any attributes/contracts you extend. Read this before the README; the API surface is small and the README examples drift between minor versions.
  • config/ — if a config file ships, note the publish tag from the service provider's publishes() call (commonly rest-presenter-config).
  • src/Commands/ — this package depends on laravel/prompts, which means it ships interactive artisan commands (a generator for presenters/scaffolding). Run php artisan list | grep -i presenter to see exactly what is available in your installed version rather than trusting docs.

Publish config only if you need to change defaults:

php artisan vendor:publish --provider="Xtend\RestPresenter\RestPresenterServiceProvider"

First use case

Take an existing controller that returns a raw Eloquent model and move it behind a presenter. Generate the class with the package command (interactive, prompts for name and model), then:

// app/Presenters/PostPresenter.php
class PostPresenter extends Presenter
{
    public function present(): array
    {
        return [
            'id'    => $this->model->id,
            'title' => $this->model->title,
        ];
    }
}
// app/Http/Controllers/Api/PostController.php
public function show(Post $post)
{
    return PostPresenter::make($post);
}

That single endpoint is the right scope for a first commit. Once it round-trips through your test suite and a real HTTP client, expand to collections and relationships.

Implementation Patterns

One presenter per output shape, not per model. The value of a presenter layer is decoupling the wire format from the schema. If the same model is returned differently on /api/v1/posts and /api/admin/posts, that is two presenters, not one presenter with flags.

Collection handling. Presenters typically resolve collections through a static entry point; confirm the signature in your version (it is usually ::collection($models) or accepting an iterable in make()). Wrap it in a controller or a small response helper so you never call the presenter class from a Blade view or an event listener.

Relationships. Present only what you have eager loaded. A presenter that reaches into $this->model->author->name will silently fire a query per row on collections. The consistent pattern is:

$posts = Post::with('author:id,name')->paginate();
return PostPresenter::collection($posts);

and in the presenter, present the relation itself with its own presenter rather than flattening fields inline. Nested presenters keep the shape reusable and make the N+1 visible at the call site.

Conditional fields. Prefer building the array conditionally in PHP over a flag argument on the presenter:

$data = ['id' => $this->model->id];

if ($this->includePrivate) {
    $data['internal_notes'] = $this->model->internal_notes;
}

Flags passed through make() tend to multiply; two small presenters age better than one with five booleans.

Integration with pagination and resources. If you are already on JsonResource, do not rewrite working endpoints for consistency's sake. Migrate opportunistically: new endpoints use presenters, existing ones stay until they are touched. Mixing the two is fine as long as each endpoint is internally consistent.

Testing. Presenters are pure functions of a model, so test them without HTTP:

$post = Post::factory()->make(['id' => 1, 'title' => 'Hello']);

$this->assertSame(
    ['id' => 1, 'title' => 'Hello'],
    (new PostPresenter($post))->present()
);

Assert on the full array rather than individual keys — the whole point is that the shape is a contract, and a assertArrayHasKey test will not catch a removed field that breaks a client.

Generator workflow in a team. The artisan commands are prompt-driven, which is great interactively and awkward in pair programming over a shared terminal. Agree on a naming convention (suffix, directory) and let people pass the name as an argument where the command supports it, so generated files are predictable in review.

Gotchas and Tips

This release (1.1.3) is a dependency widening, not a feature. The only change is support for newer versions of laravel/prompts. Upgrade is drop-in from 1.1.2 — no code changes, no deprecations. Run composer update xtend-packages/rest-presenter and confirm the lockfile resolves to a laravel/prompts version your other packages also accept. If another dependency pins prompts to an older major, you may see a conflict that did not exist at 1.1.2; in that case composer why-not laravel/prompts identifies the blocker faster than reading the solver output.

Verify the prompts version before trusting the generator. Since the constraint is now permissive, two developers on the same composer.lock are fine, but two machines that ran composer update independently can have different prompts majors. If generated files start differing in styling or prompt behaviour between branches, check composer show laravel/prompts on both.

CI and non-interactive runs. Prompt-based commands will hang or fail without a TTY. If you invoke package commands from a deploy script, pass --no-interaction and supply any required arguments explicitly, or skip the command entirely in production.

toArray() is not json_encode(). Presenters commonly expose both a raw array and a serialised form. Date formatting, enum casting, and null handling can differ between the two paths. Pick one and use it in tests so your assertions match what the client actually receives.

Debug the shape, not the endpoint. When a response looks wrong, dd(PostPresenter::make($post)->present()) isolates the presenter from middleware, response macros, and resource wrapping. Nine times out of ten the bug is a missing with() or a cast on the model, not the presenter.

Response wrapping is configured, not assumed. If your API has a data envelope, check whether the package wraps or whether your controller does. Double-wrapping ({"data":{"data":[...]}}) is the most common first-day surprise.

Extension points. The base presenter class is the intended seam — subclass it for shared behaviour (timestamps, IDs, soft-delete flags) rather than writing a trait that reaches into protected state. If you need to change how the presenter resolves models or serialises, check whether the service provider binds a contract in the container; overriding a binding is more upgrade-safe than overriding a method.

Pin for reproducibility. Because the prompts constraint is now wide, commit composer.lock and let Dependabot/renovate bump it in a PR where you can see the prompts version change alongside the package update.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource