diff --git a/src/Bootstrap.php b/src/Bootstrap.php new file mode 100644 index 00000000..079bc7d8 --- /dev/null +++ b/src/Bootstrap.php @@ -0,0 +1,75 @@ +container; + unset($this->container); + + return $c; + } + + /** + * @param non-empty-string|null $xml File or XML content + */ + public function withConfig( + ?string $xml = null, + array $inputOptions = [], + array $inputArguments = [], + array $environment = [], + ): self { + $args = [ + 'env' => $environment, + 'inputArguments' => $inputArguments, + 'inputOptions' => $inputOptions, + ]; + + // XML config file + $xml === null or $args['xml'] = $this->readXml($xml); + + // Register bindings + $this->container->bind(ConfigLoader::class, $args); + + return $this; + } + + private function readXml(string $fileOrContent): string + { + // Load content + if (\str_starts_with($fileOrContent, 'createRegistry($output); - $container = new Container(); + $container = Bootstrap::init()->withConfig( + xml: \dirname(__DIR__, 2) . '/trap.xml', + inputOptions: $input->getOptions(), + inputArguments: $input->getArguments(), + environment: \getenv(), + )->finish(); $container->set($registry); $container->set($input, InputInterface::class); $container->set(new Logger($output)); diff --git a/src/Config/Server/Frontend.php b/src/Config/Server/Frontend.php index 76ad55bb..8ad3bb60 100644 --- a/src/Config/Server/Frontend.php +++ b/src/Config/Server/Frontend.php @@ -4,7 +4,7 @@ namespace Buggregator\Trap\Config\Server; -use Buggregator\Trap\Service\Config\CliOption; +use Buggregator\Trap\Service\Config\InputOption; use Buggregator\Trap\Service\Config\Env; use Buggregator\Trap\Service\Config\XPath; @@ -15,7 +15,7 @@ final class Frontend { /** @var int<1, 65535> */ #[Env('TRAP_FRONTEND_PORT')] - #[CliOption('ui')] + #[InputOption('ui')] #[XPath('/trap/frontend/@port')] public int $port = 8000; } diff --git a/src/Service/Config/ConfigLoader.php b/src/Service/Config/ConfigLoader.php index 5b6b1196..625c6bc5 100644 --- a/src/Service/Config/ConfigLoader.php +++ b/src/Service/Config/ConfigLoader.php @@ -5,7 +5,6 @@ namespace Buggregator\Trap\Service\Config; use Buggregator\Trap\Logger; -use Symfony\Component\Console\Input\InputInterface; /** * @internal @@ -15,31 +14,23 @@ final class ConfigLoader private \SimpleXMLElement|null $xml = null; /** - * @param null|callable(): non-empty-string $xmlProvider * @psalm-suppress RiskyTruthyFalsyComparison */ public function __construct( - private Logger $logger, - private ?InputInterface $cliInput, - ?callable $xmlProvider = null, - ) - { - // Check SimpleXML extension - if (!\extension_loaded('simplexml')) { - return; - } - - try { - $xml = $xmlProvider === null - ? \file_get_contents(\dirname(__DIR__, 3) . '/trap.xml') - : $xmlProvider(); - } catch (\Throwable) { - return; + private readonly Logger $logger, + private readonly array $env = [], + private readonly array $inputArguments = [], + private readonly array $inputOptions = [], + ?string $xml = null, + ) { + if (\is_string($xml)) { + // Check SimpleXML extension + if (!\extension_loaded('simplexml')) { + $logger->info('SimpleXML extension is not loaded.'); + } else { + $this->xml = \simplexml_load_string($xml, options: \LIBXML_NOERROR) ?: null; + } } - - $this->xml = \is_string($xml) - ? (\simplexml_load_string($xml, options: \LIBXML_NOERROR) ?: null) - : null; } public function hidrate(object $config): void @@ -69,8 +60,9 @@ private function injectValue(object $config, \ReflectionProperty $property, arra /** @var mixed $value */ $value = match (true) { $attribute instanceof XPath => $this->xml?->xpath($attribute->path)[$attribute->key], - $attribute instanceof Env => \getenv($attribute->name) === false ? null : \getenv($attribute->name), - $attribute instanceof CliOption => $this->cliInput?->getOption($attribute->name), + $attribute instanceof Env => $this->env[$attribute->name] ?? null, + $attribute instanceof InputOption => $this->inputOptions[$attribute->name] ?? null, + $attribute instanceof InputArgument => $this->inputArguments[$attribute->name] ?? null, default => null, }; diff --git a/src/Service/Config/InputArgument.php b/src/Service/Config/InputArgument.php new file mode 100644 index 00000000..ff863541 --- /dev/null +++ b/src/Service/Config/InputArgument.php @@ -0,0 +1,17 @@ +factory[$class] ?? null; - $result = match(true) { - $binding === null => $this->injector->make($class, $arguments), - \is_array($binding) => $this->injector->make($class, \array_merge($binding, $arguments)), - default => ($this->factory[$class])($this), - }; + if ($binding instanceof \Closure) { + $result = $binding($this); + } else { + try { + $result = $this->injector->make($class, \array_merge((array) $binding, $arguments)); + } catch (\Throwable $e) { + throw new class(previous: $e) extends \RuntimeException implements NotFoundExceptionInterface {}; + } + } \assert($result instanceof $class, "Created object must be instance of {$class}."); // Detect Trap related types - // Configs if (\str_starts_with($class, 'Buggregator\\Trap\\Config\\')) { // Hydrate config diff --git a/tests/Unit/Service/Config/ConfigLoaderTest.php b/tests/Unit/Service/Config/ConfigLoaderTest.php index b33ed0e4..781ceb07 100644 --- a/tests/Unit/Service/Config/ConfigLoaderTest.php +++ b/tests/Unit/Service/Config/ConfigLoaderTest.php @@ -4,8 +4,11 @@ namespace Buggregator\Trap\Tests\Unit\Service\Config; -use Buggregator\Trap\Logger; +use Buggregator\Trap\Bootstrap; use Buggregator\Trap\Service\Config\ConfigLoader; +use Buggregator\Trap\Service\Config\Env; +use Buggregator\Trap\Service\Config\InputArgument; +use Buggregator\Trap\Service\Config\InputOption; use Buggregator\Trap\Service\Config\XPath; use PHPUnit\Framework\TestCase; @@ -22,8 +25,9 @@ public function testSimpleHydration(): void public string $myString; #[XPath('/trap/container/MyFloat/@value')] public float $myFloat; + #[XPath('/trap/container/Nothing/@value')] + public float $none = 3.14; }; - $xml = <<<'XML' @@ -34,12 +38,58 @@ public function testSimpleHydration(): void XML; - $loader = new ConfigLoader(new Logger(), null, fn() => $xml); - $loader->hidrate($dto); + $this->createConfigLoader(xml: $xml)->hidrate($dto); self::assertTrue($dto->myBool); self::assertSame(200, $dto->myInt); self::assertSame('foo-bar', $dto->myString); self::assertSame(42.0, $dto->myFloat); + self::assertSame(3.14, $dto->none); + } + + public function testAttributesOrder(): void + { + $dto = new class() { + #[XPath('/test/@foo')] + #[InputArgument('test')] + #[InputOption('test')] + #[Env('test')] + public int $int1; + #[Env('test')] + #[InputArgument('test')] + #[XPath('/test/@foo')] + #[InputOption('test')] + public int $int2; + #[InputArgument('test')] + #[Env('test')] + #[XPath('/test/@foo')] + #[InputOption('test')] + public int $int3; + }; + $xml = <<<'XML' + + + + XML; + + $this + ->createConfigLoader(xml: $xml, opts: ['test' => 13], args: ['test' => 69], env: ['test' => 0]) + ->hidrate($dto); + + self::assertSame(42, $dto->int1); + self::assertSame(0, $dto->int2); + self::assertSame(69, $dto->int3); + } + + private function createConfigLoader( + ?string $xml = null, + array $opts = [], + array $args = [], + array $env = [], + ): ConfigLoader { + return Bootstrap::init() + ->withConfig($xml, $opts, $args, $env) + ->finish() + ->get(ConfigLoader::class); } }