Creating a Statamic Compact Modifier

November 7, 2021 —John Koster

Throughout this article we will be creating a Statamic modifier that provides similar functionality to PHP's compact function. This function will create an array from the provided variables by name. As a refresher, let's take a look at how the compact function may be used in PHP:

1<?php
2$city = 'Anytown';
3$street = 'Anystreet';
4 
5$newArray = compact('city', 'street');
6 
7foreach ($newArray as $value) {
8 echo $value;
9}

The above code example will create an array of values by referencing the variables by name (city and street). The values of these variables will be added to our array in order, and once it has been evaluated the output would be:

1AnytownAnystreet

By the end of this article we will be able to accomplish something very similar directly in Antlers:

1---
2city: Anytown
3street: Anystreet
4---
5 
6{{ foreach :array="'view:city,view:street' | compact" }}
7 {{ value }}
8{{ /foreach }}

There are a lot of subtle things happening in the previous Antlers example, and we will not focus on them in detail just yet. For example, we are creating variables using Antlers front-matter, which can then be referenced by prefixing our variables with view:. Additionally, we are invoking Statamic's foreach tag, and supplying an interesting value for the array parameter.

Statamic's foreach tag allows the array variable to be specified by supplying a value for the array parameter. This is in contrast to how most developers are probably used to interacting with the foreach tag:

1{{ foreach:array_value }}
2 
3{{ /foreach:array_value }}

It is the fact that we can specify our own value using a parameter that will allow us to create our compact modifier and use it directly with the foreach tag later. Additionally, this article will take advantage of the fact that modifiers can be used when passing variables through dynamic bindings. If you are new to this concept or technique, consider reading through the Using Modifiers in Antlers Dynamic Bindings article before continuing.

#Basic Structure of Modifiers

In general, Antlers modifiers are simply functions that take some data as input and changes it, or modifies it, in some way. For example, a modifier can take a string and uppercase all of the characters within the string, or even create an array containing all the string's characters. The following example would return the upper-cased version of the value Hello, world!:

1{{# Outputs "HELLO, WORLD!" #}}
2{{ "Hello, world!" | upper }}

The input value for modifiers always come from the left. Modifiers can also accept parameters, and those values come from the right side of the modifier. An example of a core modifier that accepts parameters is the explode modifier:

1{{ "one, two, three" | explode:, }}

Statamic's explode modifier works the same as PHP's own explode function: it takes an input string and splits it into an array of smaller strings based on some delimiter. In the previous example, the modifier's value is one, two, three, and it has a single parameter: ,. Let's take a look at how the explode modifier is implemented in PHP to get a sense of how these values are all interacting. The following code example comes from Statamic's codebase:

1public function explode($value, $params)
2{
3 return explode(Arr::get($params, 0), $value);
4}

Modifier implementations always receive the value as their first argument, and any user-provided parameters will be available as the second argument, supplied as an array. When our previous Antlers sample is executing, the values of our function's parameters would be:

1$value:
2 "one, two, three"
3
4$params:
5 array:1 [
6 0 => ","
7 ]

It is because modifier parameters are always provided as an array that you will find similar calls to Arr::get($params, 0) throughout Statamic's internal codebase: it is purely to get a specific parameter from the input array. We will not be making use of modifier parameters when implementing our compact modifier.

#Creating Our Modifier

In this section we will work on getting our custom modifier created, and start on it's implementation. To get started, we will use Statamic's please utility to help us scaffold the code. From the root of your project, issue the following command to generate a new modifier class:

1php please make:modifier Compact

Once the command has executed you should see output similar to the following:

1Modifier created successfully.
2Your modifier class awaits: app/Modifiers/Compact.php

If you do not want to use the utility to scaffold the modifier, you may also manually create a file within your project's app/Modifiers/ directory. To follow along with this article, you would create a file app/Modifiers/Compact.php with the following content:

1<?php
2 
3namespace App\Modifiers;
4 
5use Statamic\Modifiers\Modifier;
6 
7class Compact extends Modifier
8{
9 public function index($value, $params, $context)
10 {
11 return $value;
12 }
13}

Now that the basic structure of our modifier has been generated we can begin work on the actual implementation. Before writing more PHP code, let's add a bit of Antlers to some page of our site so we can have Statamic call our modifier:

1{{ 'title,slug' | compact }}

If we were to run our code now, the $value supplied to our modifier would be title,slug. Our modifier will allow variables to be separated by commas, so we need to break that list into an array. Luckily, we've already seen the explode PHP function in this article, which we will make use of to do this. Let's update our modifier's index function to look like this:

1public function index($value, $params, $context)
2{
3 $variables = explode(',', $value);
4 
5 return $variables;
6}

When our modifier executes now, the $variables variable would contain the following value:

1array:2 [
2 0 => "title"
3 1 => "slug"
4]

At this point, we've effectively recreated a simpler version of Statamic's explode modifier. In the next section we will work on retrieving the actual values of those Antlers variables.

#Interacting With the Context

In the previous section we generated the scaffolding for our modifier, and started the basic implementation. We left off being able to split our modifier's input into an array of variable names, but did not actually retrieve the value of those variables. In this section, we will work to implement a simple way to retrieve the runtime value of Antlers variables; to do this, we will make use of the third parameter defined in our modifier's signature: the $context.

The context variable will contain all of the variables that are available to Antlers at the time our modifier was called. These variables will be supplied to our modifier as an array. We can take advantage of this to look up the variable names within the context, and then return their values. Let's update our modifier's function to look like the following example:

1public function index($value, $params, $context)
2{
3 return collect(explode(',', $value))
4 ->map(function ($variable) use ($context) {
5 
6 if (array_key_exists($variable, $context)) {
7 return $context[$variable];
8 }
9 
10 return null;
11 })->all();
12}

We now have a lot more going on, so let's take it apart:

  • The collect function will convert our array of variable names to a Collection instance, which provides many helpful methods for interacting with and manipulating arrays
  • The map method will visit each element of our array and apply some function it it. The return value of the function we supply will become that element's value in a new array that is being created behind the scenes
  • The final all method call simply returns the array contained inside the Collection instance. Without this, our modifier would return an instance of Collection (in some situations this may be preferred, but for our use-case, returning an array is fine)

Let's take a closer look at our function definition we supplied to the map method:

1function ($variable) use ($context) { ... }

We have defined an anonymous function (also known as a closure) that will receive each element of our array of variables as it's input. The interesting part, however, is that use ($context) at the end. This is known as variable capture, or inheriting a variable from the parent scope. Whenever we see anonymous functions like this it is important to remember that they execute within their own scope: by default they do not have access to variables defined outside of the anonymous function. The use language construct allows us to receive a copy of variables defined outside of our anonymous function. For our use-case, we are asking PHP to give our callback a copy of our $context. Without this, we would receive errors similar to undefined variable $context.

Our anonymous function's body is relatively simple, and checks to see if the variable name exists within the current context (using array_key_exists). If a match is found, that value is returned, otherwise null is returned. Let's update our Antlers file to the following:

1{{ foreach :array="'title,slug' | compact" }}
2 {{ value }}<br />
3{{ /foreach }}

Assuming we had the following data available:

1title: 'Home'
2slug: 'home'

We should see output similar to the following:

1Home
2home

If the Antlers code was changed to reference variable names that have no value (those that return null), the for each loop should return no visible results:

1{{ foreach :array="'not_a_variable,also_not_a_variable' | compact" }}
2 {{ value }}<br />
3{{ /foreach }}

At this point we have a modifier that is capable of taking a list of variable names and returning a new array with their values that can be supplied to the foreach tag. We are doing this by determining if a variable exists within the context array by checking if a key exists. This method works fine for "simple" variables, but breaks down when we want to do anything more complicated such as array index lookups, or accessing nested paths:

1---
2one: 'One'
3two: 'Two'
4---
5 
6{{ foreach :array="'view:one,view:two' | compact" }}
7 {{ value }}<br />
8{{ /foreach }}

Once the previous code sample has executed, we would not see any visible Antlers output. This is because our current modifier is looking for exact matches when attempting to retrieve the value of a variable. We could solve this by attempting to break apart variable names, and checking if the context contains a matching array. This process could also be done recursively. This approach, however, quickly gets complicated, is error-prone, and a much simpler alternative exists: leveraging the Antlers parser to do the work for us.

#Interacting With the Antlers Parser

In the previous section we implemented a version of our compact modifier that is able to retrieve the value of simple variables from the current context. This works, but is not able to handle complicated variables such as view:variable_name or array[index]. In this section, we are going to refactor our modifier to take advantage of the Antlers parser to retrieve our variable value for us. Let's start by changing the contents of our app/Modifiers/Compact.php file to the following:

1<?php
2 
3namespace App\Modifiers;
4 
5use Statamic\Modifiers\Modifier;
6use Statamic\Facades\Antlers;
7 
8class Compact extends Modifier
9{
10 public function index($value, $params, $context)
11 {
12 return collect(explode(',', $value))
13 ->map(function ($variable) use ($context) {
14 
15 return Antlers::parser()
16 ->getVariable($variable, $context);
17 })->all();
18 }
19}

The important changes of this refactor are including the Statamic\Facades\Antlers class at the top of the file, and the changes to the map method's body. The Antlers class that we included is a façade. The Antlers façade provides a parser method, which will return us an Antlers parser implementation.

All Antlers parser implementations define a getVariable method, which is used to parse a variable name, and return the value associated with it. In the previous code example, we are supplying the modifier's context as the second argument to the getVariable method. This will cause the Antlers parser to look through the context when evaluating the variable.

If we refer back to this Antlers code example:

1---
2one: 'One'
3two: 'Two'
4---
5 
6{{ foreach :array="'view:one,view:two' | compact" }}
7 {{ value }}<br />
8{{ /foreach }}

The output would now display something similar to the following:

1One
2Two

This refactor will also allow users to supply more complicated variables such as when referring to array indices (also known as dynamic array access within the Statamic documentation), or when using nested variables paths.

#Conclusion

Throughout this article we explored Statamic modifiers in detail, and implemented a customer compact modifier. Our compact modifier can be used to convert a comma-delimited list of variable names into an array containing those variable's values. These values can then be further iterated, or supplied to other Antlers tags or modifiers.

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.