Andrew Welch
Published , updated · 5 min read · RSS Feed
Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.
Fluent Multi-Environment Config for Craft CMS 4
Using the new fluent model config settings in Craft CMS 4.2 allows you to create better config files, with less work and fewer errors
Craft CMS stores multi-environment config files in the config/ directory, which allow you to control the behavior of Craft CMS and plugins.
A recent PR merged into Craft CMS 4.2 provides an optional fluent interface to set your config settings that is easy to use, and offers several advantages over the traditional config file syntax you’re used to.
First, let’s look at the problem with traditional config file syntax.
Link Traditional Config File Syntax
A traditional config file for Craft CMS might look like this:
use craft\helpers\App;
$isDev = App::env('CRAFT_ENVIRONMENT') === 'dev';
$isProd = App::env('CRAFT_ENVIRONMENT') === 'production';
return [
'defaultWeekStartDay' => 1,
'omitScriptNameInUrls' => true,
'devMode' => $isDev,
'allowAdminChanges' => $isDev,
'disallowRobots' => !$isProd,
];
The problem with this approach is that there are over 150 General Config settings at the time of this writing, with more being added all the time:
- It’s difficult to remember the exact name of each config setting, so you often end up having to look it up in the documentation
- Even if you do remember the exact name, it’s difficult to know precisely what each config setting does, so you often end up having to look it up in the documentation
- You don’t get any feedback on whether you’ve entered a valid setting for each config setting
So working with config files usually involves opening up your config/general.php file in one window, and then opening up the General Config settings documentation in another window, doing multiple searches, and copying & pasting.
This can be error-prone and tedious to do.
Link Fluent Config File Syntax
The same config file for Craft CMS, done using the new fluent syntax available in Craft CMS 4.2 or later would look like this:
use craft\config\GeneralConfig;
use craft\helpers\App;
$isDev = App::env('CRAFT_ENVIRONMENT') === 'dev';
$isProd = App::env('CRAFT_ENVIRONMENT') === 'production';
return GeneralConfig::create()
->defaultWeekStartDay(1)
->omitScriptNameInUrls(true)
->devMode($isDev)
->allowAdminChanges($isDev)
->disallowRobots(!$isProd)
;
This should look familiar to you, because if you’ve ever written an Element Query for Craft CMS, because they use the same fluent syntax as well.
Here’s an example of a fluent config file in action:
At first blush, this might appear to be just re-arranging the deck chairs on the Titanic. However, it offers some very important key advantages over the traditional syntax.
1. AUTOCOMPLETE
Every fluent config setting available will autocomplete in your editor, ensuring that you always get the naming correct:
Additionally, if you do type an incorrect fluent config setting, you’ll get immediate feedback that it doesn’t exist:
This autocomplete makes adding and editing config files much less error-prone.
2. INLINE DOCUMENTATION
Every fluent config setting also has inline documentation available to your editor:
This allows you to understand the details of a particular config file setting without ever having to leave your editor.
3. TYPE-CHECKING
Every fluent config setting also knows what type of data it is expecting:
So if you try to pass in an array to a config setting that takes a number or a string, your editor will immediately highlight it for you.
Link Real-World Examples
Here are two real-world examples (along with the .env file) from the devMode.fm website:
The entire website is available as MIT licensed OSS on GitHub in the nystudio107/devmode repository, and uses a flat config as discussed in the Flat Multi-Environment Config for Craft CMS 3 article.
Link A RegEx for converting config files
To help you convert over old array syntax config files, you can use this RegEx from Brandon Kelly to do it:
(?<=^ {4})'(\w+)'\s*=>\s*([\w\W]+?),(?=\n(\]| {4}'\w|( {4})?\/\/))
->$1($2)
You can do that in your favorite text editor via RegEx search/replace, or you can just click the link below, and paste in your config file to convert it online!
You’ll also need to change:
return [
to:
use craft\config\GeneralConfig;
return GeneralConfig::create()
and then delete the trailing ]; at the bottom of the file.
But that should make it pretty easy to switch over!
Link Let’s Get Fluent!
As you can see, there are several advantages to using this fluent syntax and no downsides. It still ends up setting the exact same config settings, just in a way that is much more friendly to you, the developer.
Many people (including Pixel & Tonic, the creators of Craft CMS) have already moved to Flat Multi-Environment Config setups.
Fluent configs take this a step further by providing all of the autocomplete, documentation, and type-checking goodness to you.
Link Config Settings via Environment Variable
In addition to using fluent syntax for your config files in Craft CMS 4.2 or later, it’s worth noting a new feature in Craft CMS 4: config settings via environment variable.
This means that Craft will automatically set any GeneralConfig or DbConfig setting just by using a properly named environment variable in your .env file.
To do this, combine the CRAFT_ prefix with the config setting in screaming snake case. The allowUpdates setting, for example, would be CRAFT_ALLOW_UPDATES. The database port setting would be CRAFT_DB_PORT.
There are some downsides to this approach
- There’s no autocomplete, type checking, or documentation as you get from the fluent syntax approach
- You have to make sure you convert the environment variable to screaming snake case properly, or it won’t work
- Environment variables were originally intended for local development only, but everyone started using them in production anyway. Environment variables are intended to be for things that vary from environment to environment, not a store for general settings
- Because there are some things you can’t do with this approach, you end up splitting where config settings are set between both your config file, and the .env file, so it becomes an easter egg hunt looking for where a setting comes from
Still, it’s worth noting that this is also available to you.
Link Fluent Plugin Config Settings
Plugin developers who set the minimum requirement of Craft CMS ^4.2.0 for their plugins can get fluent config files for their plugins, too!
Craft expects a plugin’s Settings Model to extend from craft\base\Model, and because the craft\config\BaseConfig class extends from craft\base\Model, you can just use it instead:
<?php
namespace mynamespace\models;
use craft\config\BaseConfig;
class Settings extends BaseConfig
{
public string $foo = 'defaultFooValue';
public string $bar = 'defaultBarValue';
public function rules(): array
{
return [
[['foo', 'bar'], 'required'],
// ...
];
}
}
Just making this change accomplishes two things:
- You can use the factory method Settings::create() to create an instance of your Settings model
- Because your Settings model extends from BaseConfig, Craft’s Config service will allow you to return that object from your config file (array syntax still works as always)
To make it work with the fluent syntax, we just need to create setter methods for our properties:
<?php
namespace mynamespace\models;
use craft\config\BaseConfig;
class Settings extends BaseConfig
{
public string $foo = 'defaultFooValue';
public string $bar = 'defaultBarValue';
public function foo(string $value): self
{
$this->foo = $value;
return $this;
}
public function bar(string $value): self
{
$this->bar = $value;
return $this;
}
public function rules(): array
{
return [
[['foo', 'bar'], 'required'],
// ...
];
}
}
This will allow people to do something like this in their config/my-plugin-config.php file:
return Settings::create()
->foo('oof')
->bar('rab')
;
So this gives us a nice fluent syntax, with autocomplete of config properties, and type checking for config values.
But to really add value, we need to add some docblock comments to our properties and methods:
<?php
namespace mynamespace\models;
use craft\config\BaseConfig;
class Settings extends BaseConfig
{
/**
* @var string Foo controls all of the foo-ness functionality
*/
public string $foo = 'defaultFooValue';
/**
* @var string Bar raises the standards for the plugin
*/
public string $bar = 'defaultBarValue';
/**
* Foo controls all of the foo-ness functionality
* @param string $value
* @return $this
*/
public function foo(string $value): self
{
$this->foo = $value;
return $this;
}
/**
* Bar raises the standards for the plugin
* @param string $value
* @return $this
*/
public function bar(string $value): self
{
$this->bar = $value;
return $this;
}
public function rules(): array
{
return [
[['foo', 'bar'], 'required'],
// ...
];
}
}
Link Into the deep
For the curious, I did explore several other methods for providing this autocomplete, type checking, and inline documentation functionality.
Alas, there were downsides to all of the other approaches:
- I initially tried using PhpStorm’s ArrayShape annotations on the GeneralConfig::__construct() method’s $config parameter, however, they don’t allow for per-array element comments, so we are missing the inline documentation, which is critically important for config file settings
- I had also tried implementing a fluent model using PHP magic methods, but @method and @property annotations only allow for a single line of documentation, so we’d be missing out on the GeneralConfig class’s copious documentation, and also then we have magic method overhead for setting each config property. It was also almost as much work as just writing fluent setter methods.
Using the relatively simplistic technique of just writing the setter methods works the best, and has the broadest support in code editors.