Skip to content

Latest commit

 

History

History
379 lines (313 loc) · 9.82 KB

usage.md

File metadata and controls

379 lines (313 loc) · 9.82 KB

🌱 Friendly Config - Usage

The goal of this project is that a developer should be able to use it without needing to read any documentation. If you know how to write PHP objects, you should not need more than the basics to get started.

However, if this approach feels as too much magic ✨, feel free to read these docs from start to finish. That way, you should learn that all that may seem as magic is just bunch of simple tricks. 😉

Basics

Until now, we were required to write Configuration definition class as explained in the official Symfony docs and learn how to use the TreeBuilder to create configuration such as this:

$rootNode
    ->children()
        ->arrayNode('connection')
            ->children()
                ->scalarNode('driver')
                    ->isRequired()
                    ->cannotBeEmpty()
                ->end()
                ->scalarNode('host')
                    ->defaultValue('localhost')
                ->end()
                ->scalarNode('username')->end()
                ->scalarNode('password')->end()
                ->booleanNode('memory')
                    ->defaultFalse()
                ->end()
            ->end()
        ->end()
        ->arrayNode('settings')
            ->addDefaultsIfNotSet()
            ->children()
                ->scalarNode('name')
                    ->isRequired()
                    ->cannotBeEmpty()
                    ->defaultValue('value')
                ->end()
            ->end()
        ->end()
    ->end()
;

Instead, we can write plain old PHP objects such as:

class MyConfig
{
    public function __construct(Connection $connection, Settings $settings) { /* your code */ }
}

class Connection
{
    public function __construct(
        string $driver,
        string $username,
        string $password,
        string $host = 'localhost',
        bool $memory = false,
    ) { /* your code */ }
}

class Settings
{
    public function __construct(string $name = 'value') { /* your code */ }
}

The following few lines will convert your pure PHP objects into valid Symfony configuration that defines schema of your bundle configuration. On top of that, the processed configuration values are used to initialize your pure PHP objects, so you can easily access the processed values.

You can register the initialised config objects as services which will allow you to easily access the initialised config objects anywhere in your application.

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Majermi4\FriendlyConfig\FriendlyConfiguration;
use Majermi4\FriendlyConfig\InitializeConfigObject;
use Majermi4\FriendlyConfig\RegisterConfigService;

class MyBundleExtension extends Extension
{
    /**
     * {@inheritdoc}
     */
    public function getConfiguration(array $config, ContainerBuilder $container) : ConfigurationInterface
    {
        return FriendlyConfiguration::fromClass(MyConfig::class, 'my_config');
    }
    
    /**
     * {@inheritdoc}
     */
    public function load(array $configs, ContainerBuilder $container): void
    {
        $configuration = $this->getConfiguration($configs, $container);
        $config = $this->processConfiguration($configuration, $configs);

        if ($configuration instanceof FriendlyConfiguration) {
            // Register config object with processed values as a service 
            RegisterConfigService::fromProcessedConfig($configuration->getConfigClass(), $config, $container);
    
            // Or ... initialise config object from processed config immediately if needed
            $initialisedConfig = InitializeConfigObject::fromProcessedConfig(MyConfig::class, $config);
        }        
    }
}

Simple types

When you pass your classes to FriendlyConfig, it analyses their constructor parameters and generates the Symfony configuration definition for you. The following sections show how different parameter types get interpreted.

Boolean

public function __construct(bool $yesOrNo) { }
$nodeBuilder
    ->booleanNode('yes_or_no')
        ->required()
    ->end();

Integer

public function __construct(int $number) { }
$nodeBuilder
    ->integerNode('number')
        ->required()
    ->end();

Float

public function __construct(float $number) { }
$nodeBuilder
    ->floatNode('number')
        ->required()
    ->end();

String

public function __construct(string $text) { }
$nodeBuilder
    ->scalarNode('text')
        ->required()
    ->end();

Nested types

Array of booleans, integers, floats, strings

Friendly Config object:

/**
 * @param array<bool>   $arrayOfBooleans
 * @param array<int>    $arrayOfIntegers
 * @param array<float>  $arrayOfFloats
 * @param array<string> $arrayOfStrings
 */
public function __construct(
    array $arrayOfBooleans,
    array $arrayOfIntegers,
    array $arrayOfFloats,
    array $arrayOfStrings
) { }

Symfony Config:

$nodeBuilder
    ->arrayNode('array_of_booleans')
        ->booleanPrototype()
        ->required()
    ->end()
    ->arrayNode('array_of_integers')
        ->integerPrototype()
        ->required()
    ->end()
    ->arrayNode('array_of_floats')
        ->floatPrototype()
        ->required()
    ->end()
    ->arrayNode('array_of_strings')
        ->scalarPrototype()
        ->required()
    ->end();

Config value example:

array_of_booleans: [ true, false, true, false ]
array_of_integers: [ 1, 2, 3, 4 ]
array_of_floats: [ 1.5, 2.5, 3.5, 4.5 ]
array_of_strings: [ "foo", "bar" ]

Nested object

Friendly Config object:

// Root config class constructor
public function __construct(NestedConfig $nestedConfig) { }

// Nested config class
class NestedConfig {
    public function __construct(string $nestedOption) { }
}

Symfony Config:

$nodeBuilder
    ->arrayNode('nestedConfig')
        ->arrayPrototype()
            ->children()
                ->scalarNode('nestedOption')
                    ->required()
                ->end()
            ->end()
        ->end()
    ->end();

Config value example:

nestedConfig:
    nestedOption: 'some value'

Array of nested objects

Friendly Config object:

// Root config class constructor
/**
 * @param array<string,NestedConfig> $nestedConfigs
 */
public function __construct(array $nestedConfigs) { }

// Nested config class
class NestedConfig {
    public function __construct(string $nestedOption) { }
}

Symfony Config:

$nodeBuilder
    ->arrayNode('nestedConfigs')
        ->useAttributeAsKey('name')
        ->arrayPrototype()
            ->children()
                ->scalarNode('nestedOption')
                    ->required()
                ->end()
            ->end()
        ->end()
    ->end();

Config value example:

nestedConfigs:
    nestedConfig1:
        nestedOption: 'some value'
    nestedConfig2:
        nestedOption: 'another value'

Optionally, you can skip the "key" part of the array param definition:

From: @param array<string,NestedConfig> $nestedConfigs To: @param array<NestedConfig> $nestedConfigs (Alternatively: @param NestedConfig[] $nestedConfigs)

This removes the ->useAttributeAsKey('name') part of the configuration definition which allows you to accept array items in the following format without the need to define the keys.

nestedConfigs:
    - { nestedOption: 'some value' }
    - { nestedOption: 'another value' }

Other inferred configuration options

Default value

When you define default value for your constructor parameters such as:

public function __construct(string $text = 'Default value') { }
$nodeBuilder
    ->scalarNode('text')
        ->defaultValue('Default value')
    ->end();

If you want to define a default value of a non-scalar parameter, you can make the parameter nullable and set the default manually inside the body of the constructor function.

Required

When a constructor parameter is nullable, or it has a default value, it is not considered required. It is considered required in all other cases.

public function __construct(
    string $requiredOption,
    ?string $nullableOption,
    string $optionWithDefault = 'some default'
) { }
$nodeBuilder
    ->scalarNode('text')
        ->required()
    ->end()
    ->scalarNode('nullableOption')->end()
    ->scalarNode('text')
        ->defaultValue('Default value')
    ->end();

Info

An important feature of the Symfony Configuration component is that it provides documentation for all available configuration options with the config:dump-reference command

When you write configuration class such as this:

class MyConfig {
    /**
     * Option info can be also parsed from the PhpDoc comment above a property
     * named the same as the constructor option in the same class. 
     */
    private string $optionDocumentedViaProperty;

    /**
     * @param string $optionDocumentedViaParameter This is another option to document config option.
     */
    public function __construct(
        string $optionDocumentedViaProperty,
        string $optionDocumentedViaParameter
    ) { }
}

Both texts from either the @param annotation or from the comment above a class property named the same as the constructor parameter will be used as documentation via the ->info() method.

$nodeBuilder
    ->scalarNode('optionDocumentedViaProperty')
        ->info('Option info can be also parsed from the PhpDoc comment above a property named the same as the constructor option in the same class.')
        ->required()
    ->end()
    ->scalarNode('optionDocumentedViaParameter')
        ->info('This is another option to document config option.')
        ->required()
    ->end();