Craft CMS 3.1 introduces some big features, most noticably Project Config. Project Config is a configuration setting which, when enabled, turns a .yaml file into the single source of truth for a site’s schema.

Project config control panel

The implications of this are that you can make changes to a site’s schema in the control panel in your development environment. When you then deploy the project config file (config/project.yaml) to your staging or production environments, the schema will automatically be updated on those sites. Developers have been asking for this functionality for years, and it is finally here!

Site Schema #

Let’s begin by defining what we mean by a site’s schema. The site schema is what is saved in the project config and consists of the following:

  • Asset volumes and image transforms
  • Category groups
  • Email settings
  • Fields and field groups
  • Global set settings
  • Matrix block types
  • Plugin settings
  • Routes (defined in the control panel)
  • Sections and entry types
  • Sites and site groups
  • System settings
  • Tag groups
  • User groups and settings

Note that plugins can also store things in addition to their own settings (which are stored automatically) in the project config, but they must do some extra work to keep changes in sync (see below).

Enabling Project Config #

To enable project config, all we need to do is enable the useProjectConfigFile config setting for all environments in config/general.php.

return [
    '*' => [
        'useProjectConfigFile' => true,
    ],
];

Once enabled, the project config will be created and stored in config/project.yaml. Any time the site schema changes, the project config will automatically be updated. So as you define and change your site schema from within the control panel, those changes are synced to the project config file.

Any changes to the project config will be synced to the config column of the Info database table. This is where the project config is actually loaded from, whether you have useProjectConfigFile enabled or not. With the config setting enabled, Craft simply monitors project.yaml for changes and if it detects any then syncs them to the database.

Project config stored in the database

I suggest you open up the project config file and take a look inside, it’s really not that intimidating. Even with project config disabled, you can see what it would look like by opening one of the backup files that is automatically created and stored in storage/config-backups.

Project config YAML

Whenever the project config file is overwritten, either from a deployment or by manually overwriting it, Craft will detect the changes and sync them to the database. This means that with project config enabled, the project config file is the single source of truth. The implications of this are that site schema changes should only be made in a development environment in which the project.yaml file is version controlled.

Project Config Workflow #

To work with project config, you should only make schema changes through the control panel of one or more development environments. The development environment’s project config file should be the only one that is version controlled and that is deployed to the other environments.

When working in a team, each developer’s development environment should be using Git (or some other form of version control) to ensure that theproject.yaml file is updated correctly and that nobody’s changes are accidentally overwritten. If there is a merge conflict then this should be resolved before the file is deployed.

To ensure that the site’s schema cannot be changed on an environment which is not a development environment, set the allowAdminChanges config setting to false on all other environments.

return [
    '*' => [
        'useProjectConfigFile' => true,
        'allowAdminChanges' => false,
    ],
    'dev' => [
        'allowAdminChanges' => true,
    ], 
];

This will prevent the project.yaml file from being written to, as well as remove the Settings section and Plugin Store from the control panel.

Control panel with admin changes disabled

There are some additional caveats that you should be aware of. Read about them in the Project Config docs.

Plugin Compatibility #

Project config was a major feature in a minor release, and as such it did introduce some issues to plugins that were using undocumented features. Here is what plugin developers should do to ensure their plugins are compatible with project config.

Use Service APIs Whenever Possible #

If your plugin manipulates the site schema or any data that is stored in the project config then this should be done using service APIs. Manipulating this data in the database will likely result in your changes being overwritten the next time the project config is synced. This is because syncing happens in one direction only:

So instead of manipulating a field through the database:

/*-- DON'T DO THIS --*/

// Get the field record from the database
$fieldRecord = Field::find()
    ->where(['handle' => 'myFieldHandle'])
    ->one();
    
// Update the name property
$fieldRecord->name = 'New Field Name';

// Save the field record in the database
$fieldRecord->save();

Use the fields service to fetch and save the field:

/*-- DO THIS INSTEAD --*/

// Get the field using the fields service
$field = Craft::$app->fields->getFieldByHandle('myFieldHandle');

// Update the name property
$field->name = 'New Field Name';

// Save the field using the fields service
Craft::$app->fields->saveField($field);

Some methods were even added in Craft 3.1 to specifically allow you to access things stored in the project config:

// Get the routes defined in the project config
$routes = Craft::$app->routes->getProjectConfigRoutes();

If you find yourself in a situation where you need to update a value in the project config directly then do so using the ProjectConfig service:

// Construct the config path using the field's UID
$configPath = Fields::CONFIG_FIELDS_KEY.'.'.$field->uid;

// Add the name property value to the config value array
$configValue = [
    'name' => 'New Field Name',
];

// Set the value(s) using the project config service
Craft::$app->projectConfig->set($configPath, $configValue);

This will automatically trigger event listeners to sync the database.

Review Plugin Migrations #

If any of your plugin migrations update the site schema (including your plugin’s settings) in the database directly, then this behaviour should be changed to update the project config instead.

For plugin settings, you should be using the Plugins service or your own settings service to update the values.

// Get the plugin settings
$settings = MyPlugin::getInstance()->getSettings();

// Update the name property
$settings->name = 'newValue';

// Save the settings using the plugins service
Craft::$app->plugins->savePluginSettings($settings->toArray());

When making changes to project config from your plugin migrations, there is a risk of the migration being run more than once. To avoid this, you should always check your plugin’s schema version in project.yaml before making project config changes. You can do this by checking the plugin’s schema version. Pass true as the second parameter to the get() method to ensure that the schema version is fetched from the project.yaml file.

public function safeUp()
{
    // Get the plugin schema version from `project.yaml`
    $schemaVersion = Craft::$app->projectConfig->get(
        'plugins.<plugin-handle>.schemaVersion', 
        true
    );

    if (version_compare($schemaVersion, '1.2', '<')) {
        // Make the project config changes here
        ...
    }
}

You might at this stage be wondering why that second parameter is important. Well, as I mentioned earlier, the project config is loaded from the config column of the Info database table by default. Passing true to the get() method tells Craft to fetch the incoming value rather than the loaded value, which is helpful in the case of migrations because Craft runs new plugin migrations before syncing incoming config changes.

Read the Project Config Migrations docs for more details.

Store UIDs Instead of IDs in Plugin Settings #

As previously stated, plugin settings are now stored in the project config. If your plugin is storing references to sites, sections, fields, or anything else in the site schema within the plugin settings, you should update those references to use UIDs instead of IDs. Their IDs will potentially be different on each environment, whereas their UIDs (universally unique identifiers) will always stay the same across all environments.

If you are storing references to sites, sections, fields, or anything else in the site schema within database tables, then those can safely continue referencing IDs.

Adding Support for Project Config to Plugins #

Before adding support for project config to a plugin, it is important to consider whether your plugin will really benefit from it and whether it fits with the workflow that you intend for your end-users. If your plugin stores any configurable components that store settings outside of your main plugin settings which should only be editable by admins, then they may be good candidates for project config support.

Plugin data that is stored in project config should only be editable by admins in a development environment.

For example, the Commerce plugin has two distinct types of settings: Store Settings and System Settings. Store settings include store location, payment currencies, regions, tax and shipping rates, etc. These are things that a user with the necessary permissions should be able to edit. System settings include email settings, order fields, gateways, product types, etc. These are things that only an admin should be able to edit. So it is probably no surprise to hear which of these settings is stored in project config and which is not. The system settings are of course stored in project config, and with the allowAdminChanges config setting disabled, the entire section is inaccessible in the control panel.

Commerce control panel with admin changes disabled

So you should only add support for project config to things in your plugin that make up part of the site schema and that can be considered admin only. This is similar to how sites, sections and fields are only editable by admins.

There is an important paradigm shift in how you update your plugin’s data in project config. Before, when the the user changed something related to your plugin, the plugin would simply update the relevant data in the database. But now, those updates should happen in project config, which in turn triggers the plugin to update the database.

The project config paradigm shift

The implementation details of adding support for project config to plugins are rather complex and dependant on the type of data that is stored, and are unfortunately beyond the scope of this article.

Read the Supporting Project Config docs for more details and explore the Commerce plugin source code for real-world implementation of it.

This article borrows heavily from the Project Config documentation and aims to present it in an easy to follow format. For the single source of truth, please refer to the current version of the docs.