Understanding Project Config in Craft CMS

20 March 2019

Craft CMS 3.1 intro­duces some big fea­tures, most not­ic­ably Pro­ject Con­fig. Pro­ject Con­fig is a con­fig­ur­a­tion set­ting which, when enabled, turns a .yaml file into the single source of truth for a site’s schema.

Project config control panel

The implic­a­tions of this are that you can make changes to a site’s schema in the con­trol pan­el in your devel­op­ment envir­on­ment. When you then deploy the pro­ject con­fig file (config/project.yaml) to your sta­ging or pro­duc­tion envir­on­ments, the schema will auto­mat­ic­ally be updated on those sites. Developers have been ask­ing for this func­tion­al­ity for years, and it is finally here!

Site Schema #

Let’s begin by defin­ing what we mean by a site’s schema. The site schema is what is saved in the pro­ject con­fig and con­sists of the following:

  • Asset volumes and image transforms
  • Cat­egory groups
  • Email set­tings
  • Fields and field groups
  • Glob­al set settings
  • Mat­rix block types
  • Plu­gin settings
  • Routes (defined in the con­trol panel)
  • Sec­tions and entry types
  • Sites and site groups
  • Sys­tem settings
  • Tag groups
  • User groups and settings

Note that plu­gins can also store things in addi­tion to their own set­tings (which are stored auto­mat­ic­ally) in the pro­ject con­fig, but they must do some extra work to keep changes in sync (see below).

Enabling Pro­ject Con­fig #

To enable pro­ject con­fig, all we need to do is enable the useProjectConfigFile con­fig set­ting for all envir­on­ments in config/general.php.

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

Once enabled, the pro­ject con­fig will be cre­ated and stored in config/project.yaml. Any time the site schema changes, the pro­ject con­fig will auto­mat­ic­ally be updated. So as you define and change your site schema from with­in the con­trol pan­el, those changes are synced to the pro­ject con­fig file.

Any changes to the pro­ject con­fig will be synced to the con­fig column of the Info data­base table. This is where the pro­ject con­fig is actu­ally loaded from, wheth­er you have useProjectConfigFile enabled or not. With the con­fig set­ting enabled, Craft simply mon­it­ors project.yaml for changes and if it detects any then syncs them to the database.

Project config stored in the database

I sug­gest you open up the pro­ject con­fig file and take a look inside, it’s really not that intim­id­at­ing. Even with pro­ject con­fig dis­abled, you can see what it would look like by open­ing one of the backup files that is auto­mat­ic­ally cre­ated and stored in storage/config-backups.

Project config YAML

Whenev­er the pro­ject con­fig file is over­writ­ten, either from a deploy­ment or by manu­ally over­writ­ing it, Craft will detect the changes and sync them to the data­base. This means that with pro­ject con­fig enabled, the pro­ject con­fig file is the single source of truth. The implic­a­tions of this are that site schema changes should only be made in a devel­op­ment envir­on­ment in which the project.yaml file is ver­sion controlled.

Pro­ject Con­fig Work­flow #

To work with pro­ject con­fig, you should only make schema changes through the con­trol pan­el of one or more devel­op­ment envir­on­ments. The devel­op­ment environment’s pro­ject con­fig file should be the only one that is ver­sion con­trolled and that is deployed to the oth­er environments.

When work­ing in a team, each developer’s devel­op­ment envir­on­ment should be using Git (or some oth­er form of ver­sion con­trol) to ensure that theproject.yaml file is updated cor­rectly and that nobody’s changes are acci­dent­ally over­writ­ten. If there is a merge con­flict then this should be resolved before the file is deployed.

To ensure that the site’s schema can­not be changed on an envir­on­ment which is not a devel­op­ment envir­on­ment, set the allowAd­min­Changes con­fig set­ting to false on all oth­er environments.

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

This will pre­vent the project.yaml file from being writ­ten to, as well as remove the Set­tings sec­tion and Plu­gin Store from the con­trol panel.

Control panel with admin changes disabled

There are some addi­tion­al caveats that you should be aware of. Read about them in the Pro­ject Con­fig docs.

Plu­gin Com­pat­ib­il­ity #

Pro­ject con­fig was a major fea­ture in a minor release, and as such it did intro­duce some issues to plu­gins that were using undoc­u­mented fea­tures. Here is what plu­gin developers should do to ensure their plu­gins are com­pat­ible with pro­ject config.

Use Ser­vice APIs Whenev­er Pos­sible #

If your plu­gin manip­u­lates the site schema or any data that is stored in the pro­ject con­fig then this should be done using ser­vice APIs. Manip­u­lat­ing this data in the data­base will likely res­ult in your changes being over­writ­ten the next time the pro­ject con­fig is synced. This is because syncing hap­pens in one dir­ec­tion only:

So instead of manip­u­lat­ing 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 ser­vice 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 meth­ods were even added in Craft 3.1 to spe­cific­ally allow you to access things stored in the pro­ject config:

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

If you find your­self in a situ­ation where you need to update a value in the pro­ject con­fig dir­ectly then do so using the Pro­ject­Con­fig 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 auto­mat­ic­ally trig­ger event listen­ers to sync the database.

Review Plu­gin Migra­tions #

If any of your plu­gin migra­tions update the site schema (includ­ing your plugin’s set­tings) in the data­base dir­ectly, then this beha­viour should be changed to update the pro­ject con­fig instead.

For plu­gin set­tings, you should be using the Plu­gins ser­vice or your own set­tings ser­vice 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 mak­ing changes to pro­ject con­fig from your plu­gin migra­tions, there is a risk of the migra­tion being run more than once. To avoid this, you should always check your plugin’s schema ver­sion in project.yaml before mak­ing pro­ject con­fig changes. You can do this by check­ing the plugin’s schema ver­sion. Pass true as the second para­met­er to the get() meth­od to ensure that the schema ver­sion 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 won­der­ing why that second para­met­er is import­ant. Well, as I men­tioned earli­er, the pro­ject con­fig is loaded from the con­fig column of the Info data­base table by default. Passing true to the get() meth­od tells Craft to fetch the incom­ing value rather than the loaded value, which is help­ful in the case of migra­tions because Craft runs new plu­gin migra­tions before syncing incom­ing con­fig changes.

Read the Pro­ject Con­fig Migra­tions docs for more details.

Store UIDs Instead of IDs in Plu­gin Set­tings #

As pre­vi­ously stated, plu­gin set­tings are now stored in the pro­ject con­fig. If your plu­gin is stor­ing ref­er­ences to sites, sec­tions, fields, or any­thing else in the site schema with­in the plu­gin set­tings, you should update those ref­er­ences to use UIDs instead of IDs. Their IDs will poten­tially be dif­fer­ent on each envir­on­ment, where­as their UIDs (uni­ver­sally unique iden­ti­fi­ers) will always stay the same across all environments.

If you are stor­ing ref­er­ences to sites, sec­tions, fields, or any­thing else in the site schema with­in data­base tables, then those can safely con­tin­ue ref­er­en­cing IDs.

Adding Sup­port for Pro­ject Con­fig to Plu­gins #

Before adding sup­port for pro­ject con­fig to a plu­gin, it is import­ant to con­sider wheth­er your plu­gin will really bene­fit from it and wheth­er it fits with the work­flow that you intend for your end-users. If your plu­gin stores any con­fig­ur­able com­pon­ents that store set­tings out­side of your main plu­gin set­tings which should only be edit­able by admins, then they may be good can­did­ates for pro­ject con­fig support.

Plu­gin data that is stored in pro­ject con­fig should only be edit­able by admins in a devel­op­ment environment.

For example, the Com­merce plu­gin has two dis­tinct types of set­tings: Store Set­tings and Sys­tem Set­tings. Store set­tings include store loc­a­tion, pay­ment cur­ren­cies, regions, tax and ship­ping rates, etc. These are things that a user with the neces­sary per­mis­sions should be able to edit. Sys­tem set­tings include email set­tings, order fields, gate­ways, product types, etc. These are things that only an admin should be able to edit. So it is prob­ably no sur­prise to hear which of these set­tings is stored in pro­ject con­fig and which is not. The sys­tem set­tings are of course stored in pro­ject con­fig, and with the allowAd­min­Changes con­fig set­ting dis­abled, the entire sec­tion is inac­cess­ible in the con­trol panel.

Commerce control panel with admin changes disabled

So you should only add sup­port for pro­ject con­fig to things in your plu­gin that make up part of the site schema and that can be con­sidered admin only. This is sim­il­ar to how sites, sec­tions and fields are only edit­able by admins.

There is an import­ant paradigm shift in how you update your plugin’s data in pro­ject con­fig. Before, when the the user changed some­thing related to your plu­gin, the plu­gin would simply update the rel­ev­ant data in the data­base. But now, those updates should hap­pen in pro­ject con­fig, which in turn trig­gers the plu­gin to update the database.

The project config paradigm shift

The imple­ment­a­tion details of adding sup­port for pro­ject con­fig to plu­gins are rather com­plex and depend­ant on the type of data that is stored, and are unfor­tu­nately bey­ond the scope of this article.

Read the Sup­port­ing Pro­ject Con­fig docs for more details and explore the Com­merce plu­gin source code for real-world imple­ment­a­tion of it.

This art­icle bor­rows heav­ily from the Pro­ject Con­fig doc­u­ment­a­tion and aims to present it in an easy to fol­low format. For the single source of truth, please refer to the cur­rent ver­sion of the docs.