Skip to content

Instantly share code, notes, and snippets.

@khalwat
Last active May 15, 2024 17:06
Show Gist options
  • Save khalwat/119044915b1fcfac95369ff354868a79 to your computer and use it in GitHub Desktop.
Save khalwat/119044915b1fcfac95369ff354868a79 to your computer and use it in GitHub Desktop.
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
<?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
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment