Sending additional data to all views for Antlers in Statamic using Laravel's View Composers

Published June 30th, 2023

Statamic gives us Globals, which are awesome. They're small yet flexible blueprint-able chunks of content that can be managed via Statamic's Control Panel, and are automatically available when using Antlers.

But have you ever needed some more global data that isn't a Statamic Global?

And as a side note, yikes how many keywords are in that title... but they all sort of need it for the problem and solution.

Given the vagueness (yet totally specific-ness) of the terms "global" and "data", searching the docs was incredibly specific for the things I wasn't actually after. So time to get stuck in to the code.

Looking at the Controller (src/Http/Controllers/FrontendController.php) that drives the front end, a Statamic View (src/View/View.php) is rendered, and that View is choc-full of delicious data. But how can I put more in? Both the Controller and View have references to "hydration" and "cascade" (get the image choice for this article now?)... could that be the key? As it turns out, this glorious Cascade Facade (src/View/Cascade.php) gave me the clue that was needed.

Looking at the hydration process here, the final step is to run the hydration callbacks. See that final line:

1public function hydrate()
2{
3 $this->data([]);
4 $this->sections = collect();
5 
6 return $this
7 ->hydrateVariables()
8 ->hydrateSegments()
9 ->hydrateGlobals()
10 ->hydrateContent()
11 ->hydrateViewModel()
12 ->runHydratedCallbacks();
13}
Copied!

Sweet, so there is some concept of callbacks... but how to get my own in there. The runHydratedCallbacks method runs all of the callbacks stored in the hydratedCallbacks array, which gets populated by calling the hydrated method.

Excellent... but the docs didn't help with the equally as vague yet specific term "hydrate". Google did help though, and found the release notes from 3.2.11 in 2021, and Jason's pull request that discusses the usage of the hydrated method.

Adding some code as simple as this to the AppServiceProvider did exactly what I was after:

1use Statamic\Facades\Cascade;
2 
3Cascade::hydrated(function ($cascade) {
4 $cascade->set('foo', 'bar');
5});
Copied!

Within Antlers, I now have access to which would output "bar". Success-ish.

Because I like to be thorough and am a good little coder, I kept reading the PR notes... and saw a few months later that Michael referenced it in an Ideas discussion about a View callback prior to rendering.

Here, Michael noted that Laravel already provided a way to hook in to the pre-render state using View Composers. You know, that section of Laravel's docs, under "The Basics", that clearly I had purged from memory, but in my defense, given most of my Laravel work is with Inertia, that only has a single layout, so surely that's a valid purging reasons, right?

When you RTFM, "view composers are callbacks or class methods that are called when a view is rendered". That's exactly what we need to do this!

Within the callback, we will have access to a ready-to-render Illuminate\View\View object that we can pass data to using the Laravel-standard with:

1use Illuminate\Support\Facades\View;
2 
3View::composer('*', function($view) {
4 $view->with('foo', 'bar');
5});
Copied!

The end result: a ready-to-render View that will give me access to and output "bar". Same result, but the preferred way to achieve this.

Note the '*' wildcard in the composer definition: this is Laravel's way of applying the callback to all views. For my instance, this is what was needed - basically global-level data.

But outside of the scope of global level data, you can also pass a string of a view name, or array of names, to the composer definition to only target a specific Statamic layout. For this example, let's assume you have multiple layouts, and a Collection in Statamic that uses a different layout - such as blog_layout.antlers.html. You could use the named approach to only provide this global level data to renders that use that view:

1use Illuminate\Support\Facades\View;
2 
3View::composer('blog_layout', function($view) {
4 $view->with('foo', 'bar');
5});
Copied!

In your normal layout.antlers.html file, you wont have access to - but you will in your blog_layout.antlers.html renders.

For things like this, I tend to drop things in to my AppServiceProvider - it's small and simple, so seems harmless. But if the app and site were to grow, this could start to become cluttered. Laravel's approach is to create a ViewServiceProvider where these composers can live. Yes, it's creating another service provider, but just like how HorizonServiceProvider has all the Horizon-specific code, and EventServiceProvider has all of our events and listeners, it makes sense to create this new ViewServiceProvider.

Just remember, when you create a new Service Provider, you need to also update the providers array in your config/app.php file.

So while Statamic doesn't really have what is necessary to get some global data to Antlers, it turns out that Laravel has that ready to go out of the box... and by learning more about how it all works, is actually a really great way to populate data to common views. Love it!

You may be interested in...