core/Config.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

namespace luya;

use luya\helpers\ArrayHelper;

/**
 * Configuration array Helper.
 *
 * The {{luya\Config}} allows you to create the configuration for different hosts and difference between web and console config.
 *
 * ```php
 * $config = new Config('myapp', dirname(__DIR__), [
 *     'siteTitle' => 'My LUYA Project',
 *     'defaultRoute' => 'cms',
 *     // other application level configurations
 * ]);
 *
 * // define global components which works either for console or web runtime
 *
 * $config->component('mail', [
 *     'host' => 'xyz',
 *     'from' => 'from@luya.io',
 * ]);
 *
 * $config->component('db', [
 *     'class' => 'yii\db\Connection',
 *     'dsn' => 'mysql:host=localhost;dbname=prod_db',
 *     'username' => 'foo',
 *     'password' => 'bar',
 * ]);
 *
 * // define components which are only for web or console runtime:
 *
 * $config->webComponent('request', [
 *     'cookieValidationKey' => 'xyz',
 * ]);
 *
 * // which is equals to, but the above is better to read and structure in the config file
 *
 * $config->component('request', [
 *     'cookieValidationKey' => 'xyz',
 * ])->webRuntime();
 *
 * // adding modules
 *
 * $config->module('admin', [
 *     'class' => 'luya\admin\Module',
 *     'secureLogin' => true,
 * ]);
 *
 * $config->module('cms', 'luya\cms\frontend\Module'); // which is equals to $config->module('cms', ['class' => 'luya\cms\frontend\Module']);
 *
 * // export and generate the config for a given enviroment or environment independent.
 *
 * return $config->toArray(); // returns the config not taking care of enviroment variables like prod, env
 *
 * return $config->toArray([Config::ENV_PROD]);
 * ```
 *
 * ## Runtime
 *
 * Each method returns an {{luya\ConfigDefinition}} object and can therefore be configured for different runtimes (console/web). The given example will add a console command
 * only for console applications:
 *
 * ```php
 * $config->application([
 *     'controllerMap' => [
 *         's3' => 'luya\aws\commands\S3Command',
 *     ]
 * ])->consoleRuntime();
 * ```
 *
 * ## Envs
 *
 * Switching between envs can be usefull if certain configurations should only apply on a certain environment. Therefore you can add `env()` behind componenets, applications and modules.
 *
 * ```php
 * $config->component('db', [
 *     'class' => 'yii\db\Connection',
 *     'dsn' => 'mysql:host=localhost;dbname=local_db',
 *     'username' => 'foo',
 *     'password' => 'bar',
 * ])->env(Config::ENV_LOCAL);
 *
 * $config->component('db', [
 *     'class' => 'yii\db\Connection',
 *     'dsn' => 'mysql:host=localhost;dbname=dev_db',
 *     'username' => 'foo',
 *     'password' => 'bar',
 * ])->env(Config::ENV_DEV);
 *
 * $config->component('db', [
 *     'class' => 'yii\db\Connection',
 *     'dsn' => 'mysql:host=localhost;dbname=prod_db',
 *     'username' => 'foo',
 *     'password' => 'bar',
 * ])->env(Config::ENV_PROD);
 *
 * return $config->toArray(Config::ENV_PROD); // would only return the prod env db component
 * ```
 *
 * > When mergin varaibles, the later will always override the former. If arrays are involved the values will be added, not replaced!
 * > Example: `'foo' => 'bar', 'values' => [1]` and `'foo' => 'baz', 'values' => [2]` will be merged to: `'foo' => 'baz', 'values' => [1,2]`.
 *
 * @author Basil Suter <basil@nadar.io>
 * @since 1.0.21
 */
class Config
{
    public const ENV_ALL = 'all';

    /**
     * @var string Predefined constant for production
     */
    public const ENV_PROD = 'prod';

    /**
     * @var string Predefined constant for preproduction
     */
    public const ENV_PREP = 'prep';

    /**
     * @var string Predefined constant for development mode
     */
    public const ENV_DEV = 'dev';

    /**
     * @var string Predefined constant for local development
     */
    public const ENV_LOCAL = 'local';

    /**
     * @var string Predefined constant for ci enviroments
     */
    public const ENV_CI = 'ci';

    public const RUNTIME_ALL = 0;

    public const RUNTIME_CONSOLE = 1;

    public const RUNTIME_WEB = 2;

    /**
     * Constructor
     *
     * @param string $id
     * @param string $basePath
     * @param array $applicationConfig
     */
    public function __construct($id, $basePath, array $applicationConfig = [])
    {
        $applicationConfig['id'] = $id;
        $applicationConfig['basePath'] = $basePath;
        $this->application($applicationConfig);
    }

    private $_env;

    /**
     * Assign the env to each component, module or application that defined inside the callback.
     *
     * Callback function has one parameter with the current {{luya\Config}} object.
     *
     * An example using env to wrap multiple configuration lines into a single environment:
     *
     * ```php
     * $config->env(Config::ENV_LOCAL, function($config) {
     *
     *     $config->callback(function() {
     *         define('YII_DEBUG', true);
     *         define('YII_ENV', 'local');
     *     });
     *
     *     $config->component('db', [
     *         'dsn' => 'mysql:host=luya_db;dbname=luya_kickstarter',
     *         'username' => 'luya',
     *         'password' => 'luya',
     *         'enableSchemaCache' => false,
     *     ]);
     *
     *     $config->module('debug', [
     *         'class' => 'yii\debug\Module',
     *         'allowedIPs' => ['*'],
     *     ]);
     *
     *     $config->bootstrap(['debug']);
     * });
     * ```
     *
     * @param string $env The environment to assigne inside the callback.
     * @param callable $callback function(\luya\Config $config)
     * @return $this
     */
    public function env($env, callable $callback)
    {
        $this->_env = $env;

        try {
            call_user_func($callback, $this);
        } finally {
            $this->_env = null;
        }

        return $this;
    }

    /**
     * register application level config
     *
     * @param array $config The array to configure
     * @return ConfigDefinition
     */
    public function application(array $config)
    {
        return $this->addDefinition(new ConfigDefinition(ConfigDefinition::GROUP_APPLICATIONS, 'application', $config));
    }

    /**
     * Register one or more bootstrap entries into the bootstrap section.
     *
     * @param array $config An array with bootstrap entries, its common to use the module name
     * @return ConfigDefinition
     */
    public function bootstrap(array $config)
    {
        return $this->addDefinition(new ConfigDefinition(ConfigDefinition::GROUP_BOOTSTRAPS, 'bootstrap', $config));
    }

    /**
     * Register a module.
     *
     * @param string $id The module identifier.
     * @param string|array $config The configuration for the given module. If a string is given this will be taken as `class` property.
     * @return ConfigDefinition
     */
    public function module($id, $config)
    {
        return $this->addDefinition(new ConfigDefinition(ConfigDefinition::GROUP_MODULES, $id, $config));
    }

    /**
     * Run a callable functions for the defined env when toArray() is called.
     *
     * @param callable $fn The function to run, the first argument of the closure is the {{luya\Config}} object.
     * @return ConfigDefinition
     * @since 1.0.23
     */
    public function callback(callable $fn)
    {
        return $this->addDefinition(new ConfigDefinition(ConfigDefinition::GROUP_CALLABLE, false, $fn));
    }

    /**
     * Register a component
     *
     * @param string $id The id of the component
     * @param string|array $config The configuration for the given module. If a string is given this will be taken as `class` property.
     * @param string $runtime The runtime for the component: all, web or console
     * @return ConfigDefinition
     */
    public function component($id, $config, $runtime = self::RUNTIME_ALL)
    {
        return $this->addDefinition(new ConfigDefinition(ConfigDefinition::GROUP_COMPONENTS, $id, $config))->runtime($runtime);
    }

    /**
     * Register a web runtime component.
     *
     * @param string $id The id of the component
     * @param string|array $config The configuration for the given module. If a string is given this will be taken as `class` property.
     * @return ConfigDefinition
     */
    public function webComponent($id, $config)
    {
        return $this->component($id, $config, self::RUNTIME_WEB);
    }

    /**
     * Register a console runtime component.
     *
     * @param string $id The id of the component
     * @param string|array $config The configuration for the given module. If a string is given this will be taken as `class` property.
     * @return ConfigDefinition
     */
    public function consoleComponent($id, $config)
    {
        return $this->component($id, $config, self::RUNTIME_CONSOLE);
    }

    private $_definitions = [];

    /**
     * Add a definition into the definitions bag.
     *
     * @param ConfigDefinition $definition
     * @return ConfigDefinition
     */
    private function addDefinition(ConfigDefinition $definition)
    {
        if ($this->_env !== null) {
            $definition->env($this->_env);
        }

        $this->_definitions[] = $definition;

        return $definition;
    }

    private $_isCliRuntime;

    /**
     * Whether runtime is cli or not
     *
     * @return boolean
     */
    public function isCliRuntime()
    {
        if ($this->_isCliRuntime === null) {
            $this->_isCliRuntime = strtolower(php_sapi_name()) === 'cli';
        }

        return $this->_isCliRuntime;
    }

    /**
     * Setter method for runtime.
     *
     * > This method is mainly used for unit testing.
     *
     * @param boolean $value
     */
    public function setCliRuntime($value)
    {
        $this->_isCliRuntime = $value;
    }

    /**
     * Export the given configuration as array for certain envs.
     *
     * @param array|string $envs A list of environments to export. if nothing is given all enviroments will be returned. A string will be threated as array with 1 entry.
     * @return array The configuration array
     */
    public function toArray($envs = [])
    {
        $config = [];
        $envs = (array) $envs;
        $envs = array_merge($envs, [self::ENV_ALL]);
        foreach ($this->_definitions as $definition) { /** @var ConfigDefinition $definition */
            // validate if current export env is in the list of envs
            if (!$definition->validateEnvs($envs)) {
                continue;
            }
            // validate runtime circumstances
            if ($definition->validateRuntime(self::RUNTIME_ALL)) {
                $this->appendConfig($config, $definition);
            } elseif ($this->isCliRuntime() && $definition->validateRuntime(self::RUNTIME_CONSOLE)) {
                $this->appendConfig($config, $definition);
            } elseif (!$this->isCliRuntime() && $definition->validateRuntime(self::RUNTIME_WEB)) {
                $this->appendConfig($config, $definition);
            }
        }

        return $config;
    }

    /**
     * Append a given definition int othe config
     *
     * @param array $config
     * @param ConfigDefinition $definition
     */
    private function appendConfig(&$config, ConfigDefinition $definition)
    {
        switch ($definition->getGroup()) {
            case ConfigDefinition::GROUP_APPLICATIONS:
                foreach ($definition->getConfig() as $k => $v) {
                    $config[$k] = $v;
                }
                break;

            case ConfigDefinition::GROUP_COMPONENTS:
                $this->handleKeyBaseMerge($config, $definition, 'components');
                break;

            case ConfigDefinition::GROUP_MODULES:
                $this->handleKeyBaseMerge($config, $definition, 'modules');
                break;

            case ConfigDefinition::GROUP_BOOTSTRAPS:
                if (!array_key_exists('bootstrap', $config)) {
                    $config['bootstrap'] = [];
                }
                foreach ($definition->getConfig() as $v) {
                    $config['bootstrap'][] = $v;
                }
                break;

            case ConfigDefinition::GROUP_CALLABLE:
                call_user_func($definition->getConfig(), $this);
                break;
        }
    }

    /**
     * Add a array key based component definition.
     *
     * @param array $config
     * @param ConfigDefinition $definition
     * @param string $section
     */
    private function handleKeyBaseMerge(&$config, ConfigDefinition $definition, $section)
    {
        // ass missing section key
        if (!array_key_exists($section, $config)) {
            $config[$section] = [];
        }

        // array key from definition in order to merge with existing values
        $key = $definition->getKey();

        // if key exists, merge otherwise create key
        if (isset($config[$section][$key])) {
            $config[$section][$key] = ArrayHelper::merge($config[$section][$key], $definition->getConfig());
        } else {
            $config[$section][$key] = $definition->getConfig();
        }
    }
}