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:
- 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.
- 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?