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?
- The data
- Fetching the entries
- Finding the sets
- Updating the data
- Putting it all together
- A note on bard sets
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: abc123type: primary_buttontext: 'Read More'url: 'http://example.com/'
id: abc123type: primary_buttontext: 'Read More'url: 'http://example.com/'
The new data should look like this:
id: abc123type: buttonsbuttons:-text: 'Read More'url: 'http://example.com/'type: primary
id: abc123type: buttonsbuttons:-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 entryif (! $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 valuedata_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 entryif (! $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 valuedata_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';