--- /dev/null
+<?php
+
+/*
+ * This file is part of the Behat Testwork.
+ * (c) Konstantin Kudryashov <ever.zet@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Behat\Testwork\ServiceContainer\Configuration;
+
+use Behat\Testwork\ServiceContainer\Exception\ConfigurationLoadingException;
+use Symfony\Component\Yaml\Yaml;
+
+/**
+ * Loads configuration from different sources.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+final class ConfigurationLoader
+{
+ /**
+ * @var null|string
+ */
+ private $configurationPath;
+ /**
+ * @var null|string
+ */
+ private $environmentVariable;
+ /**
+ * @var Boolean
+ */
+ private $profileFound;
+ /**
+ * @var array
+ */
+ private $debugInformation = array(
+ 'environment_variable_name' => 'none',
+ 'environment_variable_content' => 'none',
+ 'configuration_file_path' => 'none'
+ );
+
+ /**
+ * Constructs reader.
+ *
+ * @param string $environmentVariableName Environment variable name
+ * @param string $configurationPath Configuration file path
+ */
+ public function __construct($environmentVariableName = null, $configurationPath = null)
+ {
+ $this->environmentVariable = $environmentVariableName;
+ $this->configurationPath = $configurationPath;
+ }
+
+ /**
+ * Sets environment variable name.
+ *
+ * @param null|string $variable
+ */
+ public function setEnvironmentVariableName($variable)
+ {
+ $this->environmentVariable = $variable;
+ }
+
+ /**
+ * Returns environment variable name.
+ *
+ * @return null|string
+ */
+ public function getEnvironmentVariableName()
+ {
+ return $this->environmentVariable;
+ }
+
+ /**
+ * Sets configuration file path.
+ *
+ * @param null|string $path
+ */
+ public function setConfigurationFilePath($path)
+ {
+ $this->configurationPath = $path;
+ }
+
+ /**
+ * Returns configuration file path.
+ *
+ * @return null|string
+ */
+ public function getConfigurationFilePath()
+ {
+ return $this->configurationPath;
+ }
+
+ /**
+ * Reads configuration sequence for specific profile.
+ *
+ * @param string $profile Profile name
+ *
+ * @return array
+ *
+ * @throws ConfigurationLoadingException
+ */
+ public function loadConfiguration($profile = 'default')
+ {
+ $configs = array();
+ $this->profileFound = false;
+
+ // first is ENV config
+ foreach ($this->loadEnvironmentConfiguration() as $config) {
+ $configs[] = $config;
+ }
+
+ // second is file configuration (if there is some)
+ if ($this->configurationPath) {
+ $this->debugInformation['configuration_file_path'] = $this->configurationPath;
+
+ foreach ($this->loadFileConfiguration($this->configurationPath, $profile) as $config) {
+ $configs[] = $config;
+ }
+ }
+
+ // if specific profile has not been found
+ if ('default' !== $profile && !$this->profileFound) {
+ throw new ConfigurationLoadingException(sprintf(
+ 'Can not find configuration for `%s` profile.',
+ $profile
+ ));
+ }
+
+ return $configs;
+ }
+
+ /**
+ * Returns array with configuration debug information.
+ *
+ * @return array
+ */
+ public function debugInformation()
+ {
+ return $this->debugInformation;
+ }
+
+ /**
+ * Loads information from environment variable.
+ *
+ * @return array
+ *
+ * @throws ConfigurationLoadingException If environment variable environment var is set to invalid JSON
+ */
+ protected function loadEnvironmentConfiguration()
+ {
+ $configs = array();
+
+ if (!$this->environmentVariable) {
+ return $configs;
+ }
+
+ $this->debugInformation['environment_variable_name'] = $this->environmentVariable;
+
+ if ($envConfig = getenv($this->environmentVariable)) {
+ $config = @json_decode($envConfig, true);
+
+ $this->debugInformation['environment_variable_content'] = $envConfig;
+
+ if (!$config) {
+ throw new ConfigurationLoadingException(sprintf(
+ 'Environment variable `%s` should contain a valid JSON, but it is set to `%s`.',
+ $this->environmentVariable,
+ $envConfig
+ ));
+ }
+
+ $configs[] = $config;
+ }
+
+ return $configs;
+ }
+
+ /**
+ * Loads information from YAML configuration file.
+ *
+ * @param string $configPath Config file path
+ * @param string $profile Profile name
+ *
+ * @return array
+ *
+ * @throws ConfigurationLoadingException If config file is not found
+ */
+ protected function loadFileConfiguration($configPath, $profile)
+ {
+ if (!is_file($configPath) || !is_readable($configPath)) {
+ throw new ConfigurationLoadingException(sprintf('Configuration file `%s` not found.', $configPath));
+ }
+
+ $basePath = rtrim(dirname($configPath), DIRECTORY_SEPARATOR);
+ $config = (array) Yaml::parse(file_get_contents($configPath));
+
+ return $this->loadConfigs($basePath, $config, $profile);
+ }
+
+ /**
+ * Loads configs for provided config and profile.
+ *
+ * @param string $basePath
+ * @param array $config
+ * @param string $profile
+ *
+ * @return array
+ */
+ private function loadConfigs($basePath, array $config, $profile)
+ {
+ $configs = array();
+
+ // first load default profile from current config, but only if custom profile requested
+ if ('default' !== $profile && isset($config['default'])) {
+ $configs[] = $config['default'];
+ }
+
+ // then recursively load profiles from imports
+ if (isset($config['imports']) && is_array($config['imports'])) {
+ $configs = array_merge($configs, $this->loadImports($basePath, $config['imports'], $profile));
+ }
+
+ // then load specific profile from current config
+ if (isset($config[$profile])) {
+ $configs[] = $config[$profile];
+ $this->profileFound = true;
+ }
+
+ return $configs;
+ }
+
+ /**
+ * Loads all provided imports.
+ *
+ * @param string $basePath
+ * @param array $paths
+ * @param string $profile
+ *
+ * @return array
+ */
+ private function loadImports($basePath, array $paths, $profile)
+ {
+ $configs = array();
+ foreach ($paths as $path) {
+ foreach ($this->parseImport($basePath, $path, $profile) as $importConfig) {
+ $configs[] = $importConfig;
+ }
+ }
+
+ return $configs;
+ }
+
+ /**
+ * Parses import.
+ *
+ * @param string $basePath
+ * @param string $path
+ * @param string $profile
+ *
+ * @return array
+ *
+ * @throws ConfigurationLoadingException If import file not found
+ */
+ private function parseImport($basePath, $path, $profile)
+ {
+ if (!file_exists($path) && file_exists($basePath . DIRECTORY_SEPARATOR . $path)) {
+ $path = $basePath . DIRECTORY_SEPARATOR . $path;
+ }
+
+ if (!file_exists($path)) {
+ throw new ConfigurationLoadingException(sprintf(
+ 'Can not import `%s` configuration file. File not found.',
+ $path
+ ));
+ }
+
+ return $this->loadFileConfiguration($path, $profile);
+ }
+}