[FEATURE] [WIP] bootstrapping

This commit is contained in:
Marco Stoll 2021-01-19 15:25:00 +01:00
parent 68e7f0a4ec
commit d973d2f34b
9 changed files with 406 additions and 4 deletions

View File

@ -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": {

143
src/Runtime/Bootstrap.php Normal file
View File

@ -0,0 +1,143 @@
<?php
/**
* Definition of Bootstrap
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @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'];
}
}

View File

@ -0,0 +1,21 @@
<?php
/**
* Class BootstrapException
*
* @package FF\Services\Exceptions
* @author Marco Stoll <m.stoll@core4.de>
* @link http://core4.de CORE4 GmbH & Co. KG
* @filesource
*/
namespace FF\Runtime\Exceptions;
/**
* Class BootstrapException
*
* @package FF\Runtime\Exceptions
*/
class BootstrapException extends \RuntimeException
{
}

40
src/Runtime/Registry.php Normal file
View File

@ -0,0 +1,40 @@
<?php
/**
* Definition of Registry
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @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;
}
}

View File

@ -12,6 +12,8 @@ namespace FF\Services\Exceptions;
/**
* Class ConfigurationException
*
* @package FF\Services\Exceptions
*/
class ConfigurationException extends \RuntimeException
{

View File

@ -0,0 +1,21 @@
<?php
/**
* Definition of ResourceInvalidException
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Services\Exceptions;
/**
* Class ResourceInvalidException
*
* @package FF\Services\Exceptions
*/
class ResourceInvalidException extends \RuntimeException
{
}

View File

@ -0,0 +1,110 @@
<?php
/**
* Definition of ConfigParser
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @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;
}
}

View File

@ -0,0 +1,32 @@
<?php
/**
* Definition of RegistryTest
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @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());
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* Definition of ConfigParserTest
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @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();
}
}