Skip to content

Commit 34f78c1

Browse files
committed
schemaVersion and other migration info
1 parent 0aabac7 commit 34f78c1

File tree

2 files changed

+165
-32
lines changed

2 files changed

+165
-32
lines changed

docs/5.x/extend/migrations.md

Lines changed: 124 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Plugin migrations
1313
Content migrations
1414
: Migrations specific to your Craft project. These often contain steps that manipulate data based on handles or other identifiers that are only relevant internally.
1515

16+
[Modules](module-guide.md) are treated as part of your application, and should use content migrations.
17+
1618
## Creating Migrations
1719

1820
To create a new migration, use the `migrate/create` command:
@@ -28,48 +30,64 @@ php craft migrate/create my_migration_name
2830

2931
Enter `yes` at the prompt, and a new migration file will be created for you. You can find it at the file path output by the command; migration classes include a timestamp prefix with the format `mYYMMDD_HHMMSS`, like `m250923_000000`.
3032

31-
This file and class should _never_ be renamed after release! Doing so can cause it to run again, or out of order.
33+
This file and class should _never_ be renamed after release!
34+
Doing so can cause it to run again, or out of order.
35+
Similarly, the only time it is appropriate to modify an existing migration is when it produces errors for your users.
36+
Those changes should be published as part of a new release, and they should never result in a different schema.
3237

3338
::: tip
34-
If this is a plugin migration, increase your plugin’s [schema version](craft5:craft\base\PluginTrait::$schemaVersion), so Craft knows to check for new plugin migrations after an update.
39+
If this is a plugin migration, increase your plugin’s [schema version](#schema-version), so Craft knows to run new migrations after an update.
3540
:::
3641

3742
### What Goes Inside
3843

39-
Migration classes contain methods: [safeUp()](<yii2:yii\db\Migration::safeUp()>) and [safeDown()](<yii2:yii\db\Migration::safeDown()>). `safeUp()` is run when your migration is _applied_, and `safeDown()` is run when your migration is _reverted_.
44+
Migration classes must define two methods:
45+
46+
- [safeUp()](<yii2:yii\db\Migration::safeUp()>) — Run when the migration is _applied_.
47+
- [safeDown()](<yii2:yii\db\Migration::safeDown()>) — Run when the migration is _reverted_.
4048

4149
::: tip
4250
You can usually ignore the `safeDown()` method, as Craft doesn’t have a way to revert migrations from the control panel.
51+
During development and testing, however, you may find that it significantly easier to [roll back](#rolling-back) a migration than drop and re-import a database.
4352
:::
4453

45-
You have full access to [Craft’s API](https://docs.craftcms.com/api/v5/) from your `safeUp()` method, but plugin migrations should try to avoid calling the plugin’s own API here. As your plugin’s database schema changes over time, so will your APIs assumptions about the schema. If an old migration calls a service method that relies on database changes that haven’t been applied yet, it will result in a SQL error. So in general you should execute all SQL queries directly from your own migration class. It may feel like you’re duplicating code, but it will be more future-proof.
54+
You have full access to [Craft’s API](https://docs.craftcms.com/api/v5/) from your `safeUp()` method, but plugin migrations should try to avoid calling the plugin’s own API here.
55+
As your plugin’s database schema changes over time, so will your APIs assumptions about the schema.
56+
If a migration calls a service method that relies on database changes that haven’t been applied yet, it will result in a SQL error.
57+
In general, you should execute all SQL queries _directly from that migration class_.
58+
It may feel like you’re duplicating code, but it will be more future-proof.
59+
Read more about this in the [rollbacks and compatibility](#rollbacks-and-compatibility) section.
60+
61+
When you’ve finalized a migration, make sure its effects are reflected in the [install migration](#plugin-install-migrations), as well.
62+
When a plugin is installed
4663

4764
### Manipulating Database Data
4865

49-
Your migration class extends <craft5:craft\db\Migration>, which provides several methods for working with the database. It’s better to use these than their <craft5:craft\db\Command> counterparts, because the migration methods are both simpler to use, and they’ll output a status message to the terminal for you.
66+
Your migration class extends <craft5:craft\db\Migration>, which provides several methods for working with the database.
67+
These are often more convenient than their <craft5:craft\db\Command> counterparts, and they’ll output a status message to the terminal for you.
5068

5169
```php
52-
// Bad:
70+
// Traditional command:
5371
$this->db->createCommand()
5472
->insert('{{%mytablename}}', $rows)
5573
->execute();
5674

57-
// Good:
75+
// Migration shortcut:
5876
$this->insert('{{%mytablename}}', $rows);
5977
```
6078

61-
<craft5:craft\helpers\MigrationHelper> provides several helpful methods as well:
79+
<craft5:craft\helpers\MigrationHelper> provides several helpful methods, as well:
6280

63-
- [dropForeignKeyIfExists()](craft5:craft\helpers\MigrationHelper::dropForeignKeyIfExists()) removes a foreign key if it exists, without needing to know its exact name.
64-
- [dropIndexIfExists()](craft5:craft\helpers\MigrationHelper::dropIndexIfExists()) removes an index if it exists, without needing to know its exact name.
81+
- [dropForeignKeyIfExists()](craft5:craft\helpers\MigrationHelper::dropForeignKeyIfExists()) removes a foreign key if it exists, without needing to know its exact name (oftentimes a random string).
82+
- [dropIndexIfExists()](craft5:craft\helpers\MigrationHelper::dropIndexIfExists()) removes an index if it exists, without needing to know its exact name (oftentimes a random string).
6583
- [dropTable()](craft5:craft\helpers\MigrationHelper::dropTable()) drops a table, along with any foreign keys that reference it (some of which your plugin might not even be aware of).
6684

6785
::: warning
6886
The <yii2:yii\db\Migration::insert()>, [batchInsert()](<craft5:craft\db\Migration::batchInsert()>), and [update()](<yii2:yii\db\Migration::update()>) migration methods will automatically insert/update data in the `dateCreated`, `dateUpdated`, `uid` table columns in addition to whatever you specified in the `$columns` argument. If the table you’re working with does’t have those columns, make sure you pass `false` to the `$includeAuditColumns` argument so you don’t get a SQL error.
6987
:::
7088

7189
::: tip
72-
<craft5:craft\db\Migration> doesn’t have a method for _selecting_ data, so you will still need to go through Yii’s [Query Builder](https://www.yiiframework.com/doc/guide/2.0/en/db-query-builder) for that.
90+
<craft5:craft\db\Migration> doesn’t have a method for _selecting_ data, so you will still need to go through Yii’s [Query Builder](https://www.yiiframework.com/doc/guide/2.0/en/db-query-builder) for read-only queries.
7391

7492
```php
7593
use craft\db\Query;
@@ -78,48 +96,131 @@ $result = (new Query())
7896
// ...
7997
->all();
8098
```
81-
8299
:::
83100

84101
### Logging
85102

86-
If you want to log messages in your migration code, echo it out rather than calling [Craft::info()](<yii2:yii\BaseYii::info()>):
103+
If you want to log messages in your migration code, `echo` it rather than calling [Craft::info()](<yii2:yii\BaseYii::info()>):
87104

88105
```php
89106
echo " > some note\n";
90107
```
91108

92-
If the migration is being run from a console request, this will ensure the message is seen by whoever is executing the migration, as the message will be output into the terminal. If it’s a web request, Craft will capture it and log it to `storage/logs/` just as if you had used `Craft::info()`.
109+
When the migration is run from the console, `echo` outputs text to the terminal (`stdout`).
110+
For web requests, Craft captures the output and logs it to `storage/logs/`, as if you had used `Craft::info()`.
111+
As a consequence, use of the [console command output helpers](commands.md#output-helpers) may pollute output with ANSI control characters.
93112

94113
## Executing Migrations
95114

96-
You can have Craft apply your new migration from the terminal:
115+
You can apply your new migration from the terminal:
97116

98117
::: code
99-
100118
```bash Plugin Migration
101119
php craft migrate/up --plugin=my-plugin-handle
102120
```
103-
104121
```bash Content Migration
105122
php craft migrate/up
106123
```
107-
108124
:::
109125

110-
Or you can have Craft apply all new migrations across all migration tracks:
126+
To apply _all_ new migrations, across _all_ migration tracks, run `migrate/all`:
111127

112128
```bash
113129
php craft migrate/all
114130
```
115131

116-
Craft will also check for new plugin migrations on control panel requests, for any plugins that have a new [schema version](craft5:craft\base\PluginTrait::$schemaVersion), and content migrations can be applied from the control panel by going to Utilities → Migrations.
132+
Craft will also check for new plugin and content migrations on control panel requests.
133+
App migrations must be applied before logging in; plugin and content migrations can be run later, by visiting <Journey path="Utilities, Migrations" />.
117134

118-
## Plugin Install Migrations
135+
## Rollbacks and Compatibility
136+
137+
### Schema Version
138+
139+
Your primary plugin class should maintain a [`schemaVersion`](craft5:craft\base\PluginTrait::$schemaVersion) that reflects the last release in which a migration was introduced.
140+
When Craft notices a new schema version for a plugin, it will present control panel users with the post-upgrade “migrations” screen.
119141

120-
Plugins can have a special “Install” migration which handles the installation and uninstallation of the plugin. Install migrations live at `migrations/Install.php` alongside normal migrations. They should follow this template:
142+
Despite migrations being performed incrementally, they can result in incompatible schemas, from the currently-running code’s perspective.
143+
Keep in mind that your users may be upgrading from _any_ prior version, particularly when using your own plugin’s APIs in a migration.
144+
For example, using a custom [element type](element-types.md)’s [query class](element-types.md#element-query-class) in a migration can result in a selection that includes columns that haven’t been added to the table yet:
121145

122146
```php
147+
class ProductQuery extends ElementQuery
148+
{
149+
protected function beforePrepare(): bool
150+
{
151+
// JOIN our `products` table:
152+
$this->joinElementTable('products');
153+
154+
// Always SELECT the `price` and `currency` columns...
155+
$this->query->select([
156+
'products.price',
157+
'products.currency',
158+
]);
159+
160+
// ...and add this column, only if it exists:
161+
if (Craft::$app->getDb()->columnExists(MyTables::PRODUCTS, 'weight')) {
162+
$this->query->addSelect([
163+
'products.weight',
164+
'products.weightUnit'
165+
]);
166+
}
167+
168+
// For performance, you can also test against schema versions that you know will contain those columns:
169+
$pluginInfo = Craft::$app->getPlugins()->getStoredPluginInfo('myplugin');
170+
171+
if (version_compare($pluginInfo['schemaVersion'], '1.2.3', '>=')) {
172+
$this->query->addSelect([
173+
'products.width',
174+
'products.height',
175+
'products.depth',
176+
]);
177+
}
178+
179+
// ...
180+
}
181+
}
182+
```
183+
184+
::: warning
185+
The new `schemaVersion` is only recorded after _all_ of its pending migrations have run, so a test like the one above (using `version_compare()`) may not accurately describe the state of the database.
186+
When in doubt, explicitly check for the column’s existence.
187+
:::
188+
189+
Queries built with <craft5:craft\db\Query> are typically immune to this issue, because the selections are controlled by the current migration (rather than application code).
190+
191+
### Minimum Versions
192+
193+
As a last resort, you can create a “breakpoint” in the upgrade process by setting a [`minVersionRequired`](craft5:craft\base\PluginTrait::$minVersionRequired) from which users can update.
194+
This tends to be disruptive for developers, and means a routine upgrade must be handled across multiple deployments—even if they have applied your updates sequentially in a development environment, Craft won’t allow the jump between incompatible versions in secondary environments.
195+
196+
This “minimum version” also signals to Craft’s built-in updater what the latest compatible version is.
197+
As with expired licenses, developers _can_ still directly install a more recent version via Composer—but they are apt to be met with an error as soon as plugins are loaded:
198+
199+
> You need to be on at least `My Plugin` 1.2.3 before you can update to `My Plugin` 1.4.0.
200+
201+
### Rolling Back
202+
203+
Another way to look at the `schemaVersion` is the farthest back a developer can expect to be able to _downgrade_ your packag, before encountering schema compatibility issues.
204+
205+
You may be able to provide additional support by thoroughly implementing `safeDown()` in each of your migrations.
206+
Backtracking is handled similarly to normal upgrades; each migration’s `safeDown()` method is invoked in succession, and its record is deleted from the `migrations` table so it can be re-run.
207+
208+
```bash
209+
php craft migrate/down
210+
```
211+
212+
The `safeDown()` method must actually reverse changes from `safeUp()` for it to be undone (or redone) successfully.
213+
If a migration tries to create a table or column that already exists, it will likely result in an error.
214+
215+
## Plugin Install Migrations
216+
217+
Plugins can have a special “Install” migration which handles the installation and uninstallation of the plugin.
218+
_This is the only migration run during installation_, so it should establish your plugin’s complete database schema, in each release.
219+
Your plugin’s other, incremental migrations are _not_ run during installation.
220+
221+
The special install migration should live at `migrations/Install.php`, alongside normal migrations, and follow this template:
222+
223+
```php{6}
123224
<?php
124225
namespace mynamespace\migrations;
125226
@@ -145,7 +246,7 @@ You can give your plugin an install migration with the `migrate/create` command
145246
php craft migrate/create install --plugin=my-plugin-handle
146247
```
147248

148-
When a plugin has an Install migration, its `safeUp()` method will be called when the plugin is installed, and its `safeDown()` method will be called when the plugin is uninstalled (invoked by the plugin’s [install()](<craft5:craft\base\Plugin::install()>) and [uninstall()](<craft5:craft\base\Plugin::uninstall()>) methods).
249+
When a plugin has an install migration, its `safeUp()` method will be called when the plugin is installed, and its `safeDown()` method will be called when the plugin is uninstalled (invoked by the plugin’s [install()](<craft5:craft\base\Plugin::install()>) and [uninstall()](<craft5:craft\base\Plugin::uninstall()>) methods, respectively).
149250

150251
::: tip
151252
It is _not_ a plugin’s responsibility to manage its row in the `plugins` database table. Craft takes care of that for you.

docs/5.x/extend/plugin-editions.md

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,35 @@
11
# Plugin Editions
22

3-
The Plugin Store supports multi-edition plugins, which work similarly to Craft’s two editions (Solo and Pro).
3+
The Plugin Store supports multi-edition plugins, which work similarly to Craft’s editions (Solo, Team, and Pro).
44

55
- Plugins that support multiple editions are still comprised of a single Composer package.
66
- Plugins’ active edition is recorded in the [project config](../project-config.md).
77
- Plugins can implement feature toggles by checking their active edition.
8+
- Plugins may require a particular Craft edition.
89

910
::: warning
1011
Not every plugin can or should support editions. [Contact](https://craftcms.com/contact) Pixel & Tonic before you begin adding edition support to make sure it will be allowed for your plugin.
1112
:::
1213

1314
## Define the Editions
1415

15-
To add edition support to a plugin, begin by defining the available editions (in ascending order), by overriding <craft5:craft\base\Plugin::editions()>.
16+
To add edition support to a plugin, begin by defining the available editions (in ascending order), by overriding <craft5:craft\base\Plugin::editions()>.
1617

1718
```php
1819
class Plugin extends \craft\base\Plugin;
1920
{
21+
// The order of your constants isn’t important...
2022
const EDITION_LITE = 'lite';
2123
const EDITION_PRO = 'pro';
24+
const EDITION_EXTREME = 'extreme';
2225

2326
public static function editions(): array
2427
{
2528
return [
29+
// ...but the order you return them *is*!
2630
self::EDITION_LITE,
2731
self::EDITION_PRO,
32+
self::EDITION_EXTREME,
2833
];
2934
}
3035

@@ -37,26 +42,27 @@ class Plugin extends \craft\base\Plugin;
3742
Your feature toggles can call your plugin’s [is()](craft5:craft\base\Plugin::is()) method.
3843

3944
::: code
40-
4145
```php
4246
if (Plugin::getInstance()->is(Plugin::EDITION_PRO) {
4347
// Pro edition-only code goes here...
4448
}
45-
```
4649

50+
if (Plugin::getInstance()->is(Plugin::EDITION_LITE, '>')) {
51+
// Advanced functionality goes here (`pro` or `extreme`)
52+
}
53+
```
4754
```twig
4855
{% if plugin('plugin-handle').is('pro') %}
4956
{# Pro edition-only code goes here... #}
5057
{% endif %}
5158
```
52-
5359
:::
5460

5561
`is()` accepts two arguments, `$edition` and `$operator`.
5662

5763
`$edition` is the name of the edition you’re concerned with.
5864

59-
`$operator` is how you wish to compare that edition with the installed edition. By default it is set to `=`, which tests for version equality.
65+
`$operator` is how you wish to compare that edition with the installed edition. by default, the editions are compared for equality
6066

6167
The following operators are also supported:
6268

@@ -69,12 +75,38 @@ Operator | Tests if the active edition is ____ the given edition
6975
`==` or `eq` | …equal to… (same as default behavior)
7076
`!=`, `<>`, or `ne` | …not equal to…
7177

72-
::: tip
73-
Changing editions should always be a lossless operation; no plugin data should change as a result of the edition change. Editions can change back and forth at any time, and plugins should have no problem rolling with it.
78+
The active edition is considered “greater than” another if it appears _later_ in the declared [editions array](#define-the-editions), and “less than” another if it appears _earlier_.
79+
80+
::: warning
81+
Changing editions should _never_ result in data or configuration loss.
82+
Editions can change at any time, including due to accidents, like an incorrectly-merged project config change.
7483
:::
7584

85+
Edition comparisons can be performed once your plugin’s `init()` method is called.
86+
Consider binding event listeners only when they are relevant to the installed edition.
87+
88+
Individual [controllers](controllers.md) cannot be conditionally registered (without using distinct controller namespaces), but you _can_ check for editions in each action, or a controller’s `beforeAction()` method:
89+
90+
```php
91+
public function beforeAction($action): bool
92+
{
93+
// This controller is only accessible to Pro + Extreme installations:
94+
if (Plugin::getInstance()->is(Plugin::EDITION_PRO, '<')) {
95+
return false;
96+
}
97+
98+
return parent::beforeAction($action);
99+
}
100+
```
101+
102+
## Require a Craft Edition
103+
104+
The [`minCraftEdition`](craft5:craft\base\Plugin::$minCraftEdition) property of your plugin class can be set to any of the <craft5:craft\enums\CmsEdition>
105+
106+
For example, a plugins that provides some additional functionality to [user groups](../system/user-management.md#user-groups) (a feature only available in <Badge type="edition" text="Pro" vertical="middle" />) should have a `minCraftEdition` of `CmsEdition::Pro`.
107+
76108
## Testing
77109

78110
You can toggle the active edition by changing the `plugins.<plugin-handle>.edition` property in `config/project/project.yaml`.
79111

80-
After changing the value to a valid edition handle (one returned by your plugin’s `editions()` method), Craft will prompt you to sync your project config YAML changes into the loaded project config. Once that’s done, your plugin’s active edition will be set to the new edition, and feature toggles should start behaving accordingly.
112+
After changing the value to a valid edition handle (one returned by your plugin’s `editions()` method), Craft will prompt you to sync your project config YAML changes into the loaded project config. Once that’s done, your plugin’s active edition will be set to the new edition, and [feature toggles](#add-feature-toggles) should start behaving accordingly.

0 commit comments

Comments
 (0)