View Models are useful for preparing data for your Twig views. It’s common (and typically desirable) for the shape of the data needed by your views to be different from how it is stored in the database or Post objects. How this data is transformed can be encapsulated in a View Model, cutting down repetitive code you have in your Controllers.
Example Controller
Below is an example of a controller that prepares a Post object for a media card.
View models are automatically converted to arrays when passed into a twig template. Public methods are used as keys in this array, and the method is executed to get the value.
Below we have added 3 public methods (title, description and published). These are the keys that our view needs.
The above will ensure the $context looks like the following when the view model has been converted to an array (without having to have prepared the card structure in the Controller):
$context = [// ...'card'=> ['title'=>'Post Title','description'=>'Lorem ipsum dolor...','published'=>'2019-02-23', ],]
Remember: All view models (and collections) in the context are automatically flattened to arrays before being passed to twig views.
Manually converting view models to arrays
If you need to convert a view model to an array before it gets passed into a view, then you can use the toArray() method.
There may be times where the automatic array conversion doesn't do quite what you need. For example, if your view needed the following data:
$context['cards'] = [ ['title'=>'Post Title','description'=>'Lorem ipsum dolor...','published'=>'2019-02-23', ], ['title'=>'Post Title 2','description'=>'Lorem ipsum dolor...','published'=>'2019-02-26', ],];
This can be achieved by writing your own toArray method on your view model.
<?phpnamespaceApp\ViewModels;useRareloop\Lumberjack\Post;useRareloop\Lumberjack\ViewModel;useApp\ViewModels\MediaCardViewModel;useTightenco\Collect\Support\Collection;classMediaCardsViewModelextendsViewModel{protected $posts;publicfunction__construct(Collection $posts) {$this->posts = $posts; }publicfunctioncards() {// Create a MediaCardViewModel for each postreturn$this->posts->map(function (Post $post) {returnMediaCardViewModel($post); }); }/** * Overwrite the toArray method to return array of view models, with no key */publicfunctiontoArray():array {return$this->cards(); }}
Keeping view models reusable
When writing a view model, the __construct() should accept all the data it needs in order to do the transformation.
For example, if you had a testimonial that has a quote and a citation, the view model could look something like this:
Now your view model is really generic, and does not care about where the data is coming from. If you give it a quote and a citation, it will make sure the twig view has the correct data.
Named Constructors
If your view model does not know about how to get data, then you will have to fetch that data in each controller that uses the view model. For example:
<?phpnamespaceApp;useRareloop\Lumberjack\Post;useTimber\Timber;useApp\ViewModels\TestimonialViewModel;useRareloop\Lumberjack\Http\Responses\TimberResponse;classSingleController{publicfunctionhandle() { $context =Timber::get_context(); $post =newPost;// Get the data from somewhere, for example from ACF// You would have to duplicate these two lines in each controller $quote =get_field('testimonial_quote', $post->id); $citation =get_field('testimonial_citation', $post->id); $context['testimonial'] =newTestimonialViewModel($quote, $citation);returnnewTimberResponse('mytemplate.twig', $context); }}
In order to keep your view models generic, and your controllers light (and DRY) you can create something called a "named constructor" on your view model.
This is simply a static method on your view model that constructs the view model for a specific use case.
In our example, we are creating a testimonial from a Post object. So we can add the following method to our view model:
<?phpnamespaceApp\ViewModels;useRareloop\Lumberjack\Post;useRareloop\Lumberjack\ViewModel;classTestimonialViewModelextendsViewModel{ protected $quote;protected $citation;publicstaticfunctionforPost(Post $post) {// Get the data from somewhere, for example from ACF $quote =get_field('testimonial_quote', $post->id); $citation =get_field('testimonial_citation', $post->id);// Create a new instance of this class returnnewstatic($quote, $citation); }publicfunction__construct($quote, $citation) {$this->quote = $quote;$this->citation = $citation; }publicfunctionquote() {return$this->quote; }publicfunctioncitation() {return$this->citation; }}
And we can now refactor our controller to use the named constructor like so:
You can have multiple named constructors on a view model to construct it with different data. For example you could have a PostTeasersViewModel which transforms a collection of posts ready for a list view.
And you could have the following named constructor:
latestPosts($limit = 3)- which knows how to get the latest n posts.
relatedPosts(Post $post)- which knows how to get posts related to given post
Using Hatchet
If you are using hatchet (Lumberjack's CLI), you can easily create view models with the following command: