Yii 3 configuration concept

The concept document was prepared by @roxblnfk

By config we assume a set of options presented as an array or an object. These options are grouped inside a package or project by use-case. There could be multiple configs in a single package or project.

Developers as usually:

  • Reading existing configs.
  • Adjusting existing service config.
  • Adding a config for a new service (often by copy-pasting from readme).

Yii 2 config was good and bad at the same time. Good part was that config keys were directly mapped to configured object properties. Bad part was that it resulted in public properties and setters everywhere and didn’t play well with non-Yii style PHP-code.

One of the Yii 3 goals is allowing to configure and use any PHP-code without wrappers. Configs are a bit more complicated overall because of DI-container and composition usage.

In general, the process looks like the following (red arrow):

The application requests an instance of a certain interface from container. Container creates an instance based on config directly or by using a factory. Factory config is usually situated at params.php. In the same directory there are common.php, web.php and console.php. These files are merged and cachged by composer-config-plugin.

The concept

Current approach is relatively user friendly so I suggest keeping it with some additions:

  1. Introduce config objects (green arrows and DB Config on a diagram). These contain parameter names and default values. The config itself stays declarative as it was. You’ll have to use extra object for configuring a service but it could be done by DI container for you.
  2. In params.php config class name is used as config key (example).

That immediately gives us:

  • Ability to get config as object from DI container.
  • Independence from $params (or even composer-config-plugin) so it could be configured either manually or via container factories.
  • Because of previous point usage Yii packages in non-Yii project would be more convenient.
  • When clicking on config class name IDE, you can get to its description in no time to check what fields are available, what is the meaning of each one and what values are accepted. That levels up readability significantly.
  • Validation and normalization of values provided could be done directly in the config class. That solves a problem with typos when modifying config. Currently typos aren’t resulting in error but in value falling back to default.

The concept is partially implemented as a PR https://github.com/yiisoft/yii-cycle/pull/5

Implementation details

Let’s start with params.php:

# before
'cycle.migration' => [
    'directory' => '@root/migrations',
    'namespace' => 'App\\Migration',
    'table' => 'migration',
    'safe' => false,
],
# after
CycleMigrationConfig::class => [
    'directory' => '@root/migrations',
    'namespace' => 'App\\Migration',
    'table' => 'migration',
    'safe' => false,
],

Instead of string cycle.migration class name is now used, CycleMigrationConfig (the class is describing migrations config). Since we’ve modified params.php we have to rebuild config files cache as required by composer-config-plugin:

composer du

After merging and caching params.php, composer-config-plugin will start processing container configs (common.php, web.php, console.php etc.). When it will get to common.php, a special genrator
would analyze params.php and all the parameters from it that are related to config files will be automatically written to common.php cache as rules for creating corrsponding config-objects:

'Yiisoft\\Yii\\Cycle\\CycleMigrationConfig' => [
    '__class' => 'Yiisoft\\Yii\\Cycle\\CycleMigrationConfig',
    'configure()' => [
        [
            'directory' => '@root/migrations',
            'namespace' => 'App\\Migration',
            'table' => 'migration',
            'safe' => false,
        ],
    ],
],

So container will directly create config object without accessing params.php in runtime.

Additionally, validation could be added at config generation stage. It’s not in the concept implementation yet.

For each field (property) of the config there could be getter and setter methods defined. Setters will be used during filling config object with values (if there’s no setter for a field, the field is written directly to the property). Getters are called automatically when trying to read non-public properties, usually when getting config as array via toArray(). If there is no getter for a field, it still can be called. To achieve it, there’s some magic and @method annotations for getters in a base config class.

Setter example in CycleDbalConfig.
If file path is specified with an alias, the alias is transformed to full path in the setter.

'connection' => 'sqlite:@runtime/database.db'

Similar getter example could be found in CycleMigrationConfig,
but in this case alias transformation happens when directory value is requested.

protected $directory = '@root/migrations';

/** @var Yiisoft\Aliases\Aliases */
private $objAliases;

protected function getDirectory(): string
{
    return $this->convertAlias($this->directory);
}
protected function convertAlias(string $alias): string
{
    return $this->objAliases->get($alias, true);
}

So by using getters and setters we can do useful transformations.

Validation

Validation fits well at build and cache configs stage. Imagine that you change project config, launching config build and immediately getting a list of errors and useful recommendations on how to fix them. Useful? Yes. There’s even no need to run a web application for that.

Validation rules, in my opinion, could be specified as property annotations. Rules set could be extendable to be able to validate more complex values starting from email and IP subnets, ending with custom objects with complex structure.

Config validation is not necessary to throw exceptions on each minor error preventing config building. Warnings would allow both detecting issues and not breaking in production when a tiny and irrelevant part of the config is incorrect.

Questions

  • Is the concept clear for you?
  • Do you like such configs?
  • Do you like using annotations to describe validation rules as both package developer? Same question but as a package user?
3 Likes

How does the concept develop if the same classes are used with multiple configurations?

eg. yii2:

'db' => [class=>yii\db\Connectionm, ...]      // normal DB
'dbStat' => [class=>yii\db\Connection, ...]  // DB for statistic

Will the generated config be able to do similar like this:
eg. yii2

'paypal' => [class=>..., clientId=>!empty($_COOKIE['owfqi.....igh24ig']) ? 'sandbox client id' : 'product client id']

I know this is not an elegant solution, but it is efficient and easy to develop.

1 Like

The proposed concept does not impair the existing configuration methods

In the first case you can pass objects of the extended class. A factory can be either a custom function or a custom class object.

class DbConfig extends BaseConfig { ... } // Original config class
class StatsConfig extends DbConfig { ... } 
// params.php
return [
    DbConfig ::class => [...],
    StatsConfig ::class => [...],
];
// web.php or common.php
return [
    dbClass::class => function (Container $c) {
        return new dbClass($c->get(DbConfig ::class));
    },
    dbStatsClass::class => function (Container $c) {
        return new dbClass($c->get(StatsConfig ::class));
    },
];

In the second case, it is better to use a factory where you define arguments using custom logic.

// web.php or common.php
    'paypal' => function (Container $c) {
        $request = $c->get(ServerRequestInterface::class);
        $isSandbox = !empty(request->getCookieParams()['owfqi.....igh24ig']);
        return new SomeClass($isSandbox ? 'sanbox id' : 'production id');
    },

The concept is clear.

I do not find the detached configuration approach useful.

Adding a config for a new service (often by copy-pasting from readme).

Copy-paste from 2 different locations to two different locations instead of one location.

This kills the array_merge() functionality in environments config, which is very useful in yii2:

In the first case you can pass objects of the extended class. A factory can be either a custom function or a custom class object.

// web.php or common.php
    'paypal' => function (Container $c) {
        $request = $c->get(ServerRequestInterface::class);
        $isSandbox = !empty(request->getCookieParams()['owfqi.....igh24ig']);
        return new SomeClass($isSandbox ? 'sanbox id' : 'production id');
    },

However, I find it good that the IDE can use autocomplete in this case.

You mean DI container config and params.php? Both are array-merged.

You mean DI container config and params.php? Both are array-merged.

// params.php
return [
    // <---------------- copy here I.
    DbConfig ::class => [...],
    StatsConfig ::class => [...],
];
// web.php or common.php
return [
    // <---------------- copy here II.
]

It is not very useful to interpret the generated file as developer source code.

Yes. In this regard it’s not as convenient as Yii 2 with components array.

In the case of objects, you can also write only in common.php or web.php (not in params.php)

In addition, we can implement a system of aliases so as not to spawn classes. Mention of this has been removed from the final version of the concept.

Any more questions that I can only use JSON types in params.php config definition? During generation, are the following possible?

// params.php
MyConfig::class => [
  'callback' => function() {..},
  'aggregator' => new SumAggregator(),
  'aggrefator2' => new class extends SumAggregator {..},
],

In addition, we can implement a system of aliases so as not to spawn classes. Mention of this has been removed from the final version of the concept.

It is not common, so I think it is acceptable to create separate classes.

During merging configuration files:

// params.php
MyConfig::class => [
  // possible
  'callback' => function() {..},
  // possible
  'aggregator' => new SumAggregator(),

  //  Exception Serialization of 'class@anonymous' is not allowed 
  'aggrefator2' => new class extends SumAggregator {..},

  // possible
  'callback' => function() {
    $a = new class extends SumAggregator {..};
  },
],

If you asked about embedded objects in the config, then this is possible, but it still needs to be implemented.

1 Like

This concept would make sense to me if it is a long running Application, like a Desktop program you start once, and it runs for hours, or some server service running longer. Its okay if that tool, takes a while to start…

But in the case of Yii … it runs for a splitsecond to answer a request.

In that splitsecond, for every request, it has todo all that instantiating. This overhead cant be good.

Unless I am wrong, and the overhead is so small its negligible (< 1ms maybe would be ok?).

It’s better to measure it. Thanks for idea.

  • Yes, I suppose
  • “When clicking on config class name IDE, you can get to its description in no time to check what fields are available, what is the meaning of each one and what values are accepted. That levels up readability significantly” - like it
  • For me annotations should be annotations
1 Like

@roxblnfk measured overhead and it’s very minimal from configs themselves. The most overhead so far comes from the DI container for the case of auto-wiring dependencies.

@mathis ↑