Andrew Welch
Published , updated · 5 min read · RSS Feed
Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.
Adding Content with Content Migrations in Craft CMS
Learn how to create content migrations in Craft CMS 5 to add content that syncs across environments
If you need to create Sections, Fields, Entry Types, etc. in Craft CMS you’d typically create them in local development, and then push the changes to other environments using Project Config.
This works great for schema changes, but you can’t add content like Entries this way.
There are cases where you might want to add content in an automated way, such as generating data, importing data, or using an external API.
It’d be nice if you could write some code to add the content, and ensure that code gets run deterministically in all environments.
My goal was to seed the database with a ton of data for performance testing
My specific use-case is that I needed to be able to add a whole bunch of Entries to the database with usable seed data for performance testing purposes.
I also wanted to be able to have the content migration create all of the Fields, EntryTypes, and Sections for me in addition to adding Entries, and I wanted to be able to remove everything it added after the testing was done.
And finally, I wanted to be able to do this testing in local development, as well as in a live production environment.
Link Content Migrations
This is exactly what Content Migrations in Craft CMS allow you to do.
Content Migrations to the rescue!
Content Migrations are just Yii2 migrations by another name. These are the same migrations that Craft and some plugins run when they need to update the database in some manner.
Migrations are PHP files that contain code which alters the database from one state to another. Migrations are version controlled in your Git repository along with the rest of your project’s source code.
Content Migrations are typically run via deployment, the same way you run pending Craft or plugin migrations:
php craft up
The Craft CLI command craft up runs pending migrations and applies pending project config changes.
You can also use the craft migrate CLI command to run migrations, roll back migrations, redo migrations, and more. Let’s have a look at the help for the craft migrate CLI command to see all of the available options:
DESCRIPTION
Manages Craft and plugin migrations.
A migration means a set of persistent changes to the application environment that is shared among different
developers. For example, in an application backed by a database, a migration may refer to a set of changes to
the database, such as creating a new table, adding a new table column.
This controller provides support for tracking the migration history, updating migrations, and creating new
migration skeleton files.
The migration history is stored in a database table named migrations. The table will be automatically
created the first time this controller is executed, if it does not exist.
Below are some common usages of this command:
~~~
# creates a new migration named 'create_user_table' for a plugin with the handle pluginHandle.
craft migrate/create create_user_table --plugin=pluginHandle
# applies ALL new migrations for a plugin with the handle pluginHandle
craft migrate up --plugin=pluginHandle
~~~
SUB-COMMANDS
- migrate/all Runs all pending Craft, plugin, and content migrations.
- migrate/create Creates a new migration.
- migrate/down Downgrades the application by reverting old migrations.
- migrate/fresh Not supported.
- migrate/history Displays the migration history.
- migrate/mark Modifies the migration history to the specified version.
- migrate/new Displays the un-applied new migrations.
- migrate/redo Redoes the last few migrations.
- migrate/to Upgrades or downgrades till the specified version.
- migrate/up (default) Upgrades Craft by applying new migrations.
To see the detailed information about individual sub-commands, enter:
craft help <sub-command>
You’ll also see the migration in the Craft CP via Utilities → Migrations, where you can run them as well (but we’ll stick to the CLI):
Link Creating a Migration
We’ve explored creating content migrations before in the Indexing by Custom Field in Craft CMS article before, so let’s create a new migration using the CLI again using craft migrate/create:
/var/www/project/cms_v5 $ php craft migrate/create seed_db_data
Create new migration '/var/www/project/cms_v5/migrations/m240430_143914_seed_db_data.php'? (yes|no) [yes]:
New migration created successfully.
Content Migrations you create are stored in the migrations/ directory in your project. Note that it also added a date and timestamp that prefixes the migration name we gave it.
Then after we’re done writing our migration, we can check it into our project’s repository like any other code.
The skeleton migration it created for us doesn’t do anything at the moment:
<?php
namespace craft\contentmigrations;
use Craft;
use craft\db\Migration;
/**
* m240430_143914_seed_db_data migration.
*/
class m240430_143914_seed_db_data extends Migration
{
/**
* @inheritdoc
*/
public function safeUp(): bool
{
// Place migration code here...
return true;
}
/**
* @inheritdoc
*/
public function safeDown(): bool
{
echo "m240430_143914_seed_db_data cannot be reverted.\n";
return false;
}
}
The safeUp() method is called when the migration is run. The safeDown() method is optionally implemented, and is called when the migration is reverted.
Link Migration code overview
We’re going to show the code section by section, with explanations as we go. But if you’d like to see the full migration in one copy & pasteable place, you can find it here in a Gist.
We’re going to flesh out the skeleton migration we created earlier by adding code that:
- Creates Fields
- Creates EntryTypes
- Creates Sections
- Adds the Fields we create to the EntryTypes
- Creates a large number of Entries with random seed data
We’ll also implement code to undo all of this so we can remove our seed data seamlessly.
While what we’re doing here may not be exactly your use case, the techniques we’ll delve into should be useful regardless of what changes you want to make to the database and where the data is coming from.
N.B. As of Craft 5.0.0, changes to Project Config (such as we’re making here by adding Fields, Sections, etc.) are now safe to make within migrations, even when allowAdminChanges is false.
If you just want to create or import data from some source via a content migration, you can eschew making schema changes in it, and just skip down to the createEntryData() method.
Link Faking it with Faker
Before we jump into the code, we’re going to be using a PHP package called Faker to generate random data for us.
Even though Faker has been sunsetted, it’s still a very useful package to use if you need to generate some random but useful data.
But first we’ll need to require it for our project with Composer:
composer require fakerphp/faker
Now that we’ve added Faker to our project, let’s jump in and have a look at the code we’re adding to our migration class.
Link Constants
We define a number of constants that define what we’re going to create:
// Handle to the user that should own the entries
private const USER_NAME = 'admin';
// The number of entries to create
private const NUM_ENTRIES = 10000;
// Array of configs for the Fields to create
private const FIELD_CONFIGS = [
[
'class' => PlainText::class,
'name' => 'Demo Data',
'handle' => 'demoData',
'translationMethod' => Field::TRANSLATION_METHOD_NONE,
'multiline' => 0,
'columnType' => Schema::TYPE_STRING,
],
];
// Array of configs for the EntryTypes to create
private const ENTRY_TYPE_CONFIGS = [
[
'class' => EntryType::class,
'name' => 'Demo',
'handle' => 'demo',
'customFields' => [
'demoData'
]
],
];
// Array of configs for the Sections to create
private const SECTION_CONFIGS = [
[
'class' => Section::class,
'name' => 'Demo',
'handle' => 'demo',
'type' => Section::TYPE_CHANNEL,
'enableVersioning' => false,
'entryTypes' => [
'demo',
]
],
];
The _CONFIGS constants are arrays of arrays that define the object configuration that we pass to the Yii2 method createObject().
Think of createObject() as an enhanced version of the new operator in PHP. It allows you to pass in the class of the object to create, along with any default values for properties.
We define them as constants so it’s easy to add whatever Fields, Sections, etc. by adding them to the constant declaration, instead of having to modify our code.
Link safeUp()
The safeUp() method is called when your migration is run via craft migrate/up:
/**
* @inheritdoc
*/
public function safeUp(): bool
{
try {
$this->createFields(self::FIELD_CONFIGS);
$this->createEntryTypes(self::ENTRY_TYPE_CONFIGS);
$this->createSections(self::SECTION_CONFIGS);
$this->addFieldsToEntryTypes(self::SECTION_CONFIGS, self::ENTRY_TYPE_CONFIGS, self::FIELD_CONFIGS);
$this->createEntryData(self::FIELD_CONFIGS, self::SECTION_CONFIGS[0]['handle'], self::ENTRY_TYPE_CONFIGS[0]['handle'], self::USER_NAME, self::NUM_ENTRIES);
} catch (Throwable $e) {
Craft::error($e->getMessage(), __METHOD__);
return false;
}
return true;
}
Here we’re calling methods to create the Fields, EntryTypes & Sections defined in our constants, then adding the Fields to the EntryTypes, and finally populating a whole bunch of new Entries with seed data.
In our case, we’re passing in the various config via constants, but they could come from a separate JSON file, or an API endpoint, or whatever makes sense for your use-case.
Having the methods accept an array of configuration arrays makes it pretty generic and re-usable.
Link safeDown()
The safeDown() method is called when your migration is run via craft migrate/down:
/**
* @inheritdoc
*/
public function safeDown(): bool
{
try {
$this->deleteEntryData();
$this->removeFieldsFromEntryTypes();
$this->deleteFields(self::FIELD_CONFIGS);
$this->deleteSections(self::SECTION_CONFIGS);
$this->deleteEntryTypes(self::ENTRY_TYPE_CONFIGS);
} catch (Throwable $e) {
Craft::error($e->getMessage(), __METHOD__);
return false;
}
return true;
}
Here, we do the inverse of what the safeUp() method does, removing the things we added in the reverse order that we added them via safeUp().
The methods deleteEntryData() and removeFieldsFromEntryTypes() don’t actually do anything, because the Entry data and the Field associations with EntryTypes are both deleted when we delete the EntryTypes via the deleteEntryTypes() method.
I included them here just for instructional purposes, to show that we’re undoing what we did in safeUp(). Normally I’d just omit these methods (since they do nothing) and just put a comment in the code noting that deleteEntryTypes() would take care of it.
Note that not all migrations need to implement safeDown() at all; none of the Craft CMS migrations do.
I wanted to implement safeDown() so that I could easily remove all traces of the seeded data, but if that doesn’t make sense for your migration, don’t feel compelled to implement this method (and the other methods called by it).
Link Creating & Deleting Fields
Here, we have two reciprocal methods that create and delete the Fields defined in the passed-in array of configs:
/**
* Create Fields based on $fieldConfigs
*
* @param array $fieldConfigs
* @return void
* @throws InvalidConfigException
* @throws Throwable
*/
protected function createFields(array $fieldConfigs): void
{
$fields = Craft::$app->getFields();
foreach ($fieldConfigs as $fieldConfig) {
$handle = $fieldConfig['handle'];
if ($fields->getFieldByHandle($handle)) {
Console::outputWarning("Field $handle already exists");
continue;
}
// Create & save each Field
$field = Craft::createObject(array_merge($fieldConfig, [
]));
$fields->saveField($field);
}
}
Here we check to see if the field already exists, and skip creating it in that case.
Then we call createObject() for each passed-in configuration array, and then save that field out.
Note that we’re calling array_merge() to merge in the passed-in config with anything we might want to add to the config. In this case, we’re not adding anything, but it’s there for easy future expansion.
/**
* Delete Fields based on $fieldConfigs
*
* @param array $fieldConfigs
* @return void
* @throws Throwable
*/
protected function deleteFields(array $fieldConfigs): void
{
$fields = Craft::$app->getFields();
foreach ($fieldConfigs as $fieldConfig) {
$handle = $fieldConfig['handle'];
// Get and delete each field
$field = $fields->getFieldByHandle($handle);
if ($field) {
Craft::$app->getFields()->deleteField($field);
}
}
}
In the reciprocal method, we iterate through and delete each field in the passed-in configuration array
Link Creating & Deleting EntryTypes
Here, we have two reciprocal methods that create and delete the EntryTypes defined in the passed-in array of configs:
/**
* Create EntryTypes based on $entryTypeConfigs
*
* @param array $entryTypeConfigs
* @return void
* @throws EntryTypeNotFoundException
* @throws InvalidConfigException
* @throws Throwable
*/
protected function createEntryTypes(array $entryTypeConfigs): void
{
$entries = Craft::$app->getEntries();
foreach ($entryTypeConfigs as $entryTypeConfig) {
$handle = $entryTypeConfig['handle'];
if ($entries->getEntryTypeByHandle($handle)) {
Console::outputWarning("EntryType $handle already exists");
continue;
}
// We use the custom field handles later on, to add them to the EntryType's layout
unset($entryTypeConfig['customFields']);
// Create & save each EntryType
$entryType = Craft::createObject(array_merge($entryTypeConfig, [
]));
if (!$entries->saveEntryType($entryType)) {
$entryType->validate();
Console::outputWarning("EntryType $handle could not be saved" . PHP_EOL . print_r($entryType->getErrors(), true));
return;
}
}
}
Note that we’re doing something special here, in that we’re calling unset() on the customFields property from the configuration array before passing it in to createObject().
This is because customFields is not actually a property for an EntryType; we’re using it because we need to know what custom field handles to add to the entry type later on, and this is a convenient place to stash that data.
/**
* Delete EntryTypes based on the $entryTypeConfigs
*
* @param array $entryTypeConfigs
* @return void
* @throws Throwable
*/
protected function deleteEntryTypes(array $entryTypeConfigs): void
{
$entries = Craft::$app->getEntries();
foreach ($entryTypeConfigs as $entryTypeConfigConfig) {
$handle = $entryTypeConfigConfig['handle'];
$entryType = $entries->getEntryTypeByHandle($handle);
if ($entryType) {
$entries->deleteEntryType($entryType);
}
}
}
In this reciprocal method, we’re just iterating through the configuration arrays and deleting the EntryTypes by handle.
Link Creating & Deleting Sections
Here, we have two reciprocal methods that create and delete the Sections defined in the passed-in array of configs:
/**
* Create Sections based on the $sectionConfigs
*
* @param array $sectionConfigs
* @return void
* @throws InvalidConfigException
* @throws SectionNotFoundException
* @throws SiteNotFoundException
* @throws Throwable
*/
protected function createSections(array $sectionConfigs): void
{
$entries = Craft::$app->getEntries();
foreach ($sectionConfigs as $sectionConfig) {
$handle = $sectionConfig['handle'];
if ($entries->getSectionByHandle($handle)) {
Console::outputWarning("Section $handle already exists");
continue;
}
// Get all of the entry types by handle
$entryTypes = [];
$entryTypeHandles = $sectionConfig['entryTypes'];
foreach ($entryTypeHandles as $entryTypeHandle) {
$entryType = $entries->getEntryTypeByHandle($entryTypeHandle);
if ($entryType) {
$entryTypes[] = $entryType;
}
}
// Create & save each Section
/** @var Section $section */
$section = Craft::createObject(array_merge($sectionConfig, [
'siteSettings' => array_map(
fn(Site $site) => new Section_SiteSettings([
'siteId' => $site->id,
'hasUrls' => true,
'uriFormat' => "$handle/{slug}",
'template' => "$handle/_entry",
]),
Craft::$app->getSites()->getAllSites(true),
),
'entryTypes' => $entryTypes,
]));
if (!$entries->saveSection($section)) {
$section->validate();
Console::outputWarning("Section $handle could not be saved" . PHP_EOL . print_r($section->getErrors(), true));
}
}
}
We once again skip any Section handles that already exist, then we loop through all of the entryTypes to accumulate them, because we’ll need them later on when we create our Section object.
Then we create our Section via createObject(), merging the passed-in configuration array with the required properties siteSettings & entryTypes, which we create dynamically, and then we save the new Section.
/**
* Delete Sections based on the $sectionConfigs
*
* @param array $sectionConfigs
* @return void
* @throws Throwable
*/
protected function deleteSections(array $sectionConfigs): void
{
$entries = Craft::$app->getEntries();
foreach ($sectionConfigs as $sectionConfig) {
$handle = $sectionConfig['handle'];
$section = $entries->getSectionByHandle($handle);
if ($section) {
$entries->deleteSection($section);
}
}
}
In this reciprocal method, we iterate through the passed-in configuration array, and delete all of the Sections.
Link Adding and Removing Fields
Here, we have two reciprocal methods that add and remove the Fields we’ve created to the EntryTypes we’ve created:
/**
* Add the Fields to the EntryTypes for each $sectionConfigs
*
* @param array $sectionConfigs
* @param array $entryTypeConfigs
* @param array $fieldConfigs
* @return void
* @throws EntryTypeNotFoundException
* @throws InvalidConfigException
* @throws Throwable
*/
protected function addFieldsToEntryTypes(array $sectionConfigs, array $entryTypeConfigs, array $fieldConfigs): void
{
$entries = Craft::$app->getEntries();
foreach ($sectionConfigs as $sectionConfig) {
// Iterate through each Section
$sectionHandle = $sectionConfig['handle'];
$section = $entries->getSectionByHandle($sectionHandle);
if (!$section) {
Console::outputWarning("Section $sectionHandle doesn't exist");
return;
}
// Iterate through each EntryType
foreach ($entryTypeConfigs as $entryTypeConfig) {
$entryTypeHandle = $entryTypeConfig['handle'];
if (!in_array($entryTypeHandle, $sectionConfig['entryTypes'], true)) {
continue;
}
$entryType = $entries->getEntryTypeByHandle($entryTypeHandle);
if (!$entryType) {
Console::outputWarning("EntryType $entryTypeHandle doesn't exist");
return;
}
// Iterate through each Field
$elements = [];
foreach ($fieldConfigs as $fieldConfig) {
$fieldHandle = $fieldConfig['handle'];
if (!in_array($fieldHandle, $entryTypeConfig['customFields'], true)) {
continue;
}
$field = Craft::$app->getFields()->getFieldByHandle($fieldHandle);
if (!$field) {
Console::outputWarning("Field $fieldHandle doesn't exist");
continue;
}
$elements[] = Craft::createObject([
'class' => CustomField::class,
'fieldUid' => $field->uid,
'required' => false,
]);
}
// Just assign the fields to the first tab
$layout = $entryType->getFieldLayout();
$tabs = $layout->getTabs();
$tabs[0]->setElements(array_merge($tabs[0]->getElements(), $elements));
$layout->setTabs($tabs);
$entryType->setFieldLayout($layout);
$entries->saveEntryType($entryType);
}
}
}
Here we iterate through all of the Sections in the passed-in configuration array, skipping any Sections that don’t exist.
Then for each Section, we iterate through all of the EntryTypes in the passed-in configuration array, skipping any EntryTypes that our Section doesn’t use, and also skipping any EntryTypes that don’t exist.
Then for each EntryType, we iterate through all of the Fields in the passed-in configuration array, skipping any Fields that our EntryType doesn’t use, and also skipping any Fields that don’t exist.
We then create a CustomField class to represent the Field instance in the FieldLayout, and add those CustomFields to the first tab in our default FieldLayout for the EntryType, and save EntryType.
/**
* Remove the Fields from the EntryTypes
*
* @return void
*/
protected function removeFieldsFromEntryTypes(): void
{
// Do nothing, these will be destroyed along with the EntryType
}
This reciprocal method does nothing, since the field instances will be destroyed when we delete our EntryType.
Link Creating & Deleting Entries
Here, we have two reciprocal methods that create and delete Entries populated with seed data from Faker:
/**
* Create the Entry data
*
* @param array $fieldConfigs
* @param string $sectionHandle
* @param string $entryTypeHandle
* @param string $userName
* @param int $numEntries
* @return void
* @throws InvalidConfigException
* @throws Throwable
* @throws ElementNotFoundException
* @throws Exception
*/
protected function createEntryData(array $fieldConfigs, string $sectionHandle, string $entryTypeHandle, string $userName, int $numEntries): void
{
$faker = Factory::create();
$entries = Craft::$app->getEntries();
$section = $entries->getSectionByHandle($sectionHandle);
if (!$section) {
Console::outputWarning("Section $sectionHandle doesn't exist");
return;
}
$entryType = $entries->getEntryTypeByHandle($entryTypeHandle);
if (!$entryType) {
Console::outputWarning("EntryType $entryTypeHandle doesn't exist");
return;
}
$user = Craft::$app->users->getUserByUsernameOrEmail($userName);
if (!$user) {
Console::outputWarning("User $userName doesn't exist");
return;
}
// Do it as a bulk operation, see: https://github.com/craftcms/cms/pull/14032
$bulkKey = Craft::$app->elements->beginBulkOp();
for ($i = 0; $i < $numEntries; $i++) {
$name = $faker->unique()->name();
/* @var Entry $entry */
$entry = Craft::createObject([
'class' => Entry::class,
'sectionId' => $section->id,
'typeId' => $entryType->id,
'authorId' => $user->id,
'title' => $name,
]);
// Just the essentials for bulk import/creation
$entry->setScenario(Element::SCENARIO_ESSENTIALS);
foreach ($fieldConfigs as $fieldConfig) {
$handle = $fieldConfig['handle'];
$entry->setFieldValue($handle, $name);
}
if (Craft::$app->elements->saveElement($entry)) {
Console::output("#$i - Added entry " . Console::ansiFormat($name, [Console::FG_YELLOW]));
} else {
Console::outputWarning("#$i - Failed to add entry $name");
}
}
Craft::$app->elements->endBulkOp($bulkKey);
}
Here we create a Factory for Faker, then ensure that the passed in Section, EntryType, and User exist.
Next, we tell Craft & plugins that we’re performing a bulk operation via beginBulkOp() & endBulkOp(), so that they can wait until the bulk operation is done before doing additional processing.
Then, we loop $numEntries times, creating a new Entry element via createObject(). We then loop through the Fields configuration array to set the custom field value for each custom field with seed data from Faker and save the Entry element.
Note that we also set the Entry element’s scenario to Element::ESSENTIALS so that minimal validation is done when saving the element.
Finally, to make things look pretty, we use the built-in Yii2 Console helper class to add some color to the console output (and also make warnings stand out).
/**
* Delete all entries from $sectionHandle
*
* @return void
*/
protected function deleteEntryData(): void
{
// Do nothing, these will be destroyed along with the EntryType
}
This reciprocal method does nothing, since the Entries will be destroyed when we delete our EntryType.
Link The Entire Migration Class
Here’s the entire migration you can copy & paste into a file named m240430_143914_seed_db_data.php in your migrations/ directory (you can also get it from the Gist):
<?php
/**
* Craft CMS 5 content migration that creates & deletes Fields, EntryTypes & Sections, adds the Fields to the
* EntryTypes, adds the EntryTypes to the Sections, and then seeds the newly created Section with faked data.
* Also has a `safeDown()` method to revert what the migration adds
*
* @licence MIT
* @link https://nystudio107.com
* @copyright Copyright (c) nystudio107
*/
namespace craft\contentmigrations;
use Craft;
use craft\base\Element;
use craft\base\Field;
use craft\db\Migration;
use craft\elements\Entry;
use craft\errors\ElementNotFoundException;
use craft\errors\EntryTypeNotFoundException;
use craft\errors\SectionNotFoundException;
use craft\errors\SiteNotFoundException;
use craft\fieldlayoutelements\CustomField;
use craft\fields\PlainText;
use craft\helpers\Console;
use craft\models\EntryType;
use craft\models\Section;
use craft\models\Section_SiteSettings;
use craft\models\Site;
use Faker\Factory;
use Throwable;
use yii\base\Exception;
use yii\base\InvalidConfigException;
use yii\db\Schema;
/**
* m240430_143914_seed_db_data migration.
*/
class m240430_143914_seed_db_data extends Migration
{
// Handle to the user that should own the entries
private const USER_NAME = 'admin';
// The number of entries to create
private const NUM_ENTRIES = 10000;
// Array of configs for the Fields to create
private const FIELD_CONFIGS = [
[
'class' => PlainText::class,
'name' => 'Demo Data',
'handle' => 'demoData',
'translationMethod' => Field::TRANSLATION_METHOD_NONE,
'multiline' => 0,
'columnType' => Schema::TYPE_STRING,
],
];
// Array of configs for the EntryTypes to create
private const ENTRY_TYPE_CONFIGS = [
[
'class' => EntryType::class,
'name' => 'Demo',
'handle' => 'demo',
'customFields' => [
'demoData'
]
],
];
// Array of configs for the Sections to create
private const SECTION_CONFIGS = [
[
'class' => Section::class,
'name' => 'Demo',
'handle' => 'demo',
'type' => Section::TYPE_CHANNEL,
'enableVersioning' => false,
'entryTypes' => [
'demo',
]
],
];
/**
* @inheritdoc
*/
public function safeUp(): bool
{
try {
$this->createFields(self::FIELD_CONFIGS);
$this->createEntryTypes(self::ENTRY_TYPE_CONFIGS);
$this->createSections(self::SECTION_CONFIGS);
$this->addFieldsToEntryTypes(self::SECTION_CONFIGS, self::ENTRY_TYPE_CONFIGS, self::FIELD_CONFIGS);
$this->createEntryData(self::FIELD_CONFIGS, self::SECTION_CONFIGS[0]['handle'], self::ENTRY_TYPE_CONFIGS[0]['handle'], self::USER_NAME, self::NUM_ENTRIES);
} catch (Throwable $e) {
Craft::error($e->getMessage(), __METHOD__);
return false;
}
return true;
}
/**
* @inheritdoc
*/
public function safeDown(): bool
{
try {
$this->deleteEntryData();
$this->removeFieldsFromEntryTypes();
$this->deleteFields(self::FIELD_CONFIGS);
$this->deleteSections(self::SECTION_CONFIGS);
$this->deleteEntryTypes(self::ENTRY_TYPE_CONFIGS);
} catch (Throwable $e) {
Craft::error($e->getMessage(), __METHOD__);
return false;
}
return true;
}
/**
* Create Fields based on $fieldConfigs
*
* @param array $fieldConfigs
* @return void
* @throws InvalidConfigException
* @throws Throwable
*/
protected function createFields(array $fieldConfigs): void
{
$fields = Craft::$app->getFields();
foreach ($fieldConfigs as $fieldConfig) {
$handle = $fieldConfig['handle'];
if ($fields->getFieldByHandle($handle)) {
Console::outputWarning("Field $handle already exists");
continue;
}
// Create & save each Field
$field = Craft::createObject(array_merge($fieldConfig, [
]));
$fields->saveField($field);
}
}
/**
* Delete Fields based on $fieldConfigs
*
* @param array $fieldConfigs
* @return void
* @throws Throwable
*/
protected function deleteFields(array $fieldConfigs): void
{
$fields = Craft::$app->getFields();
foreach ($fieldConfigs as $fieldConfig) {
$handle = $fieldConfig['handle'];
// Get and delete each field
$field = $fields->getFieldByHandle($handle);
if ($field) {
Craft::$app->getFields()->deleteField($field);
}
}
}
/**
* Create EntryTypes based on $entryTypeConfigs
*
* @param array $entryTypeConfigs
* @return void
* @throws EntryTypeNotFoundException
* @throws InvalidConfigException
* @throws Throwable
*/
protected function createEntryTypes(array $entryTypeConfigs): void
{
$entries = Craft::$app->getEntries();
foreach ($entryTypeConfigs as $entryTypeConfig) {
$handle = $entryTypeConfig['handle'];
if ($entries->getEntryTypeByHandle($handle)) {
Console::outputWarning("EntryType $handle already exists");
continue;
}
// We use the custom field handles later on, to add them to the EntryType's layout
unset($entryTypeConfig['customFields']);
// Create & save each EntryType
$entryType = Craft::createObject(array_merge($entryTypeConfig, [
]));
if (!$entries->saveEntryType($entryType)) {
$entryType->validate();
Console::outputWarning("EntryType $handle could not be saved" . PHP_EOL . print_r($entryType->getErrors(), true));
return;
}
}
}
/**
* Delete EntryTypes based on the $entryTypeConfigs
*
* @param array $entryTypeConfigs
* @return void
* @throws Throwable
*/
protected function deleteEntryTypes(array $entryTypeConfigs): void
{
$entries = Craft::$app->getEntries();
foreach ($entryTypeConfigs as $entryTypeConfigConfig) {
$handle = $entryTypeConfigConfig['handle'];
$entryType = $entries->getEntryTypeByHandle($handle);
if ($entryType) {
$entries->deleteEntryType($entryType);
}
}
}
/**
* Create Sections based on the $sectionConfigs
*
* @param array $sectionConfigs
* @return void
* @throws InvalidConfigException
* @throws SectionNotFoundException
* @throws SiteNotFoundException
* @throws Throwable
*/
protected function createSections(array $sectionConfigs): void
{
$entries = Craft::$app->getEntries();
foreach ($sectionConfigs as $sectionConfig) {
$handle = $sectionConfig['handle'];
if ($entries->getSectionByHandle($handle)) {
Console::outputWarning("Section $handle already exists");
continue;
}
// Get all of the entry types by handle
$entryTypes = [];
$entryTypeHandles = $sectionConfig['entryTypes'];
foreach ($entryTypeHandles as $entryTypeHandle) {
$entryType = $entries->getEntryTypeByHandle($entryTypeHandle);
if ($entryType) {
$entryTypes[] = $entryType;
}
}
// Create & save each Section
/** @var Section $section */
$section = Craft::createObject(array_merge($sectionConfig, [
'siteSettings' => array_map(
fn(Site $site) => new Section_SiteSettings([
'siteId' => $site->id,
'hasUrls' => true,
'uriFormat' => "$handle/{slug}",
'template' => "$handle/_entry",
]),
Craft::$app->getSites()->getAllSites(true),
),
'entryTypes' => $entryTypes,
]));
if (!$entries->saveSection($section)) {
$section->validate();
Console::outputWarning("Section $handle could not be saved" . PHP_EOL . print_r($section->getErrors(), true));
}
}
}
/**
* Delete Sections based on the $sectionConfigs
*
* @param array $sectionConfigs
* @return void
* @throws Throwable
*/
protected function deleteSections(array $sectionConfigs): void
{
$entries = Craft::$app->getEntries();
foreach ($sectionConfigs as $sectionConfig) {
$handle = $sectionConfig['handle'];
$section = $entries->getSectionByHandle($handle);
if ($section) {
$entries->deleteSection($section);
}
}
}
/**
* Add the Fields to the EntryTypes for each $sectionConfigs
*
* @param array $sectionConfigs
* @param array $entryTypeConfigs
* @param array $fieldConfigs
* @return void
* @throws EntryTypeNotFoundException
* @throws InvalidConfigException
* @throws Throwable
*/
protected function addFieldsToEntryTypes(array $sectionConfigs, array $entryTypeConfigs, array $fieldConfigs): void
{
$entries = Craft::$app->getEntries();
foreach ($sectionConfigs as $sectionConfig) {
// Iterate through each Section
$sectionHandle = $sectionConfig['handle'];
$section = $entries->getSectionByHandle($sectionHandle);
if (!$section) {
Console::outputWarning("Section $sectionHandle doesn't exist");
return;
}
// Iterate through each EntryType
foreach ($entryTypeConfigs as $entryTypeConfig) {
$entryTypeHandle = $entryTypeConfig['handle'];
if (!in_array($entryTypeHandle, $sectionConfig['entryTypes'], true)) {
continue;
}
$entryType = $entries->getEntryTypeByHandle($entryTypeHandle);
if (!$entryType) {
Console::outputWarning("EntryType $entryTypeHandle doesn't exist");
return;
}
// Iterate through each Field
$elements = [];
foreach ($fieldConfigs as $fieldConfig) {
$fieldHandle = $fieldConfig['handle'];
if (!in_array($fieldHandle, $entryTypeConfig['customFields'], true)) {
continue;
}
$field = Craft::$app->getFields()->getFieldByHandle($fieldHandle);
if (!$field) {
Console::outputWarning("Field $fieldHandle doesn't exist");
continue;
}
$elements[] = Craft::createObject([
'class' => CustomField::class,
'fieldUid' => $field->uid,
'required' => false,
]);
}
// Just assign the fields to the first tab
$layout = $entryType->getFieldLayout();
$tabs = $layout->getTabs();
$tabs[0]->setElements(array_merge($tabs[0]->getElements(), $elements));
$layout->setTabs($tabs);
$entryType->setFieldLayout($layout);
$entries->saveEntryType($entryType);
}
}
}
/**
* Remove the Fields from the EntryTypes
*
* @return void
*/
protected function removeFieldsFromEntryTypes(): void
{
// Do nothing, these will be destroyed along with the EntryType
}
/**
* Create the Entry data
*
* @param array $fieldConfigs
* @param string $sectionHandle
* @param string $entryTypeHandle
* @param string $userName
* @param int $numEntries
* @return void
* @throws InvalidConfigException
* @throws Throwable
* @throws ElementNotFoundException
* @throws Exception
*/
protected function createEntryData(array $fieldConfigs, string $sectionHandle, string $entryTypeHandle, string $userName, int $numEntries): void
{
$faker = Factory::create();
$entries = Craft::$app->getEntries();
$section = $entries->getSectionByHandle($sectionHandle);
if (!$section) {
Console::outputWarning("Section $sectionHandle doesn't exist");
return;
}
$entryType = $entries->getEntryTypeByHandle($entryTypeHandle);
if (!$entryType) {
Console::outputWarning("EntryType $entryTypeHandle doesn't exist");
return;
}
$user = Craft::$app->users->getUserByUsernameOrEmail($userName);
if (!$user) {
Console::outputWarning("User $userName doesn't exist");
return;
}
// Do it as a bulk operation, see: https://github.com/craftcms/cms/pull/14032
$bulkKey = Craft::$app->elements->beginBulkOp();
for ($i = 0; $i < $numEntries; $i++) {
$name = $faker->unique()->name();
/* @var Entry $entry */
$entry = Craft::createObject([
'class' => Entry::class,
'sectionId' => $section->id,
'typeId' => $entryType->id,
'authorId' => $user->id,
'title' => $name,
]);
// Just the essentials for bulk import/creation
$entry->setScenario(Element::SCENARIO_ESSENTIALS);
foreach ($fieldConfigs as $fieldConfig) {
$handle = $fieldConfig['handle'];
$entry->setFieldValue($handle, $name);
}
if (Craft::$app->elements->saveElement($entry)) {
Console::output("#$i - Added entry " . Console::ansiFormat($name, [Console::FG_YELLOW]));
} else {
Console::outputWarning("#$i - Failed to add entry $name");
}
}
Craft::$app->elements->endBulkOp($bulkKey);
}
/**
* Delete all entries from $sectionHandle
*
* @return void
*/
protected function deleteEntryData(): void
{
// Do nothing, these will be destroyed along with the EntryType
}
}
Link Running the Migration
So now that we have gone through the content migration section by section, let’s run it and see what it does.
Run the migration with:
php craft migrate m240430_143914_seed_db_data --interactive=0
The --interactive=0 flag just causes it to skip the prompts that asks you if you really want to run the migration.
You’ll see it logging each entry that it creates, and then once it is done, if you visit your CP, you’ll see that it’s created the Field demoData, the EntryType demo, the Section demo, and it’s populated the demo Section with a whole lot of Entries:
Now let’s say you’re playing around with your migration, and you want to redo the migration based on some changes you’ve made to your migration code. You can redo the last migration you ran with this command:
php craft migrate/redo --interactive=0
This will cause it to run the safeDown() method to revert the migration, and then run the safeUp() method again to re-run the migration. This is especially useful when you’re working on developing your migration, and want to test it out, fix bugs, etc.
Finally, assuming your migration was a temporary one, and you want to revert the changes it made, you can revert the last migration you ran with:
php craft migrate/down --interactive=0
Link Migrating Away
While you may not have the need to seed your database with a bunch of random data as I did here (it was for performance testing) hopefully, this explainer has given you the basis for writing your own content migrations!
If you want to learn more about content migrations, check out the CraftQuest course Content Migrations in Craft.
Happy migrating, don’t forget to pack your sunscreen!