How to Write Migrations for Builder
Migrating Tables/Columns
To start, we must run this command inside the migrator container.
php migrator make:migration --path=migrations/database/builder/{version} {migration_name}
with proper version and migration name. To get the correct version, pull the latest master, check the latest version, and create a new one with greater value.
Example: latest version is 1.0.74, we must create 1.0.75. Migration name examples: if you change a table or column, the good name is create_table_name_table
or update_table_name_column_name
, or if the changes are related to layouts, just describe it in a few words, for instance, update_checkout_page_form_submit_buttons
or fix_shopping_cart_duplicate_buttons
.
The migration body always contains the up
method; we don't need the down
method, as migrations always go in one direction.
Note: Be extremely careful, as we can't roll back it if something is wrong with migration; always test multiple times with multiple possible values and cases.
Everyone is responsible for his written migration and must be very careful, as this is the crucial part, and if something is wrong with migration, the entire application can crash.
Let's start with easy migration, which is adding/updating columns, or adding tables.
For example, we are requested to add a new table in the builder, to start, we create a migration file with the command mentioned above and then write actual code.
Example:
public function up(): void
{
if (!Schema::hasTable('redirections')) {
Schema::create('redirections', function (Blueprint $table) {
$table->id();
$table->string('from', 255)->unique();
$table->string('to', 255)->index();
$table->smallInteger('type');
$table->smallInteger('status')->index();
$table->timestamps();
});
}
}
Column update example:
public function up(): void
{
if (Schema::getColumnType('legal\_docs', 'description') === Types::TEXT) {
Schema::table('legal\_docs', function (Blueprint $table) {
$table->longText('description')->change();
});
}
if (Schema::getColumnType('legal\_doc\_translations', 'description') === Types::TEXT) {
Schema::table('legal\_doc\_translations', function (Blueprint $table) {
$table->longText('description')->change();
});
}
}
Another case is when the entire structure of the table is going to be changed. First, we must get the old data, update the table structure, and then insert data to not lose any.
Example:
Refer to this file 2023_10_16_145339_update_scripts_and_scripts_translations_table.php
located in 1.0.72
.
Layout Migration
Moving forward, let's focus on layout migration. We have draft_layouts
and layouts(published)
, and we have contents for both in draft_layout_contents
and layout_contents
tables which contain the translatable part of widgets with hashes. Each layout widget has params
and may or may not contain props
which are translatable parts (content).
We can start with an easy change; for example, some parameters were changed in the layout JSON file; first of all, we must get all layout files using this trait's method, trait: InteractsWithWidgets
method getProjectAllLayouts
. It has one argument, which is project id, $projectId = config('currentProjectId');
.
Then we start to loop over all paths and check if the content is not empty.
foreach ($projectLayoutPaths as $path) {
$content = Storage::get($path);
$schema = json\_decode($content, true);
if (empty($schema)) {
continue;
}
}
Let's suppose that we are requested to change the contactDetails
widgets params discriminator
to email. We must create a new method that will recursively iterate over all children; let's name it updateContactDetailsParams
.
protected function updateContactDetailsParams(array &$schema): void
{
foreach ($decoded as &$item) {
if($item\['type'\] === 'contactDetails') {
$item\['params'\]\['discriminator'\] = 'email';
}
if (isset($item\['children'\])) {
$this->updateContactDetailsParams($item\['children'\]);
}
}
}
Then call the updateContactDetailsParams
method here:
foreach ($projectLayoutPaths as $path) {
$content = Storage::get($path);
$schema = json\_decode($content, true);
if (empty($schema)) {
continue;
}
$this->updateContactDetailsParams($schema);
Storage::put($path, json\_encode($schema));
}
Storage::put($path, json_encode($schema));
Will persist changes into the given path. We can do optimizations, like adding a modified flag, and if modified, then persist to file.
Let's consider a scenario when a new child is added to an existing one. This task involves some complexity. The important part is having both content and schema files already defined in the directory.
$newChildPaths = \[
'widgetSchema' => storage\_path('files/product\_list/new\_last\_child\_label\_schema.json'),
'widgetContents' => storage\_path('files/product\_list/new\_last\_child\_label\_contents.json')
\];
Here we have new methods trait: InteractsWithPages
method: retrievePaths
method2: getLayoutSchemas
. The first method gets the draft and published path from the given path, and the second method gets the draft and published layout schemas.
The next method is trait: InteractsWithWidgets
method: syncLayoutsWidgets
. In this case, we want to add a child to the price
widget. First, we must sync
the draft and published layouts. What does this mean? There can be cases when a draft version is completely different from the published, so this sync checks if it is not different and then adds new schema and content with the same hashes for the draft and the published layouts. If there are differences, it creates new hashes for draft and published and persists content.
foreach ($this->getProjectLayouts($projectId) as $path) {
$paths = $this->retrievePaths($path);
$schemas = $this->getLayoutSchemas($paths);
if (empty($schemas)) {
continue;
}
$sortedWidgets = $this->syncLayoutsWidgets(
'price',
$schemas\['draft'\],
$schemas\['published'\],
$newChildPaths,
$defaultLanguageId
);
}
The details can be found in 2023_08_03_051044_update_price_widget_add_new_child.php
under 1.0.51
folder.
There are lots of cases like inserting a new widget in a concrete position (we use array_splice
), changing some other params, or adding a new one, etc. All these examples can be found in the migrations.
Another helpful method is the trait: InteractsWithWidgets
method: collectWidgetReferences
which collects all widgets from the layout by their type, and then you can do modifications to that list.
Another example is when you need to migrate a specific widget that is appearing only on a specific page. To migrate, let's observe an example.
Here is another method trait: InteractsWithWidgets
method:getPageLayoutPaths
which gets only layouts of the current given page.
Example:
public function up(): void
{
$page = DB::table('pages')->where('alias', 'my-account')->whereNull('parent\_id');
if (!$page->exists()) {
return;
}
$projectId = config('currentProjectId');
$paths = $this->getPageLayoutPaths($page, $projectId);
$schemas = $this->getLayoutSchemas($paths);
if (empty($schemas)) {
return;
}
// Logic goes here
}
UI Elements Migration
For UI elements migration, we must get data from DB, ui_element_settings
table content
column. The data inside the content contains params
, children,
and other details. To migrate, we need to first get content, make the changes, and then update DB.
Example:
public function up() : void
{
$uiElementSettings = DB::table('ui\_element\_settings')->get(\['id', 'content'\]);
foreach ($uiElementSettings as $setting) {
$settingContent = $setting->content;
if (!$settingContent) {
continue;
}
$content = json\_decode($settingContent, true);
$this->convert($content); // The main logic
DB::table('ui\_element\_settings')
->where('id', $setting->id)
->update(\['content' => json\_encode($content)\]);
}
}
VariantsStyles Migration
Every params of widget contains variantsStyles
and it may or may not contain data. If we are requested to change some styles in it, then after migration, we must do CSS generation. We must generate new CSS only if something changes in variantsStyles
or the UiElements content, or it can be something that is related to CSS; this must be checked by the developer. CSS generation is just an HTTP call to the builder API that generates all CSS.
To do so, we have a method trait: InteractsWithPages
method: generateLayoutCss
.
Example:
To not write a big example here, the details can be found in this migration 2023_08_11_092907_remove_is_microelement_from_add_to_cart_buttons
located under 1.0.62
version.
The important part is that at the end, we are calling $this->generateLayoutCss();
Test Migrations
To ensure your migration is working properly, you must run this command inside the migrator container php migrator go:migrate:project --id=1 --v=1.0.70 --name=builder.ucraft.dev
. --id
represents your proejct id, --v
represents to what version you want upgrade, --name
is you project name. After running this command, you must see something like this:
Then you can open VE to check if your changes were applied, or open DB and/or layouts folder and check manually that everything is correct (table schema, layout JSON, etc.). If something is not correct, you can do CTRL + Z
in the layout file to bring back the old file, then re-run migration. If something is wrong with DB, you can fresh:seed
and then try again.
Note: Write migrations in a way that after running multiple times same migration will not break data. To ensure this, you can delete the migration record from the migrations
table from your project and re-run it again.