additionapps / flexible-presenter
Easily define just the right data for your InertiaJS views
Installs: 51 722
Dependents: 0
Suggesters: 0
Security: 0
Stars: 119
Watchers: 5
Forks: 8
Open Issues: 1
Requires
- php: ^8.1
- illuminate/pagination: ^10.0|^11.0
- illuminate/support: ^10.0|^11.0
Requires (Dev)
- fakerphp/faker: ^1.12
- laravel/legacy-factories: ^1.0
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^9.0|^10.0
README
Easily define just the right data for your Inertia views (or anywhere else you want to, uh, flexibly present).
This package allows you to define presenter classes that take logic involved in getting data ready for your view layer out of your controller. It also provides an expressive, fluent API that allows you to modify and reuse your presenters on the fly so that you're only ever providing relevant data.
This package was built specifically for use with Inertia (:heart_eyes:) because we didn't like the fact we were sending more data than we needed to our views. That said, you're free to use it however you please - it's not dependant on Inertia in any way.
Installation
You can install the package via composer:
composer require additionapps/flexible-presenter
Usage
The package includes an artisan command to create a new presenter:
php artisan make:presenter PostsPresenter
This presenter will have the App\Presenters
namespace and will be saved in app/Presenters
.
You can also specify a custom namespace, say, App\Blog
php artisan make:presenter "Blog/Presenters/PostsPresenter"
This presenter will have the App\Blog\Presenters
namespace and will be saved in app/Blog/Presenters
.
Defining values
A presenter is a class in which you can define all the possible fields that you might want to expose to your Inertia views. When you call the class from a controller method you can use methods such as only
or except
to define which of these fields you want to expose in that given context. More on the API in a bit.
The only required method in a presenter class is values()
which should return an array with all the possible fields you might want to display in a view. These could simply be values directly on your model. Note that you can access model properties directly from the $this
variable, just as you can when using Laravel API Resources.
For now, here's a simple presenter class:
class PostPresenter extends FlexiblePresenter { public function values() { return [ 'id' => $this->id, 'title' => $this->title, 'body' => $this->body, ]; } }
Once a presenter is defined, you are free to return it as part of an Inertia response or in any other context that you need the presented data:
class PostController extends Controller { public function show(Post $post) { return Inertia::render('Posts/Show', [ 'post' => PostPresenter::make($post)->get(), ]); } }
Lazy evaluation
You can also define fields that are computed in more complex ways - for example you may want to add a presenter value that is derived from some other data like a relationship. In these situations it can be handy to wrap your value definition in a closure. Doing so will ensure that the value is only computed if it's actually asked for:
public function values() { return [ 'id' => $this->id, 'comment_count' => function () { return $this->comments->count(); }, ]; }
If you are using PHP >= 7.4 you can make things a little more readable by using the new short closure syntax:
[ 'comment_count' => fn() => $this->comments->count(), ];
Now if we call PostPresenter::make($post)->only(['id'])
the comment_count
value will not be evaluated meaning we do not need to worry about ensuring that relationship is loaded on the model.
Nested presenters
If necessary you can define a value that returns another presenter. This is handy if you want to only return certain values for a relation of the model you're working with:
public function values() { return [ 'id' => $this->id, 'comments' => function () { return CommentPresenter::collection($this->comments) ->except('body', 'updated_at'); }, ]; }
Note that in the example above we're using lazy evaluation so that we don't have to worry about the comments
relationship being loaded on the Post
model. If you know this relationship will always be loaded you could dispense with the closure.
Instantiating presenters
Once you have defined your presenter class you can use it in your controller (or elsewhere) in your application. There are three methods for creating a new presenter instance:
PostPresenter::make($post)
The make
method accepts a single resource as a parameter. In the majority of cases this will be an Eloquent model but there is no requirement that you pass a model specifically. For instance you could pass a some other object or even an associative array and then within the values()
method in your presenter use it like so:
public function values() { return [ 'id' => $this->resource['id'], 'title' => $this->resource['title'], ]; }
PostPresenter::collection($posts)
The collection
method accepts an Eloquent collection (or a plain array) of resources as a parameter. Each member of the collection will be transformed by the presenter as specified. Again the members of that collection can be Eloquent models, other objects or arrays:
$posts = PostPresenter::collection(Post::all()) ->only('id', 'title') ->get();
Using Pagination
As well as passing an Eloquent collection or array, you can also pass a Laravel paginator instance. You are free to pass an instance of either Illuminate\Pagination\Paginator
or Illuminate\Pagination\LengthAwarePaginator
. You can also pass a custom paginator as long as it extends the Illuminate\Pagination\AbstractPaginator
class and implements the Illuminate\Contracts\Support\Arrayable
interface.
Here's an example of a presenter used with an eloquent collection using simple pagination:
$posts = PostPresenter::collection(Post::simplePaginate()) ->only('id', 'title') ->get();
Which will output:
[ 'current_page' => 1, 'data' => [ [ 'id' => 1, 'title' => 'foo', ], [ 'id' => 2, 'title' => 'bar', ], ], 'first_page_url' => 'http://example.com/list?page=1', 'from' => 1, 'next_page_url' => null, 'path' => 'http://example.com/list', 'per_page' => 2, 'prev_page_url' => null, 'to' => 2, ];
PostPresenter::new()
The new
method accepts no parameters. This method is useful for when you have no resource or collection to pass into your presenter (perhaps because the presenter itself is responsible for gathering the resources it needs).
If one of your presenters has a key should return a presented relation as a value, you can use the convenience method whenLoaded
to conditionally include the relation:
// In PostPresenter return [ 'comments' => CommentPresenter::collection($this->whenLoaded('comments')), ];
In the above example the comments
key will be a collection of presented comments if the relation is loaded and null
if they are not.
Configuring presenters
With a new presenter instance you are now free to configure it in whatever way makes sense for your the current context. All the following methods can be chained onto make()
, collection()
or new()
.
only()
The only
method accepts keys that you want to return from your presenter instance. You can pass these keys as an array (->only(['id', 'title'])
) or as individual string arguments (->only('id', 'title')
).
except()
The except
method accepts keys that you want to exclude from your presenter instance. You can pass these keys as an array (->except(['id', 'title'])
) or as individual string arguments (->except('id', 'title')
).
with()
The with
method accepts a closure that takes as it's only argument the resource that is being transformed. You can use the with
method to overwrite the values of existing keys or to add entirely new keys to your presenter on-the-fly. It should return an array with the keys you want to overwrite/add along with their values.
PostPresenter::make($post)->with(function($post){ return [ 'title' => strtoupper($post->title), 'new_key' => 'Some value', ]; });
preset()
You may find that there are combinations of presenter values that you use repeatedly throughout your application. If so, rather than explicitly asking for those particular fields each time you can add a 'preset' to your presenter class and ask for that preset instead:
PostPresenter::make($post)->preset('summary');
Within your presenter class you should create a method with the name of your preset, prefixed with 'preset'. Given the example above we would create a method like this:
public function presetSummary() { return $this->only('title', 'published_at'); }
Your preset method should use the same presenter API methods above to build up a set of values. Note that all of these API methods can be chained, so, for example, you can modify a preset on the fly if you wish:
PostPresenter::make($post)->preset('summary')->with(function () { return [ 'comment_count' => $post->comments->count(), ]; });
Caveat: repeated method calls
Just bear in mind that if you use an API method in your preset method (for example only
) and then chain another only
method onto it when using your presenter, that the last call to only
will be the one to take effect:
// In PostPresenter... public function presetSummary() { return $this->only('title', 'body'); } // In Controller... PostPresenter::make($post)->preset('summary'); // Will return ['title' => 'foo', 'body' => 'bar'] PostPresenter::make($post)->preset('summary')->only('id'); // Will return ['id' => 1]
appends()
If you are presenting a paginated collection of resources you may want to add some additional key/value pairs to the outer array that wraps your data. To do this, you can use the appends()
method to specify the keys and values you wish to set. Let's say you're presenting a custom paginator instance that produces this output:
[ 'current_page' => 1, 'data' => [ // your presented resources ], 'first_page_url' => 'http://example.com/list?page=1', 'from' => 1, 'next_page_url' => null, 'path' => 'http://example.com/list', 'per_page' => 2, 'prev_page_url' => null, 'to' => 2, 'links' => [ 'create' => 'some-url', ], ];
Using appends you are free to add (or overwrite) the keys in this array:
$posts = PostPresenter::collection($paginatedCollection) ->only('id') ->appends([ 'foo' => 'bar', 'links' => ['store' => 'some-other-url'], ]) ->get();
This will now output:
[ 'current_page' => 1, 'data' => [ // your presented resources ], 'first_page_url' => 'http://example.com/list?page=1', 'from' => 1, 'next_page_url' => null, 'path' => 'http://example.com/list', 'per_page' => 2, 'prev_page_url' => null, 'to' => 2, 'foo' => 'bar', 'links' => [ 'create' => 'some-url', 'store' => 'some-other-url', ], ];
Note that appended values are merged recursively (as in the links
example above).
Returning values
Once you've configured an instantiated presenter the way you want, you can get the values as an array by chaining the get
method:
PostPresenter::make($post)->only('id', 'title')->get();
If you wish to return all the defined values in your presenter (without any configuration) then you can use the all
method:
PostPresenter::make($post)->all();
This is equivalent to the following:
PostPresenter::make($post)->get();
Finally, all presenters implement the Arrayable
interface, so you are passing your presenter to a context that looks for this contract, your presenter values will be automatically converted to an array without you having to use get()
(or all()
).
Here's an example using an Inertia response:
return Inertia::render('Posts/Show', [ 'post' => PostPresenter::make($post)->only('title'), ]);
In the example above we're defining our Inertia props manually in an array. You can also pass a presenter instance directly - it will be converted into an array of props automatically:
return Inertia::render('Posts/Show', PostPresenter::make($post)->only('title'));
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security
If you discover any security related issues, please email john@addition.com.au instead of using the issue tracker.
Treeware
You're free to use this package, but if it makes it to your production environment we would highly appreciate you buying or planting the world a tree.
It’s now common knowledge that one of the best tools to tackle the climate crisis and keep our temperatures from rising above 1.5C is to plant trees. If you contribute to my forest you’ll be creating employment for local families and restoring wildlife habitats.
You can buy trees at Offset Earth
Read more about Treeware at Treeware
Credits
John Wyles
All Contributors
License
The MIT License (MIT). Please see License File File for more information.