Skip to content

Commit

Permalink
Add bootstrap and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
roxblnfk committed May 1, 2024
1 parent e2c9998 commit 1fc81b1
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 39 deletions.
75 changes: 75 additions & 0 deletions src/Bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Buggregator\Trap;

use Buggregator\Trap\Service\Config\ConfigLoader;
use Buggregator\Trap\Service\Container;

/**
* Build the container based on the configuration.
*
* @internal
*/
final class Bootstrap
{
private function __construct(
private Container $container,
) {
}

public static function init(Container $container = new Container()): self
{
return new self($container);
}

public function finish(): Container
{
$c = $this->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, '<?xml')) {
$xml = $fileOrContent;
} else {
\file_exists($fileOrContent) or throw new \InvalidArgumentException('Config file not found.');
$xml = \file_get_contents($fileOrContent);
$xml === false and throw new \RuntimeException('Failed to read config file.');
}

// Validate Schema
// todo

return $xml;
}
}
9 changes: 7 additions & 2 deletions src/Command/Run.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
namespace Buggregator\Trap\Command;

use Buggregator\Trap\Application;
use Buggregator\Trap\Bootstrap;
use Buggregator\Trap\Config\Server\SocketServer;
use Buggregator\Trap\Info;
use Buggregator\Trap\Logger;
use Buggregator\Trap\Sender;
use Buggregator\Trap\Service\Container;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\SignalableCommandInterface;
Expand Down Expand Up @@ -83,7 +83,12 @@ protected function execute(

$registry = $this->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));
Expand Down
4 changes: 2 additions & 2 deletions src/Config/Server/Frontend.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}
40 changes: 16 additions & 24 deletions src/Service/Config/ConfigLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace Buggregator\Trap\Service\Config;

use Buggregator\Trap\Logger;
use Symfony\Component\Console\Input\InputInterface;

/**
* @internal
Expand All @@ -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
Expand Down Expand Up @@ -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,
};

Expand Down
17 changes: 17 additions & 0 deletions src/Service/Config/InputArgument.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Buggregator\Trap\Service\Config;

/**
* @internal
*/
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class InputArgument implements ConfigAttribute
{
public function __construct(
public string $name,
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* @internal
*/
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class CliOption implements ConfigAttribute
final class InputOption implements ConfigAttribute
{
public function __construct(
public string $name,
Expand Down
16 changes: 10 additions & 6 deletions src/Service/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Buggregator\Trap\Destroyable;
use Buggregator\Trap\Service\Config\ConfigLoader;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Yiisoft\Injector\Injector;

/**
Expand Down Expand Up @@ -81,16 +82,19 @@ public function make(string $class, array $arguments = []): object
{
$binding = $this->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
Expand Down
58 changes: 54 additions & 4 deletions tests/Unit/Service/Config/ConfigLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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'
<?xml version="1.0"?>
<trap my-string="foo-bar">
Expand All @@ -34,12 +38,58 @@ public function testSimpleHydration(): void
</trap>
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 version="1.0"?>
<test foo="42">
</test>
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);
}
}

0 comments on commit 1fc81b1

Please sign in to comment.