Last active
May 16, 2024 14:16
-
-
Save khalwat/40207181865d7bdb79a71dd170ec4f15 to your computer and use it in GitHub Desktop.
Craft CMS content migration that creates & deletes Sections, Field Groups, and Fields, and adds them to the Section, and then seeds the newly created Section with faked data. Also has a `safeDown()` method to revert what the migration adds
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
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\models\EntryType; | |
use craft\models\FieldGroup; | |
use craft\models\Section; | |
use craft\models\Section_SiteSettings; | |
use yii\base\Exception; | |
use yii\base\InvalidConfigException; | |
use yii\db\Schema; | |
/** | |
* m231102_025445_seed_db_data migration. | |
*/ | |
class m231102_025445_seed_db_data extends Migration | |
{ | |
private const SECTION_CONFIGS = [ | |
[ | |
'class' => Section::class, | |
'name' => 'Demo', | |
'handle' => 'demo', | |
'type' => Section::TYPE_CHANNEL, | |
], | |
]; | |
private const FIELD_GROUP_CONFIGS = [ | |
[ | |
'class' => FieldGroup::class, | |
'name' => 'Demo' | |
], | |
]; | |
private const FIELD_CONFIGS = [ | |
[ | |
'class' => PlainText::class, | |
'name' => 'Demo Data', | |
'handle' => 'demoData', | |
'translationMethod' => Field::TRANSLATION_METHOD_NONE, | |
'multiline' => 0, | |
'columnType' => Schema::TYPE_STRING, | |
], | |
]; | |
private const ENTRY_TYPE_HANDLE = 'default'; | |
private const USER_NAME = 'admin'; | |
private const NUM_ENTRIES = 100000; | |
/** | |
* @inheritdoc | |
*/ | |
public function safeUp(): bool | |
{ | |
try { | |
$this->createSections(self::SECTION_CONFIGS); | |
$this->createFieldGroups(self::FIELD_GROUP_CONFIGS); | |
$this->createFields(self::FIELD_CONFIGS, self::FIELD_GROUP_CONFIGS[0]['name']); | |
$this->addFieldsToSection(self::FIELD_CONFIGS, self::SECTION_CONFIGS[0]['handle'], self::ENTRY_TYPE_HANDLE); | |
$this->createEntryData(self::FIELD_CONFIGS, self::SECTION_CONFIGS[0]['handle'], self::ENTRY_TYPE_HANDLE, self::USER_NAME, self::NUM_ENTRIES); | |
} catch (\Throwable $e) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* @inheritdoc | |
*/ | |
public function safeDown(): bool | |
{ | |
try { | |
$this->deleteEntryData(self::SECTION_CONFIGS[0]['handle']); | |
$this->removeFieldsFromSection(self::FIELD_CONFIGS, self::SECTION_CONFIGS[0]['handle'], self::ENTRY_TYPE_HANDLE); | |
$this->deleteFields(self::FIELD_CONFIGS); | |
$this->deleteFieldGroups(self::FIELD_GROUP_CONFIGS); | |
$this->deleteSections(self::SECTION_CONFIGS); | |
} catch (\Throwable $e) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Create Sections based on the $sectionConfigs | |
* | |
* @param array $sectionConfigs | |
* @return void | |
* @throws \Throwable | |
* @throws SectionNotFoundException | |
* @throws SiteNotFoundException | |
*/ | |
protected function createSections(array $sectionConfigs): void | |
{ | |
foreach ($sectionConfigs as $sectionConfig) { | |
$handle = $sectionConfig['handle']; | |
if (Craft::$app->sections->getSectionByHandle($handle)) { | |
echo "Section {$handle} already exists" . PHP_EOL; | |
continue; | |
} | |
$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), | |
), | |
])); | |
if (!Craft::$app->sections->saveSection($section)) { | |
echo "Section {$handle} could not be saved" . PHP_EOL; | |
} | |
} | |
} | |
/** | |
* Delete Sections based on the $sectionConfigs | |
* | |
* @param array $sectionConfigs | |
* @return void | |
* @throws \Throwable | |
*/ | |
protected function deleteSections(array $sectionConfigs): void | |
{ | |
foreach ($sectionConfigs as $sectionConfig) { | |
$handle = $sectionConfig['handle']; | |
$section = Craft::$app->sections->getSectionByHandle($handle); | |
if ($section) { | |
Craft::$app->sections->deleteSection($section); | |
} | |
} | |
} | |
/** | |
* Create FieldGroups based on the $fieldGroupConfigs | |
* | |
* @param array $fieldGroupConfigs | |
* @return void | |
* @throws InvalidConfigException | |
*/ | |
protected function createFieldGroups(array $fieldGroupConfigs): void | |
{ | |
foreach ($fieldGroupConfigs as $fieldGroupConfig) { | |
$name = $fieldGroupConfig['name']; | |
if ($this->getFieldGroupByName($name)) { | |
echo "Group {$name} already exists" . PHP_EOL; | |
continue; | |
} | |
$group = Craft::createObject($fieldGroupConfig); | |
Craft::$app->getFields()->saveGroup($group); | |
} | |
} | |
/** | |
* Delete FieldGroups based on the $fieldGroupConfigs | |
* | |
* @param array $fieldGroupConfigs | |
* @return void | |
*/ | |
protected function deleteFieldGroups(array $fieldGroupConfigs): void | |
{ | |
foreach ($fieldGroupConfigs as $fieldGroupConfig) { | |
$name = $fieldGroupConfig['name']; | |
$group = $this->getFieldGroupByName($name); | |
if ($group) { | |
Craft::$app->getFields()->deleteGroup($group); | |
} | |
} | |
} | |
/** | |
* Create Fields based on $fieldConfigs | |
* | |
* @param array $fieldConfigs | |
* @param string $groupName | |
* @return void | |
* @throws InvalidConfigException | |
* @throws \Throwable | |
*/ | |
protected function createFields(array $fieldConfigs, string $groupName): void | |
{ | |
$group = $this->getFieldGroupByName($groupName); | |
if (!$group) { | |
echo "FieldGroup {$groupName} doesn't exist" . PHP_EOL; | |
return; | |
} | |
$fieldsService = Craft::$app->getFields(); | |
foreach ($fieldConfigs as $fieldConfig) { | |
$handle = $fieldConfig['handle']; | |
if ($fieldsService->getFieldByHandle($handle)) { | |
echo "Field {$handle} already exists" . PHP_EOL; | |
continue; | |
} | |
$field = Craft::createObject(array_merge($fieldConfig, [ | |
'groupId' => $group->id, | |
])); | |
$fieldsService->saveField($field); | |
} | |
} | |
/** | |
* Delete Fields based on $fieldConfigs | |
* | |
* @param array $fieldConfigs | |
* @return void | |
* @throws \Throwable | |
*/ | |
protected function deleteFields(array $fieldConfigs): void | |
{ | |
$fieldsService = Craft::$app->getFields(); | |
foreach ($fieldConfigs as $fieldConfig) { | |
$handle = $fieldConfig['handle']; | |
$field = $fieldsService->getFieldByHandle($handle); | |
if ($field) { | |
Craft::$app->getFields()->deleteField($field); | |
} | |
} | |
} | |
/** | |
* Add the Fields to the $sectionHandle | |
* | |
* @param array $fieldConfigs | |
* @param string $sectionHandle | |
* @param string $entryTypeHandle | |
* @return void | |
* @throws \Throwable | |
* @throws EntryTypeNotFoundException | |
*/ | |
protected function addFieldsToSection(array $fieldConfigs, string $sectionHandle, string $entryTypeHandle): void | |
{ | |
$section = Craft::$app->sections->getSectionByHandle($sectionHandle); | |
if (!$section) { | |
echo "Section {$sectionHandle} doesn't exist" . PHP_EOL; | |
return; | |
} | |
$entryType = $this->getEntryTypeByHandle($section, $entryTypeHandle); | |
if (!$entryType) { | |
echo "EntryType {$entryTypeHandle} doesn't exist" . PHP_EOL; | |
return; | |
} | |
// Get all of our fields | |
$elements = []; | |
foreach ($fieldConfigs as $fieldConfig) { | |
$handle = $fieldConfig['handle']; | |
$field = Craft::$app->getFields()->getFieldByHandle($handle); | |
if (!$field) { | |
echo "Field {$handle} doesn't exist" . PHP_EOL; | |
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); | |
Craft::$app->sections->saveEntryType($entryType); | |
} | |
/** | |
* Remove the Fields from the Section | |
* @param array $fieldConfigs | |
* @param string $sectionHandle | |
* @param string $entryTypeHandle | |
* @return void | |
*/ | |
protected function removeFieldsFromSection(array $fieldConfigs, string $sectionHandle, string $entryTypeHandle): void | |
{ | |
// Do nothing, these will be destroyed along with the Section | |
} | |
/** | |
* 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) | |
{ | |
$faker = \Faker\Factory::create(); | |
$section = Craft::$app->getSections()->getSectionByHandle($sectionHandle); | |
if (!$section) { | |
echo "Section {$sectionHandle} doesn't exist" . PHP_EOL; | |
return; | |
} | |
$entryType = $this->getEntryTypeByHandle($section, $entryTypeHandle); | |
if (!$entryType) { | |
echo "EntryType {$entryTypeHandle} doesn't exist" . PHP_EOL; | |
return; | |
} | |
$user = Craft::$app->users->getUserByUsernameOrEmail($userName); | |
if (!$user) { | |
echo "User {$userName} doesn't exist" . PHP_EOL; | |
return; | |
} | |
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)) { | |
echo "#{$i} - Added entry {$name}" . PHP_EOL; | |
} else { | |
echo "#{$i} - Failed to add entry {$name}" . PHP_EOL; | |
} | |
} | |
} | |
/** | |
* Delete all entries from $sectionHandle | |
* | |
* @param string $sectionHandle | |
* @return void | |
* @throws \Throwable | |
*/ | |
protected function deleteEntryData(string $sectionHandle): void | |
{ | |
// There are more efficient ways to do this, but whatever | |
$section = Craft::$app->getSections()->getSectionByHandle($sectionHandle); | |
if (!$section) { | |
echo "Section {$sectionHandle} doesn't exist" . PHP_EOL; | |
return; | |
} | |
$i = 0; | |
foreach (Entry::find()->sectionId($section->id)->ids() as $entryId) { | |
if (Craft::$app->elements->deleteElementById($entryId, null, null, true)) { | |
echo "#{$i} - Deleted entry id {$entryId}" . PHP_EOL; | |
} else { | |
echo "#{$i} - Failed to delete entry id {$entryId}" . PHP_EOL; | |
} | |
$i++; | |
} | |
} | |
/** | |
* Get an EntryType by $entryTypeHandle | |
* | |
* @param Section $section | |
* @param string $entryTypeHandle | |
* @return EntryType|null | |
*/ | |
private function getEntryTypeByHandle(Section $section, string $entryTypeHandle): ?EntryType | |
{ | |
$entryTypes = $section->getEntryTypes(); | |
foreach ($entryTypes as $entryType) { | |
if ($entryType->handle === $entryTypeHandle) { | |
return $entryType; | |
} | |
} | |
return null; | |
} | |
/** | |
* Get a field group by $name | |
* | |
* @param string $name | |
* @return ?FieldGroup | |
*/ | |
private function getFieldGroupByName(string $name): ?FieldGroup | |
{ | |
$groups = Craft::$app->getFields()->getAllGroups(); | |
foreach ($groups as $group) { | |
if ($group->name === $name) { | |
return $group; | |
} | |
} | |
return null; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment