Skip to main content
Jack Sleight .DEV
Distill

Refactoring Statamic content with Distill

Sometimes it's necessary to refactor your blueprints—for example, if you decide that a field should be renamed or a set should have a different structure. While it's easy to edit the field configurations, updating existing content to match can be a chore, especially if you have deeply nested sets in page builder type fields.

What's Distill?

My Distill addon is a general-purpose tool for querying the values within your entries, and can assist with refactoring content. When you run a query with Distill it fetches all the matching items from anywhere in the nested data as a flat array. Those items will include the key path to that item in the data, which you can then use to update the content.

The data

In this example I have a replicator based page builder with a primary_button set, and I want to refactor all instances of that into a more general-purpose buttons set that can contain multiple buttons each with their own type.

The old data looks like this:

id: abc123
type: primary_button
text: 'Read More'
url: 'http://example.com/'
id: abc123
type: primary_button
text: 'Read More'
url: 'http://example.com/'

The new data should look like this:

id: abc123
type: buttons
buttons:
-
text: 'Read More'
url: 'http://example.com/'
type: primary
id: abc123
type: buttons
buttons:
-
text: 'Read More'
url: 'http://example.com/'
type: primary

Fetching the entries

The first thing we need to do is fetch the entries we want to update. Distill can't query for entries, it only runs queries on the values inside the entries. For this example I'm just going to fetch all entries, but you'll probably want to limit the query to the affected collections. If you have a lot of entries I'd also recommend using chunking so you're not loading everything at once. We'll then need to loop over the entries and process each one individually.

use Statamic\Facades\Entry;
 
$entries = Entry::all();
 
foreach ($entries as $entry) {
// ...
}
use Statamic\Facades\Entry;
 
$entries = Entry::all();
 
foreach ($entries as $entry) {
// ...
}

Finding the sets

Next we need to search each entry for any old sets. Distill makes this very easy, just pass in the entry and specify the type of data you're looking for. You can add other conditions to the query if necessary, check the Distill docs. If the entry doesn't contain any instances of that set we can skip it entirely.

use JackSleight\StatamicDistill\Facades\Distill;
 
$items = Distill::query($entry)
->type('set:primary_button')
->get();
 
if (! $items->count()) {
continue;
}
use JackSleight\StatamicDistill\Facades\Distill;
 
$items = Distill::query($entry)
->type('set:primary_button')
->get();
 
if (! $items->count()) {
continue;
}

Updating the data

If the entry does contain instances of the set we'll go ahead and update it. The first thing we need to do is fetch the full entry data as a plain array. This is what we will be updating as we process each item.

$data = $entry->data()->all();
$data = $entry->data()->all();

Then, we loop over the found sets, and using the key path that Distill provides, fetch the old set value, build a new set value, and set it back in the data.

foreach ($items as $item)
$oldSet = data_get($data, $item->info->path);
$newSet = [
'id' => $oldSet['id'],
'type' => 'buttons',
'buttons' => [
[
'text' => $oldSet['text'],
'url' => $oldSet['url'],
'type' => 'primary',
],
],
];
data_set($data, $item->info->path, $newSet);
}
foreach ($items as $item)
$oldSet = data_get($data, $item->info->path);
$newSet = [
'id' => $oldSet['id'],
'type' => 'buttons',
'buttons' => [
[
'text' => $oldSet['text'],
'url' => $oldSet['url'],
'type' => 'primary',
],
],
];
data_set($data, $item->info->path, $newSet);
}

Finally we can update the entry with the updated data and save it. I'm using the save quietly method here as I don't want this to fire any events.

$entry->data($data);
$entry->saveQuietly();
$entry->data($data);
$entry->saveQuietly();

Putting it all together

Here's the complete process, wrapped up in a command you can run from the command line:

namespace App\Console\Commands;
 
use Illuminate\Console\Command;
use JackSleight\StatamicDistill\Facades\Distill;
use Statamic\Facades\Entry;
 
class RefactorSets extends Command
{
protected $signature = 'app:refactor-sets';
 
public function handle()
{
$entries = Entry::all();
 
foreach ($entries as $entry) {
// Search for the sets to be refactored
$items = Distill::query($entry)
->type('set:primary_button')
->get();
 
// If there are no matches skip this entry
if (! $items->count()) {
continue;
}
 
// Get the entry data
$data = $entry->data()->all();
 
foreach ($items as $item) {
// Get the old set value
$oldSet = data_get($data, $item->info->path);
 
// Create the new set value
$newSet = [
'id' => $oldSet['id'],
'type' => 'buttons',
'buttons' => [
[
'text' => $oldSet['text'],
'url' => $oldSet['url'],
'type' => 'primary',
],
],
];
 
// Set the new set value
data_set($data, $item->info->path, $newSet);
}
 
// Set the updated entry data and save
$entry->data($data);
$entry->saveQuietly();
}
}
}
namespace App\Console\Commands;
 
use Illuminate\Console\Command;
use JackSleight\StatamicDistill\Facades\Distill;
use Statamic\Facades\Entry;
 
class RefactorSets extends Command
{
protected $signature = 'app:refactor-sets';
 
public function handle()
{
$entries = Entry::all();
 
foreach ($entries as $entry) {
// Search for the sets to be refactored
$items = Distill::query($entry)
->type('set:primary_button')
->get();
 
// If there are no matches skip this entry
if (! $items->count()) {
continue;
}
 
// Get the entry data
$data = $entry->data()->all();
 
foreach ($items as $item) {
// Get the old set value
$oldSet = data_get($data, $item->info->path);
 
// Create the new set value
$newSet = [
'id' => $oldSet['id'],
'type' => 'buttons',
'buttons' => [
[
'text' => $oldSet['text'],
'url' => $oldSet['url'],
'type' => 'primary',
],
],
];
 
// Set the new set value
data_set($data, $item->info->path, $newSet);
}
 
// Set the updated entry data and save
$entry->data($data);
$entry->saveQuietly();
}
}
}

A note on bard sets

The example above is updating replicator sets. If you have bard sets, bear in mind that they're structured slightly differently, so you'll need to account for that when updating them. You can check whether the set is from a replicator or bard value with Distill's parent value:

$isBard = $item->info->parent->info->type === 'value:bard';
$isBard = $item->info->parent->info->type === 'value:bard';
28th November 2024