Craft CMS 3 Content Migration Examples

Mike Hudson
8 min readJul 20, 2018

--

Having access to the migration feature that was released with Craft 3 has been a game-changer for us at Vend. It allows us to review and test every change made to the backend schema. If anything goes wrong, we can migrate/down and reverse the migration. This article will run through some common scenarios when writing Craft 3 migrations for field, matrix, entry type and other structures.

Craft gives us access to the whole spectrum of services, models, fields and other classes. This means there are little limitations to what we can do. And if we really need to, we can use the Query class to directly perform SQL queries on the database. A detailed Craft 3 class reference is available on https://docs.craftcms.com/api/v3/.

Using the Craft migration CLI

Before we get started with writing migrations, let’s take a look at the CLI tool that Craft provides:

Running `./craft help` in the console gives us this info

The functions that we will be using in this tutorial are:

migrate/create <name>

Create a new migration file in the /migrations folder. This is the file that we will be working with. The name should describe the contents of the file.

migrate/up

Apply all migrations that haven’t run yet.

migrate/down

Revert the (single) migration that has been run last.

Awesome! Now we can get started on writing some example migration files.

Example Project

For the purpose of this article, we will look at creating a simple long-form template. This template will have the following contents:

  • Header image
  • Intro text
  • Matrix with various block types

We will start by creating the fields and then later on attach them to a new entry type.

Example 0: New field group migration

Before we start adding fields to our backend with migrations, we need a group to add them to. Let’s start by generating the file with the CLI.

./craft migrate/create add_example_group

This creates a new migration file in our /migrations folder

Create a new migration by running `./craft migrate/create <name>`

This is how the basic markup of a migration file looks:

The important things to note are the safeUp() and safeDown() methods. We will use safeUp() to write our changes to the database and safeDown() to revert said changes. In our case, the up method creates the group while the down method deletes it.

Creating a group is straightforward: we initialize an instance of the FieldGroup model and then save it with the fields service method saveGroup().

As we will discover throughout the rest of this tutorial, most migrations follow the same pattern that we just introduced: Create an instance of a model and save it with the service.

Running the migration with ./craft migrate/up shows that it gets successfully applied.

`./craft migrate/up` runs all new migrations

To make sure we can revert this change, we need to delete the group in our safeDown() part. This is a bit trickier since we need to know the groups’ id in order to delete it.

The first step is finding the right group. There are multiple ways to do this, the most straightforward being to query the database directly for it. For more information check out the yii query docs.

$group = (new \craft\db\Query())
->select("id")
->from("fieldgroups")
->where(["name" => "Example Group"])
->one();

Once we have the group, we can delete it with the deleteGroupById() method from the field service.

Let’s run the down migration in the CLI.

We reverted our migration with `./craft migrate/down`

That’s it! We can now run ./craft migrate/up to create the field group and ./craft migrate/down to revert the migration.

Example 1: New text field migration

Creating a text field is just as simple as creating the field group. After generating our new the migration file with …

./craft migrate/create add_text_field

… we can start writing our text field migration.

In the first part of the safeUp() method we find the group we created in the previous migration. We’re then using the PlainText model and the fields service to save the field, just like when we saved the group.

On the safeDown() method we retrieve the field with the getFieldByHandle() method and then delete it with deleteFieldById().

If we take a look in the backend after running this migration, we see the newly created field under the group that we created earlier.

The field that we created with the migration

Great! Let’s move on to some more complicated field types.

Example 2: New asset field migration

For the header image we will create a new asset field. In the settings of the asset field type, we need to provide a folderId so Craft knows where to save the asset to. Let’s start by finding the folder’s id:

$v = Craft::$app->volumes->getVolumeByHandle("siteAssets");
$folders = Craft::$app->assets->getFolderTreeByVolumeIds([$v->id]);
$folder = $folders[0]->id;

This snippet grabs a volume called siteAssets and gets all the folders that are assigned to that volume. Since we want to save to the root level of that volume, we grab the first folder in the tree with $folders[0]. With that information we can create the asset field.

We’re restricting the file types to be only of type image and set the source and upload location to the folder we selected. The safeDown() function should look exactly like the one from the text field migration, so we won’t need to look at that here.

Example 3: New matrix field migration

While writing a migration for a matrix field in Craft is a bit more bulky than creating a simple text or asset field, but we’re still using the same principles as with the other fields.

Let’s get straight into it and look at the code.

We are creating the matrix field, which contains multiple matrix block types, which each hold one or many fields. Let’s see how we did that:

Creating the matrix field

Same as with any other fields, a few special attributes such as minBlocks, maxBlocks and of course the blockTypes which will hold all of our block types in an array.

Creating the matrix block types inside of the “blockTypes” array

Matrix block types are basically wrappers around the fields that the block will hold. All the fields go into the fields array.

Adding the fields to the “fields” array

There’s really nothing more to it—we create instances of the field models that we want to be in our block type.

Running this migration leaves us with a matrix that has two different block types:

  • “Text” with a text field
  • “Image and Text” with an asset field and a text field
Two new block types in our new matrix field

Cool, now we’ve covered how to create certain types of fields. But what if we need to add a block type to an existing matrix?

Example 4: Add block type to existing matrix

Let’s add an opening hours block type to our previously created matrix. It will hold a table field with one row for day and one row for openingHours. We can achieve this by creating that block type instance with the block type model class and setting the fieldId to the id of the matrix field that it should be attached to.

We utilize the saveBlockType method of the matrix service to save that block type. Yup, it’s really that easy!

New matrix block type with a table

On the down migration we grab all the block types of our matrix field with getBlockTypes and loop through them to find the one with the right handle. Then we can just delete it with the deleteBlockType() method of the matrix service.

Example 5: Add field to a block type

Sometimes we might want to add a further field to a block type. Let’s say we want to be able to choose between three different text colors with a drop down in our justText block type.

This might seem a bit confusing, but it actually makes a lot of sense. We’re following these steps:

  1. Find the matrix field with getFieldByHandle()
  2. Find the right block type with looping through all the block types that getBlockTypes() gives us (and checking for the justText block type)
  3. Get all fields of that block type with getFields() and add the new one to the end of that array
  4. Set the fields and save the block type.

To revert that change, we use the same routine to retrieve the field and then simply delete it with the deleteFieldById() method.

Example 5: Update fields

Updating fields can be a dangerous game depending on our backend structure, so make sure whatever we update doesn’t break our site. We want to change the instructions attribute on one of our fields.

You probably guessed it already: we need to find the field, change the value and save the field. But what about safeDown? The migration file doesn’t really know what the instructions said before the change. For all we know, the instructions could have been altered in the backend interface by an admin. This is why it’s ok to leave the safeDown empty here. We should make sure that it returns true or otherwise it will block us from any migrations that lie further back.

Final words

We had a look at some basic ways to run migrations and create, update and delete fields and matrix blocks, but there is an almost unlimited amount of things you can do with migrations. I have uploaded all the examples used in this article (and more) to a GitHub repo that you can find here:

In a future tutorial, I will go through some further common scenarios such as creating sections / entry types and adding fields to a field layout tab. If there is anything else you’d like to see just let me know!

Next Tutorial coming very soon, stay posted
Creating sections, entry types and field layout

--

--