Skip to content

Commit

Permalink
refactor(core,graphql,serializer,spa): Component Config injection (#197)
Browse files Browse the repository at this point in the history
Issue: #152
  • Loading branch information
LastDragon-ru authored Nov 15, 2024
2 parents a381dd0 + cb67fd9 commit f10bb33
Show file tree
Hide file tree
Showing 65 changed files with 1,739 additions and 669 deletions.
110 changes: 110 additions & 0 deletions packages/core/src/Application/Configuration/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\Core\Application\Configuration;

use ArrayAccess;
use Illuminate\Support\Str;
use LogicException;
use Override;
use ReflectionNamedType;
use ReflectionProperty;

use function array_is_list;
use function is_a;
use function is_array;
use function is_string;
use function sprintf;
use function str_contains;

/**
* @implements ArrayAccess<string, mixed>
*/
abstract class Configuration implements ArrayAccess {
protected function __construct() {
// empty
}

/**
* @param array<string, mixed> $array
*/
public static function __set_state(array $array): static {
return new static(...$array); // @phpstan-ignore new.static (this is developer responsibility)
}

#[Override]
public function offsetExists(mixed $offset): bool {
throw new LogicException('Not supported.');
}

#[Override]
public function offsetGet(mixed $offset): mixed {
throw new LogicException('Not supported.');
}

#[Override]
public function offsetSet(mixed $offset, mixed $value): void {
throw new LogicException('Not supported.');
}

#[Override]
public function offsetUnset(mixed $offset): void {
throw new LogicException('Not supported.');
}

/**
* @deprecated %{VERSION} Array-based config is deprecated. Please migrate to object-based config.
*
* @param array<array-key, mixed> $array
*/
public static function fromArray(array $array): static {
$properties = [];

foreach ($array as $key => $value) {
$property = static::fromArrayGetPropertyName($key);
$properties[$property] = static::fromArrayGetPropertyValue($property, $value);
}

return static::__set_state($properties);
}

/**
* @deprecated %{VERSION}
*/
protected static function fromArrayGetPropertyName(int|string $property): string {
if (!is_string($property)) {
throw new LogicException(
sprintf(
'The `%s::$%s` is not a valid property name.',
static::class,
$property,
),
);
}

if (str_contains($property, '_')) {
$property = Str::camel($property);
}

return $property;
}

/**
* @deprecated %{VERSION}
*/
protected static function fromArrayGetPropertyValue(string $property, mixed $value): mixed {
if (is_array($value) && (!array_is_list($value) || $value === [])) {
$property = new ReflectionProperty(static::class, $property);
$type = $property->getType();

if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$name = $type->getName();

if (is_a($name, self::class, true)) {
$value = $name::fromArray($value);
}
}
}

return $value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\Core\Application\Configuration;

use LastDragon_ru\LaraASP\Core\Application\ConfigResolver;
use LastDragon_ru\LaraASP\Core\Application\Resolver;

/**
* @template TConfiguration of Configuration
*
* @extends Resolver<TConfiguration>
*/
abstract class ConfigurationResolver extends Resolver {
public function __construct(ConfigResolver $config) {
parent::__construct(
static function () use ($config): Configuration {
/** @var TConfiguration $configuration */
$configuration = $config->getInstance()->get(static::getName());

return $configuration;
},
);
}

abstract protected static function getName(): string;

/**
* @return TConfiguration
*/
abstract public static function getDefaultConfig(): Configuration;
}
103 changes: 103 additions & 0 deletions packages/core/src/Application/Configuration/ConfigurationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\Core\Application\Configuration;

use LastDragon_ru\LaraASP\Core\Testing\Package\TestCase;
use LogicException;
use PHPUnit\Framework\Attributes\CoversClass;

/**
* @internal
*/
#[CoversClass(Configuration::class)]
final class ConfigurationTest extends TestCase {
public function testSetState(): void {
$expected = new ConfigurationTest_ConfigurationA();
$expected->a = 321;
$expected->b = new ConfigurationTest_ConfigurationB();
$actual = ConfigurationTest_ConfigurationA::__set_state([
'a' => 321,
'b' => new ConfigurationTest_ConfigurationB(),
]);

self::assertEquals($expected, $actual);
}

public function testOffsetGet(): void {
self::expectException(LogicException::class);

$config = new ConfigurationTest_ConfigurationA();

$config['a']; // @phpstan-ignore expr.resultUnused (for test)
}

public function testOffsetExists(): void {
self::expectException(LogicException::class);

$config = new ConfigurationTest_ConfigurationA();

isset($config['a']); // @phpstan-ignore expr.resultUnused (for test)
}

public function testOffsetUnset(): void {
self::expectException(LogicException::class);

$config = new ConfigurationTest_ConfigurationA();

unset($config['a']);
}

public function testOffsetSet(): void {
self::expectException(LogicException::class);

$config = new ConfigurationTest_ConfigurationA();

$config['a'] = 123;
}

public function testFromArray(): void {
$expected = new ConfigurationTest_ConfigurationA();
$expected->a = 321;
$expected->b = new ConfigurationTest_ConfigurationB();
$expected->b->b = true;
$expected->b->bA = 'cba';
$actual = ConfigurationTest_ConfigurationA::fromArray([
'a' => 321,
'b' => [
'b' => true,
'b_a' => 'cba',
],
]);

self::assertEquals($expected, $actual);
}
}

// @phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses
// @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps

/**
* @internal
* @noinspection PhpMultipleClassesDeclarationsInOneFile
*/
class ConfigurationTest_ConfigurationA extends Configuration {
public function __construct(
public int $a = 123,
public ?ConfigurationTest_ConfigurationB $b = null,
) {
parent::__construct();
}
}

/**
* @internal
* @noinspection PhpMultipleClassesDeclarationsInOneFile
*/
class ConfigurationTest_ConfigurationB extends Configuration {
public function __construct(
public bool $b = false,
public string $bA = 'abc',
) {
parent::__construct();
}
}
63 changes: 63 additions & 0 deletions packages/core/src/Provider/WithConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,32 @@
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Foundation\CachesConfiguration;
use Illuminate\Support\ServiceProvider;
use LastDragon_ru\LaraASP\Core\Application\Configuration\Configuration;
use LastDragon_ru\LaraASP\Core\Application\Configuration\ConfigurationResolver;
use LastDragon_ru\LaraASP\Core\Package;
use LastDragon_ru\LaraASP\Core\Utils\ConfigMerger;

use function is_array;
use function trigger_deprecation;

/**
* @see Configuration
*
* @phpstan-require-extends ServiceProvider
*/
trait WithConfig {
use Helper;

/**
* @deprecated %{VERSION} Please migrate to {@see self::registerConfig()} and object-based config.
*/
protected function bootConfig(): void {
trigger_deprecation(
Package::Name,
'%{VERSION}',
'Please migrate to `self::registerConfig()` and object-based config.',
);

$package = $this->getName();
$path = $this->getPath('../defaults/config.php');

Expand All @@ -35,4 +52,50 @@ protected function loadConfigFrom(string $path, string $key): void {
]);
}
}

/**
* @template C of Configuration
* @template T of ConfigurationResolver<C>
*
* @param class-string<T> $resolver
*/
protected function registerConfig(string $resolver): void {
$package = $this->getName();

$this->app->singletonIf($resolver);
$this->loadPackageConfig($resolver);
$this->publishes([
$this->getPath('../defaults/config.php') => $this->app->configPath("{$package}.php"),
], 'config');
}

/**
* @param class-string<ConfigurationResolver<covariant Configuration>> $resolver
*/
private function loadPackageConfig(string $resolver): void {
if (!($this->app instanceof CachesConfiguration && $this->app->configurationIsCached())) {
$repository = $this->app->make(Repository::class);
$package = $this->getName();
$current = $repository->get($package, null);

if ($current === null) {
$repository->set([
$package => $resolver::getDefaultConfig(),
]);
} elseif (is_array($current)) {
// todo(core): Remove somewhere in v9 or later.
trigger_deprecation(
Package::Name,
'%{VERSION}',
'Array-based config is deprecated. Please migrate to object-based config.',
);

$repository->set([
$package => $resolver::getDefaultConfig()::fromArray((new ConfigMerger())->merge($current, [])),
]);
} else {
// empty
}
}
}
}
Loading

0 comments on commit f10bb33

Please sign in to comment.