From d973d2f34bb3566a427bed684aa5c9c78399c13b Mon Sep 17 00:00:00 2001 From: Marco Stoll Date: Tue, 19 Jan 2021 15:25:00 +0100 Subject: [PATCH] [FEATURE] [WIP] bootstrapping --- composer.json | 8 +- src/Runtime/Bootstrap.php | 143 ++++++++++++++++++ src/Runtime/Exceptions/BootstrapException.php | 21 +++ src/Runtime/Registry.php | 40 +++++ .../Exceptions/ConfigurationException.php | 2 + .../Exceptions/ResourceInvalidException.php | 21 +++ src/Services/Runtime/ConfigParser.php | 110 ++++++++++++++ tests/Runtime/RegistryTest.php | 32 ++++ tests/Services/Runtime/ConfigParserTest.php | 33 ++++ 9 files changed, 406 insertions(+), 4 deletions(-) create mode 100644 src/Runtime/Bootstrap.php create mode 100644 src/Runtime/Exceptions/BootstrapException.php create mode 100644 src/Runtime/Registry.php create mode 100644 src/Services/Exceptions/ResourceInvalidException.php create mode 100644 src/Services/Runtime/ConfigParser.php create mode 100644 tests/Runtime/RegistryTest.php create mode 100644 tests/Services/Runtime/ConfigParserTest.php diff --git a/composer.json b/composer.json index 8aa460a..29782cb 100644 --- a/composer.json +++ b/composer.json @@ -16,10 +16,10 @@ "php": ">=7.2", "fastforward/data-structures": "^1.0.0", "fastforward/factories": "^1.2.0", - "symfony/config": "~4.3", - "symfony/http-foundation": "~4.3", - "symfony/routing": "~4.3", - "symfony/yaml": "~4.3", + "symfony/config": "~5.2", + "symfony/http-foundation": "~5.2", + "symfony/routing": "~5.2", + "symfony/yaml": "~5.2", "twig/twig": "^2.0" }, "require-dev": { diff --git a/src/Runtime/Bootstrap.php b/src/Runtime/Bootstrap.php new file mode 100644 index 0000000..b83e179 --- /dev/null +++ b/src/Runtime/Bootstrap.php @@ -0,0 +1,143 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Runtime; + +use Composer\Autoload\ClassLoader; +use FF\Factories\ServicesFactory; +use FF\Factories\SF; +use FF\Runtime\Exceptions\BootstrapException; +use FF\Services\Exceptions\ResourceInvalidException; +use FF\Services\Runtime\ConfigParser; + +/** + * Class Bootstrap + * + * @package FF\Runtime + */ +class Bootstrap +{ + /** + * Environment labels + */ + const ENV_PROD = 'production'; + const ENV_DEV = 'development'; + const ENV_TEST = 'testing'; + + /** + * Adds a namespace location to Composer's class loader + * + * @param ClassLoader $loader + * @param string $namespace + * @param string $src + * @param bool $prepend + * @return $this + */ + public function addNamespace(ClassLoader $loader, string $namespace, string $src, bool $prepend = false) + { + $loader->addPsr4(rtrim($namespace, '\\') . '\\', rtrim($src, '/') . '/', $prepend); + return $this; + } + + /** + * Stores initial data in the system-wide registry + * + * Sets keys 'basePath' and 'environment' using the given argument values. + * + * @param string $basePath + * @param string $environment + * @param array $additionalData + * @return $this + */ + public function initRegistry(string $basePath, string $environment = self::ENV_DEV, array $additionalData = []) + { + $data = array_merge([ + 'basePath' => $basePath, + 'environment' => $environment + ], $additionalData); + Registry::getInstance()->setData($data); + return $this; + } + + /** + * Initializes the services factory + * + * If $environment is omitted, tries to retrieve it from the Registry. + * If then an non-empty $environment is present, looks for an environment-specific services configuration + * and merges its values into the given $servicesYml. + * + * @param string $servicesYml + * @param string $environment + * @return $this + * @throws BootstrapException + */ + public function initServiceFactory(string $servicesYml, string $environment = null) + { + if (!is_file($servicesYml) || !is_readable($servicesYml)) { + throw new BootstrapException('services yml [' . $servicesYml . '] not found or not readable'); + } + + try { + $configParser = new ConfigParser(); + + $contents = $configParser->load($servicesYml); + $servicesConfig = $configParser->parse($contents); + + do { + if (empty($environment)) { + break; + } + + $environment = Registry::getInstance()->getField('environment'); + $envYml = $this->buildEnvironmentServicesYmlFileName($servicesYml, $environment); + $envContents = $configParser->load($envYml); + if (empty($envContents)) { + // do nothing if no env-specific config file is present + break; + } + $envConfig = $configParser->parse($envContents); + + $servicesConfig = $configParser->merge($servicesConfig, $envConfig); + } while (false); + } catch (ResourceInvalidException $exception) { + throw new BootstrapException('error while parsing services configuration', 0, $exception); + } + + SF::setInstance(new ServicesFactory($servicesConfig)); + + return $this; + } + + + /** + * Retrieves the file name with the suffix injected + * + * @param string $servicesYml + * @param string $environment + * @param string $delimiter + * @return string + */ + protected function buildEnvironmentServicesYmlFileName( + string $servicesYml, + string $environment, + string $delimiter = '-' + ) { + $pathInfo = pathinfo($servicesYml); + + if (!isset($pathInfo['extension'])) { + return $servicesYml . $delimiter . $environment; + } + + return $pathInfo['dirname'] . DIRECTORY_SEPARATOR + . $pathInfo['filename'] + . $delimiter . $environment + . '.' . $pathInfo['extension']; + } +} diff --git a/src/Runtime/Exceptions/BootstrapException.php b/src/Runtime/Exceptions/BootstrapException.php new file mode 100644 index 0000000..416a6d7 --- /dev/null +++ b/src/Runtime/Exceptions/BootstrapException.php @@ -0,0 +1,21 @@ + + * @link http://core4.de CORE4 GmbH & Co. KG + * @filesource + */ + +namespace FF\Runtime\Exceptions; + +/** + * Class BootstrapException + * + * @package FF\Runtime\Exceptions + */ +class BootstrapException extends \RuntimeException +{ + +} diff --git a/src/Runtime/Registry.php b/src/Runtime/Registry.php new file mode 100644 index 0000000..06a9249 --- /dev/null +++ b/src/Runtime/Registry.php @@ -0,0 +1,40 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Runtime; + +use FF\DataStructures\Record; + +/** + * Class Registry + * + * @package FF\Runtime + */ +class Registry extends Record +{ + /** + * @var Registry + */ + protected static $instance; + + /** + * Retrieves the singleton instance of this class + * + * return Registry + */ + public static function getInstance(): Registry + { + if (is_null(self::$instance)) { + self::$instance = new Registry(); + } + + return self::$instance; + } +} diff --git a/src/Services/Exceptions/ConfigurationException.php b/src/Services/Exceptions/ConfigurationException.php index e591369..8bb337f 100644 --- a/src/Services/Exceptions/ConfigurationException.php +++ b/src/Services/Exceptions/ConfigurationException.php @@ -12,6 +12,8 @@ namespace FF\Services\Exceptions; /** * Class ConfigurationException + * + * @package FF\Services\Exceptions */ class ConfigurationException extends \RuntimeException { diff --git a/src/Services/Exceptions/ResourceInvalidException.php b/src/Services/Exceptions/ResourceInvalidException.php new file mode 100644 index 0000000..3a9675e --- /dev/null +++ b/src/Services/Exceptions/ResourceInvalidException.php @@ -0,0 +1,21 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Services\Exceptions; + +/** + * Class ResourceInvalidException + * + * @package FF\Services\Exceptions + */ +class ResourceInvalidException extends \RuntimeException +{ + +} diff --git a/src/Services/Runtime/ConfigParser.php b/src/Services/Runtime/ConfigParser.php new file mode 100644 index 0000000..f81072b --- /dev/null +++ b/src/Services/Runtime/ConfigParser.php @@ -0,0 +1,110 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Services\Runtime; + +use FF\Services\AbstractService; +use FF\Services\Exceptions\ResourceInvalidException; +use FF\Utils\ArrayUtils; +use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Yaml; + +/** + * Class ConfigParser + * + * @package FF\Services\Runtime + */ +class ConfigParser extends AbstractService +{ + /** + * Loads the contents of a config file + * + * Replaces any occurrence of '<<$key>>', where $key is a key within $replacements. + * + * @param string $file + * @param array $replacements + * @return string|null + */ + public function load(string $file, array $replacements = []): ?string + { + if (!is_file($file) || !is_readable($file)) { + return null; + } + + $contents = file_get_contents($file); + foreach ($replacements as $token => $value) { + $contents = str_replace('<<' . $token . '>>', $value, $contents); + } + + return $contents; + } + + /** + * Parses the contents of a config file + * + * @param string $ymlContents + * @return array + * @throws ResourceInvalidException not valid yml + */ + public function parse(string $ymlContents): array + { + try { + return Yaml::parse($ymlContents); + } catch (ParseException $exception) { + throw new ResourceInvalidException('not valid yml', 0, $exception); + } + } + + /** + * Recursively merges two config arrays + * + * @param array $config1 + * @param array $config2 + * @return array + */ + public function merge(array $config1, array $config2) + { + foreach (array_keys($config2) as $key) { + // test if $key is new to $config1 + if (!array_key_exists($key, $config1)) { + $config1[$key] = $config2[$key]; // copy key and value to $config1 + continue; + } + + // test if either value of $config1 or $config2 is non-array + if (!is_array($config1[$key]) || !is_array($config2[$key])) { + $config1[$key] = $config2[$key]; // replace value in $config1 + continue; + } + + // both values are arrays + $isAssoc1 = ArrayUtils::isAssoc($config1[$key]); + $isAssoc2 = ArrayUtils::isAssoc($config2[$key]); + switch (true) { + case $isAssoc1 != $isAssoc2 : + // array types differ -> replace value in first array + $config1[$key] = $config2[$key]; + break; + case !$isAssoc1 && !$isAssoc2 : + // both numeric arrays -> append second to first + $config1[$key] = array_merge($config1[$key], $config2[$key]); + break; + case $isAssoc1 && $isAssoc2 : + // both associative arrays -> start recursion + $config1[$key] = $this->merge($config1[$key], $config2[$key]); + break; + default : + break; + } + } + + return $config1; + } +} diff --git a/tests/Runtime/RegistryTest.php b/tests/Runtime/RegistryTest.php new file mode 100644 index 0000000..1c09093 --- /dev/null +++ b/tests/Runtime/RegistryTest.php @@ -0,0 +1,32 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Tests\Runtime; + +use FF\Runtime\Registry; +use PHPUnit\Framework\TestCase; + +/** + * Test RegistryTest + * + * @package FF\Tests + */ +class RegistryTest extends TestCase +{ + /** + * Tests the namesake method/feature + */ + public function testGetInstance() + { + $instance = Registry::getInstance(); + $this->assertInstanceOf(Registry::class, $instance); + $this->assertSame($instance, Registry::getInstance()); + } +} diff --git a/tests/Services/Runtime/ConfigParserTest.php b/tests/Services/Runtime/ConfigParserTest.php new file mode 100644 index 0000000..31c1ecb --- /dev/null +++ b/tests/Services/Runtime/ConfigParserTest.php @@ -0,0 +1,33 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +use FF\Services\Runtime\ConfigParser; +use PHPUnit\Framework\TestCase; + +/** + * Test ConfigParserTest + * + * @package FF\Tests + */ +class ConfigParserTest extends TestCase +{ + /** + * @var ConfigParser + */ + protected $uut; + + /** + * {@inheritdoc} + */ + protected function setUp(): void + { + $this->uut = new ConfigParser(); + } +}