Swapping Antlers Layouts and Passing Data to Layouts

April 10, 2023 —John Koster

In this article, we will work through creating a custom Statamic Tag. Our custom tag will allow us to dynamically change the layout used by an Antlers template, similar to Blade's @extends directive. The results of this article are available as a Statamic addon through the Marketplace:

However, if you are curious on the through process behind the implementation of the package, this article is for you!

#Getting Started

Statamic provides a helpful command line utility to help scaffold custom addons and extensions. The command we use to do this is the make:addon command, which accepts the vendor and package name. For me, I supplied the following input to the command:

1php please make:addon stillat/antlers-layouts

After the command has executed, we should see some output similar to the following in our terminal application:

1Creating addon...
2[✓] Addon boilerplate created successfully.
3 
4Installing your addon with Composer. This may take a moment...
5Repository added to your application's composer.json successfully.
6[✓] Addon installed successfully.
7 
8🎉 Your addon package is ready: addons/stillat/antlers-layouts
9Learn how to build addons in our docs: https://statamic.dev/extending/addons

If we navigate to the new folder created for us, we will see a small number of files and folders that were made for us:

1├── .gitignore
2├── README.md
3├── composer.json
4└── src
5 ├── ServiceProvider.php

The .gitignore file contains a sensible default list of file and directory patterns that will be ignored by git.

The composer.json file contains the minimum information Composer requires to autoload our custom package, and some extra information to help Statamic and Laravel understand our package. We will be making changes to this file later.

The more exciting file created by the make:addon is the src/ServiceProvider.php file. This file contains our addon's ServiceProvider class, which will serve as the entry point for the package.

Statamic's service provider concept is very similar to Laravel's, providing a centralized location to configure our addon, such as registering custom Tags, event listeners, classes, etc. We will use some Statamic-specific features to register our custom Tag implementation later.

#Adding Statamic as a Dependency

The first change we will make to our addon's composer.json file is to require Statamic as a dependency:

1{
2 "name": "stillat/antlers-layouts",
3 "autoload": {
4 "psr-4": {
5 "Stillat\\AntlersLayouts\\": "src"
6 }
7 },
8 "require": {
9 "php": "^7.3|^8.0",
10 "statamic/cms": "^3.4"
11 },
12 "extra": {
13 "statamic": {
14 "name": "Antlers Layouts",
15 "description": "Antlers Layouts addon"
16 },
17 "laravel": {
18 "providers": [
19 "Stillat\\AntlersLayouts\\ServiceProvider"
20 ]
21 }
22 }
23}

We've added a new require section to let Composer know our package's dependencies. The php entry states that our package supports at least PHP 7.3 or PHP 8.0. The statamic/cms entry means that our package can run on any version of Statamic, at least version 3.4, but less than version 4.

When working with our addon inside an existing Statamic application, we usually get away with skipping this step as the Statamic libraries will already be available to us. However, listing these dependencies will be necessary if we wanted to start writing tests for our addon.

With our dependencies defined, we can now issue the following command from within our addon's root directory (not the root directory of the Statamic site itself):

1composer update

After some time, our addon's dependencies will be installed inside a newly created vendor folder.

#Scaffolding our Layout Tag

Now that we have most of our boilerplate out of the way and our addon's dependencies have been installed, we need to start thinking of how we will implement our custom layout Tag. Ideally, we should be able to write Antlers code similar to the following to change the layout file our template will use:

1{{ layout:layouts/new-layout }}

If we are successful, Statamic will use the file located at resources/views/layouts/new-layout.antlers.html as the layout for our template instead of the default resources/views/layout.antlers.html.

How would we even begin implementing this? To get started, we can at least scaffold our Layout tag by creating a new file at addons/stillat/antlers-layouts/src/Tags/Layout.php with the following contents:

1<?php
2 
3namespace Stillat\AntlersLayouts\Tags;
4 
5use Statamic\Tags\Tags;
6 
7class Layout extends Tags
8{
9 
10}

Now that we have our custom Tag scaffolded, we must inform Statamic to automatically load it. We can do this by updating our addon's service provider class and adding our Layout Tag to the $tags array:

1<?php
2 
3namespace Stillat\AntlersLayouts;
4 
5use Statamic\Providers\AddonServiceProvider;
6use Stillat\AntlersLayouts\Tags\Layout;
7 
8class ServiceProvider extends AddonServiceProvider
9{
10 protected $tags = [
11 Layout::class,
12 ];
13 
14 public function bootAddon()
15 {
16 }
17}

Perfect, we now have a custom Layout Tag scaffolded and have updated our addon's service provider to inform Statamic about it. However, we still need to figure out how to implement the dynamic layout functionality.

#Researching Statamic's Layout Behavior

Before we can implement our custom layout behavior, we must determine how Statamic does this internally. Through source diving, we will find that Statamic utilizes instances of the Statamic\View\View class when rendering Antlers views. Of particular interest is the render method, which makes a call to the shouldUseLayout method to determine if a layout should be used or not:

1<?php
2 
3namespace Statamic\View;
4 
5// ...
6 
7class View
8{
9 // ...
10 
11 public function render(): string
12 {
13 $cascade = $this->gatherData();
14 
15 if ($this->shouldUseLayout()) {
16 // ...
17 
18 $contents = view($this->templateViewName(), $cascade);
19 
20 // ...
21 
22 $contents = view(
23 $this->layoutViewName(),
24 // ...
25 );
26 } else {
27 $contents = view($this->templateViewName(), $cascade);
28 }
29 
30 // ...
31 
32 return $renderedContents;
33 }
34 
35 // ...
36}

If this check passes, we can see that it will render our template and call the layoutViewName to determine which layout template should be used. Digging into the implementation of this method, we see that it references an internal $layout property:

1<?php
2 
3namespace Statamic\View;
4 
5// ...
6 
7class View
8{
9 // ...
10 
11 protected function layoutViewName()
12 {
13 $view = $this->layout;
14 
15 if (view()->exists($subdirectoried = 'layouts.'.$view)) {
16 return $subdirectoried;
17 }
18 
19 return $view;
20 }
21 
22 // ...
23}

Now the question becomes, "How does this property get set?". Searching the file for $this->layout = , we can see that this variable can be set by calling the layout method with arguments:

1<?php
2 
3namespace Statamic\View;
4 
5// ...
6 
7class View
8{
9 // ...
10 
11 public function layout($layout = null)
12 {
13 if (func_num_args() === 0) {
14 return $this->layout;
15 }
16 
17 $this->layout = $layout;
18 
19 return $this;
20 }
21 
22 // ...
23}

Perfect. We now know how we can influence the layout used by our Antler's template, but we have yet to figure out a way to do this from our custom Tag.

#Accessing the View Instance

We have determined we can probably change the template's layout by calling the layout method on the Statamic\View\View instance. But how do we gain access to this View instance in order to actually do this?

Through more source diving, we can see that there are no clear events or life-cycle hooks provided by Statamic to gain access to this class instance before the view has actually been rendered; at the point the view is rendered, it is already too late to change the layout.

Knowing that there are no clear events that will give us access to the current View instance that will be used to generate our response, our next question is how Statamic creates this class instance? Are there some life-cycle hooks or events there?

After much more source diving, we can see that our View instance is created within the Statamic\Http\Responses\DataResponse class during typical front-end requests. Specifically, it is created inside the protected view method:

1<?php
2 
3namespace Statamic\Http\Responses;
4 
5// ...
6 
7class DataResponse implements Responsable
8{
9 // ..
10 
11 protected function view()
12 {
13 return app(View::class)
14 ->template($this->data->template())
15 ->layout($this->data->layout())
16 ->with($this->with)
17 ->cascadeContent($this->data);
18 }
19 
20 // ...
21}

Analyzing this file shows that no events or hooks are available to let us access the created View instance; overriding core classes is also out of the question since that leads to headaches/problems later. It might be tempting to give up at this point since there does not appear to be any supported way to do this. However, suppose we study the view method carefully. In that case, we can see that our View instance is created by Laravel's container by calling the app function.

The fact that Laravel's container will be creating the View instances for Statamic is interesting since Laravel's container does have life-cycle events we can tap into whenever a specific class is resolved by the container.

We can use Laravel's resolving method to be notified whenever a specific class is created/resolved by the container. Inside our addon's service provider class, we can make the following changes:

1<?php
2 
3namespace Stillat\AntlersLayouts;
4 
5use Statamic\Providers\AddonServiceProvider;
6use Statamic\View\View;
7use Stillat\AntlersLayouts\Tags\Layout;
8 
9class ServiceProvider extends AddonServiceProvider
10{
11 protected $tags = [
12 Layout::class,
13 ];
14 
15 public function bootAddon()
16 {
17 app()->resolving(View::class, function ($view) {
18 dd($view);
19 });
20 }
21}

Attempting to refresh our site, we can see that the dd function has dumped out the current View instance being used by Statamic. Fantastic! We now have a way to access the current View instance and can start implementing our custom Layout Tag.

#Implementing our Layout Tag

Let's add properties and methods to our Layout tag inside src/Tags/Layout.php and work through what each part is doing.

1<?php
2 
3namespace Stillat\AntlersLayouts\Tags;
4 
5use Statamic\Tags\Tags;
6use Statamic\View\State\ResetsState;
7 
8class Layout extends Tags implements ResetsState
9{
10 public static $lastView = null;
11 
12 public function index()
13 {
14 $this->setLayout($this->params->get('layout'));
15 }
16 
17 protected function setLayout($layout)
18 {
19 if (self::$lastView != null) {
20 self::$lastView->layout($layout);
21 }
22 }
23 
24 public function wildcard($tag)
25 {
26 $this->setLayout($tag);
27 }
28 
29 public static function resetStaticState()
30 {
31 self::$lastView = null;
32 }
33}

First, the public methods, index and wildcard, are how we will interact with our custom Tag within an Antlers template. If we were to write the following in a template, the index method would be called:

1{{ layout layout="custom-layout-file" }}

However, if we have the following Antlers template, the wildcard method will be called, and the $tag argument will be set to my-layout:

1{{ layout:my-layout }}

Each method ultimately calls the protected setLayout method, which calls the layout method on a static $lastView variable. This variable will eventually contain a reference to our View instance, but we assume it has it for now.

The next interesting part of our custom Tag implementation includes the ResetsState interface and the resetStaticState static method. The Antlers Runtime will call our resetStaticState method whenever it has finished creating a response, allowing us to reset any static variables we may have used. This is important as it will enable us to clean things up and prevent the current state from leaking between page generations, such as when using the static site generator (SSG) addon.

Now that our Tag will call the layout method on its static $lastView instance, we need to set this up. We will do this by updating our addon's service provider again; instead of using the dd function to dump the current view, we will instead set the static $lastView property on our Layout tag class:

1<?php
2 
3namespace Stillat\AntlersLayouts;
4 
5use Statamic\Providers\AddonServiceProvider;
6use Statamic\View\View;
7use Stillat\AntlersLayouts\Tags\Layout;
8 
9class ServiceProvider extends AddonServiceProvider
10{
11 protected $tags = [
12 Layout::class,
13 ];
14 
15 public function bootAddon()
16 {
17 app()->resolving(View::class, function ($view) {
18 dd($view);
19 Layout::$lastView = $view;
20 });
21 }
22}

If we were to add the following to a template in our Statamic project:

1{{ layout:my-custom-layout }}

and view that page in the browser, we are likely to receive a View [my-custom-layout] not found. error. This is great news, as we can now dynamically swap out the layout file used by our Antlers templates.

#Sharing Variables with our Layouts

Earlier in this article, I briefly mentioned that Statamic will first render the template and inject the rendered template's contents into the layout via. a template_contents variable. This behavior can sometimes make it non-obvious to pass variables or custom state to our layout template without resorting to complication section hacks. To help with this, we will update our Layout Tag to add a new share method that will allow us to inject new variables into any layout view:

1{{ layout:share custom-variable="value"
2 :custom-title="title" /}}

To accomplish this, we can add a new share method to our Layout implementation:

1<?php
2 
3namespace Stillat\AntlersLayouts\Tags;
4 
5use Statamic\Tags\Tags;
6use Statamic\View\State\ResetsState;
7 
8class Layout extends Tags implements ResetsState
9{
10 public static $lastView = null;
11 
12 public static $variables = [];
13 
14 public function index()
15 {
16 $this->setLayout($this->params->get('layout'));
17 }
18 
19 protected function setLayout($layout)
20 {
21 if (self::$lastView != null) {
22 self::$lastView->layout($layout);
23 }
24 }
25 
26 public function share()
27 {
28 self::$variables = array_merge(
29 self::$variables, $this->params->all()
30 );
31 }
32 
33 public function wildcard($tag)
34 {
35 $this->setLayout($tag);
36 }
37 
38 public static function resetStaticState()
39 {
40 self::$lastView = null;
41 self::$variables = [];
42 }
43}

Each time we use the layout:share Tag inside our Antlers template, it will merge all of the Tag's parameters with our static $variables array. Similar to how we reset the $lastView variable inside our resetStaticState method, we also clear all of our variables in-between page generations. Our layout:share Tag will now add all of our custom variables to our $variables array, but we still need to find a way to inject these into our layouts. To do this, we can take advantage of another Laravel concept: view composers.

An interesting side effect of adding the share method is that we can no longer use the {{ layout:share }} Tag to load a layout named "share". While this is certainly an unlikely occurrence, this is why we also added support for the index method so template authors can instead use {{ layout layout="share" }}.

View composers allow us to execute a callback function each time a view with a specific filename or pattern is created. We can update our addon's service provider like so:

1<?php
2 
3namespace Stillat\AntlersLayouts;
4 
5use Statamic\Providers\AddonServiceProvider;
6use Statamic\View\View;
7use Stillat\AntlersLayouts\Tags\Layout;
8 
9class ServiceProvider extends AddonServiceProvider
10{
11 protected $tags = [
12 Layout::class,
13 ];
14 
15 public function bootAddon()
16 {
17 app()->resolving(View::class, function ($view) {
18 Layout::$lastView = $view;
19 });
20 
21 view()->composer(['layout', 'layouts/*'], function ($view) {
22 $view->with(Layout::$variables);
23 Layout::$variables = [];
24 });
25 }
26}

By passing an array containing both layout and layouts/* to the view()->composer() method, each time the normal Statamic layout file is created, or any template within the resources/views/layouts directory is created, our callback function is will be executed. We then utilize the with method in our callback to inject all our variables into the layout. With these changes, we can now pass custom variables from anywhere in our site's template back up to our template.

#Wrapping Up

Throughout this article, we have taken several deep dives into the inner workings of Statamic and leveraged a few of Laravel's more exciting features to implement our custom Layout Tag. Using our custom Layout tag, we can now dynamically change our template's layout and share data with our layouts.

If you found this article useful, consider sharing it with your friends, colleagues, etc. It really helps! 😊

Some absolutely amazing
people

The following amazing people help support this site and my open source projects ♥️
If you're interesting in supporting my work and want to show up on this list, check out my GitHub Sponsors Profile.