<?php namespace LakeDrops\Component\Composer; use Composer\Json\JsonFile; use Seld\JsonLint\ParsingException; use Symfony\Component\Yaml\Yaml; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\Loader\ArrayLoader; /** * Manages project config. */ final class Config { /** * All config values. * * @var array */ private static array $values; /** * Custom config values. * * @var array */ private static array $customValues; /** * Config values to overwrite. * * @var array */ private static array $overwriteValues; /** * Use specific config values. * * @var array */ private static array $userValues; /** * The component for which this config object holds the values. * * @var string */ private string $component; /** * The Twig array loaded. * * @var \Twig\Loader\ArrayLoader */ private ArrayLoader $twigLoader; /** * The Twig environment. * * @var \Twig\Environment */ private Environment $twig; /** * The .env service. * * @var \LakeDrops\Component\Composer\Dotenv */ private Dotenv $env; /** * The filename of the config file. * * @var string */ private string $configFile; /** * The filename of user specific config file. * * @var string */ private string $userConfigFile; /** * Config constructor. * * @param string $component * The component for which this config object holds the values. * @param array $default_values * The default configuration from the component. * @param \LakeDrops\Component\Composer\Dotenv $env * The .env service. */ public function __construct(string $component, array $default_values, Dotenv $env) { $this->component = $component; $this->env = $env; $this->twigLoader = new ArrayLoader([]); $this->twig = new Environment($this->twigLoader); $this->configFile = getcwd() . '/.lakedrops.yml'; $this->userConfigFile = getcwd() . '/.lakedrops.user.yml'; $this->init(); $this->merge($default_values, self::$customValues[$this->component] ?? []); $this->merge([], self::$overwriteValues[$this->component] ?? [], FALSE); $this->merge([], self::$userValues[$this->component] ?? [], FALSE); $this->migrateFromComposerJson(); } /** * Read project configuration if not already happened. */ private function init(): void { if (!isset(self::$values)) { self::$values = []; self::$overwriteValues = []; if (!file_exists($this->configFile)) { self::$customValues = []; $this->save(); } else { self::$customValues = Yaml::parseFile($this->configFile); // Apply overwrite by stage declarations. $stage = getenv('PROJECT_BRANCH'); if (isset(self::$customValues['stage_overwrites'][$stage])) { self::$overwriteValues = self::$customValues['stage_overwrites'][$stage]; } } if (!file_exists($this->userConfigFile)) { self::$userValues = []; } else { self::$userValues = Yaml::parseFile($this->userConfigFile); } } } /** * Migrate custom settings from composer.json if required. */ private function migrateFromComposerJson(): void { $filename = getcwd() . '/composer.json'; $jsonFile = new JsonFile($filename); try { $content = $jsonFile->read(); } catch (ParsingException $e) { $content = []; } if (isset($content['extra'][$this->component])) { $this->merge([], $content['extra'][$this->component]); $this->save(); unset($content['extra'][$this->component]); try { $jsonFile->write($content); } catch (\Exception $ex) { // Ignored, if composer.json is read-only there is a general problem. } } } /** * Helper function to merge and optionally store config values. * * @param array $defaultValues * Array with default values. * @param array $customValues * Array with custom value to be merged on top of default. * @param bool $store * TRUE, if the resulting array should be stored back to the config file. */ private function merge(array $defaultValues, array $customValues, bool $store = TRUE): void { $default = self::$values[$this->component] ?? []; $custom = self::$customValues[$this->component] ?? []; if (!empty($defaultValues)) { $default = NestedArray::mergeDeep($default, $defaultValues); } if (!empty($customValues)) { $default = NestedArray::mergeDeep($default, $customValues); if ($store) { $custom = NestedArray::mergeDeep($custom, $customValues); } } self::$values[$this->component] = $this->env->replaceEnvironmentVariables($default); self::$customValues[$this->component] = $custom; } /** * Save the current settings. */ private function save(): void { file_put_contents($this->configFile, Yaml::dump(self::$customValues, 9, 2)); } /** * Read a value from config array. * * @param array $values * The array holding all config values. * @param array $keys * List of keys of how we should dive deep into the value array. * * @return mixed|null * The found value or NULL, if it doesn't exist. */ private function readValueFromArray(array $values, array $keys) { $key = array_shift($keys); if (!isset($values[$key])) { return NULL; } if (empty($keys)) { return $values[$key]; } return $this->readValueFromArray($values[$key], $keys); } /** * Read value from config. * * @param string|array $keys * List of keys of how we should dive deep into the value array. If a top * level value should be read, the key can be provided as a string instead * of an array. * * @return mixed|null * The found value or NULL, if it doesn't exist. */ public function readValue($keys) { if (is_string($keys)) { $keys = [$keys]; } return $this->readValueFromArray(self::$values[$this->component], $keys); } /** * Set a config value. * * @param string $key * The top level key of the value in the config array. * @param mixed $value * The value to be stored. * @param bool $store * Whether the new value should be stored in the config file. */ public function setValue(string $key, $value, bool $store = TRUE): void { $this->merge([], [$key => $value], $store); $this->save(); } /** * Render a Twig template by using config values. * * @param string $filename * The filename of the Twig template. * @param string $content * The content to be rendered. * * @return string * The rendered string. */ public function render(string $filename, string $content): string { $this->twigLoader->setTemplate($filename, $content); try { return $this->twig->render($filename, self::$values[$this->component]); } catch (LoaderError | RuntimeError | SyntaxError $e) { } return ''; } }