Andrew Welch · Insights · #craft-4 #craftcms #config

Published , updated · 5 min read ·


Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.

Fluent Multi-Environment Config for Craft CMS 4

Using the new flu­ent mod­el con­fig set­tings in Craft CMS 4.2 allows you to cre­ate bet­ter con­fig files, with less work and few­er errors

Fluent multi environment config files craft 4

Craft CMS stores mul­ti-envi­ron­ment con­fig files in the config/ direc­to­ry, which allow you to con­trol the behav­ior of Craft CMS and plugins.

A recent PR merged into Craft CMS 4.2 pro­vides an option­al flu­ent inter­face to set your con­fig set­tings that is easy to use, and offers sev­er­al advan­tages over the tra­di­tion­al con­fig file syn­tax you’re used to.

First, let’s look at the prob­lem with tra­di­tion­al con­fig file syntax.

Link Traditional Config File Syntax

A tra­di­tion­al con­fig 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 prob­lem with this approach is that there are over 150 Gen­er­al Con­fig set­tings at the time of this writ­ing, with more being added all the time:

  • It’s dif­fi­cult to remem­ber the exact name of each con­fig set­ting, so you often end up hav­ing to look it up in the documentation
  • Even if you do remem­ber the exact name, it’s dif­fi­cult to know pre­cise­ly what each con­fig set­ting does, so you often end up hav­ing to look it up in the documentation
  • You don’t get any feed­back on whether you’ve entered a valid set­ting for each con­fig setting

So work­ing with con­fig files usu­al­ly involves open­ing up your config/general.php file in one win­dow, and then open­ing up the Gen­er­al Con­fig set­tings doc­u­men­ta­tion in anoth­er win­dow, doing mul­ti­ple search­es, and copy­ing & pasting.

This can be error-prone and tedious to do.

Link Fluent Config File Syntax

The same con­fig file for Craft CMS, done using the new flu­ent syn­tax avail­able in Craft CMS 4.2 or lat­er 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 famil­iar to you, because if you’ve ever writ­ten an Ele­ment Query for Craft CMS, because they use the same flu­ent syn­tax as well.

Here’s an exam­ple of a flu­ent con­fig file in action:

At first blush, this might appear to be just re-arrang­ing the deck chairs on the Titan­ic. How­ev­er, it offers some very impor­tant key advan­tages over the tra­di­tion­al syntax.

1. AUTO­COM­PLETE

Every flu­ent con­fig set­ting avail­able will auto­com­plete in your edi­tor, ensur­ing that you always get the nam­ing correct:

Fluent config autocomplete

Auto­com­plete of con­fig settings

Addi­tion­al­ly, if you do type an incor­rect flu­ent con­fig set­ting, you’ll get imme­di­ate feed­back that it does­n’t exist:

Fluent config wrong name

High­light­ing incor­rect con­fig settings

This auto­com­plete makes adding and edit­ing con­fig files much less error-prone.

2. INLINE DOCUMENTATION

Every flu­ent con­fig set­ting also has inline doc­u­men­ta­tion avail­able to your editor:

Fluent config documentation

Inline Doc­u­men­ta­tion for con­fig settings

This allows you to under­stand the details of a par­tic­u­lar con­fig file set­ting with­out ever hav­ing to leave your editor.

3. TYPE-CHECK­ING

Every flu­ent con­fig set­ting also knows what type of data it is expecting:

Fluent config type checking

Type check­ing of con­fig set­ting values

So if you try to pass in an array to a con­fig set­ting that takes a num­ber or a string, your edi­tor will imme­di­ate­ly high­light it for you.

Link Real-World Examples

Here are two real-world exam­ples (along with the .env file) from the dev​Mode​.fm website:

The entire web­site is avail­able as MIT licensed OSS on GitHub in the nystudio107/​devmode repos­i­to­ry, and uses a flat con­fig as dis­cussed in the Flat Mul­ti-Envi­ron­ment Con­fig for Craft CMS 3 article.

Link A RegEx for converting config files

To help you con­vert over old array syn­tax con­fig files, you can use this RegEx from Bran­don Kel­ly to do it:

(?<=^ {4})'(\w+)'\s*=>\s*([\w\W]+?),(?=\n(\]| {4}'\w|( {4})?\/\/))
->$1($2)

You can do that in your favorite text edi­tor via RegEx search/​replace, or you can just click the link below, and paste in your con­fig file to con­vert it online!

https://​regexr​.com/​6rl4g

You’ll also need to change:

return [

to:

use craft\config\GeneralConfig;
return GeneralConfig::create()

and then delete the trail­ing ]; at the bot­tom of the file.

But that should make it pret­ty easy to switch over!

Link Let’s Get Fluent!

As you can see, there are sev­er­al advan­tages to using this flu­ent syn­tax and no down­sides. It still ends up set­ting the exact same con­fig set­tings, just in a way that is much more friend­ly to you, the developer.

Many peo­ple (includ­ing Pix­el & Ton­ic, the cre­ators of Craft CMS) have already moved to Flat Mul­ti-Envi­ron­ment Con­fig setups.

Flu­ent con­figs take this a step fur­ther by pro­vid­ing all of the auto­com­plete, doc­u­men­ta­tion, and type-check­ing good­ness to you.

Link Config Settings via Environment Variable

In addi­tion to using flu­ent syn­tax for your con­fig files in Craft CMS 4.2 or lat­er, it’s worth not­ing a new fea­ture in Craft CMS 4: con­fig set­tings via envi­ron­ment vari­able.

This means that Craft will auto­mat­i­cal­ly set any Gen­er­al­Con­fig or DbCon­fig set­ting just by using a prop­er­ly named envi­ron­ment vari­able in your .env file.

To do this, com­bine the CRAFT_ pre­fix with the con­fig set­ting in scream­ing snake case. The allowUpdates set­ting, for exam­ple, would be CRAFT_ALLOW_UPDATES. The data­base port set­ting would be CRAFT_DB_PORT.

There are some down­sides to this approach

  • There’s no auto­com­plete, type check­ing, or doc­u­men­ta­tion as you get from the flu­ent syn­tax approach
  • You have to make sure you con­vert the envi­ron­ment vari­able to scream­ing snake case prop­er­ly, or it won’t work
  • Envi­ron­ment vari­ables were orig­i­nal­ly intend­ed for local devel­op­ment only, but every­one start­ed using them in pro­duc­tion any­way. Envi­ron­ment vari­ables are intend­ed to be for things that vary from envi­ron­ment to envi­ron­ment, not a store for gen­er­al settings
  • Because there are some things you can’t do with this approach, you end up split­ting where con­fig set­tings are set between both your con­fig file, and the .env file, so it becomes an east­er egg hunt look­ing for where a set­ting comes from

Still, it’s worth not­ing that this is also avail­able to you.

Link Fluent Plugin Config Settings

Plu­g­in devel­op­ers who set the min­i­mum require­ment of Craft CMS ^4.2.0 for their plu­g­ins can get flu­ent con­fig files for their plu­g­ins, too!

Craft expects a plug­in’s Set­tings Mod­el 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 mak­ing this change accom­plish­es two things:

  • You can use the fac­to­ry method Settings::create() to cre­ate an instance of your Settings model
  • Because your Settings mod­el extends from BaseConfig, Craft’s Config ser­vice will allow you to return that object from your con­fig file (array syn­tax still works as always)

To make it work with the flu­ent syn­tax, we just need to cre­ate set­ter meth­ods 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 peo­ple to do some­thing like this in their config/my-plugin-config.php file:

        return Settings::create()
            ->foo('oof')
            ->bar('rab')
            ;

So this gives us a nice flu­ent syn­tax, with auto­com­plete of con­fig prop­er­ties, and type check­ing for con­fig values.

But to real­ly add val­ue, we need to add some docblock com­ments to our prop­er­ties 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'],
            // ...
        ];
    }
}

This is what pro­vides the extreme­ly use­ful inline doc­u­men­ta­tion for your con­fig set­tings in the devel­op­er’s editor.

While it may seem like a bit of work, it’s rel­a­tive­ly pain­less to add this func­tion­al­i­ty to your plug­in’s con­fig settings.

And your users will thank you.

Link Into the deep

For the curi­ous, I did explore sev­er­al oth­er meth­ods for pro­vid­ing this auto­com­plete, type check­ing, and inline doc­u­men­ta­tion functionality.

Alas, there were down­sides to all of the oth­er approaches:

  • I ini­tial­ly tried using Php­Stor­m’s ArrayShape anno­ta­tions on the GeneralConfig::__construct() method­’s $config para­me­ter, how­ev­er, they don’t allow for per-array ele­ment com­ments, so we are miss­ing the inline doc­u­men­ta­tion, which is crit­i­cal­ly impor­tant for con­fig file settings
  • I had also tried imple­ment­ing a flu­ent mod­el using PHP mag­ic meth­ods, but @method and @property anno­ta­tions only allow for a sin­gle line of doc­u­men­ta­tion, so we’d be miss­ing out on the GeneralConfig class’s copi­ous doc­u­men­ta­tion, and also then we have mag­ic method over­head for set­ting each con­fig prop­er­ty. It was also almost as much work as just writ­ing flu­ent set­ter methods.

Using the rel­a­tive­ly sim­plis­tic tech­nique of just writ­ing the set­ter meth­ods works the best, and has the broad­est sup­port in code editors.