Lines
97.56%
40 / 41
Functions and Methods
80.00%
4 / 5
Classes and Traits
0.00%
0 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| AppArgumentResolver | 97.56% | 40 / 41 | 80.00% | 4 / 5 | 25 | 0.00% | 0 / 1 | |||
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| resolveArguments | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 5 | |||||
| resolveParameter | 93.75% | 15 / 16 | 0.00% | 0 / 1 | 11.03 | |||||
| findStoreFor | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 4 | |||||
| isBuiltOnHaContext | 100.00% | 8 / 8 | 100.00% | 1 / 1 | 4 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace Stewart\Testing\App; | |
| 6 | ||
| 7 | use ReflectionClass; | |
| 8 | use ReflectionNamedType; | |
| 9 | use ReflectionParameter; | |
| 10 | use Stewart\Contracts\App; | |
| 11 | use Stewart\Contracts\App\AppId; | |
| 12 | use Stewart\Contracts\HaContext; | |
| 13 | use Stewart\Contracts\Store\GlobalStore; | |
| 14 | use Stewart\Contracts\Store\PeerStore; | |
| 15 | use Stewart\Contracts\Store\Store; | |
| 16 | use Stewart\Store\ReadOnlyStore; | |
| 17 | use Stewart\Store\Stores; | |
| 18 | use Stewart\Testing\Exception\HarnessError; | |
| 19 | use Stewart\Testing\Exception\HarnessException; | |
| 20 | ||
| 21 | /** @internal */ | |
| 22 | final readonly class AppArgumentResolver | |
| 23 | { | |
| 24 | /** @param array<string, object> $servicesByType */ | |
| 25 | public function __construct( | |
| 26 | private array $servicesByType, | |
| 27 | private Stores $stores, | |
| 28 | private HaContext $ha, | |
| 29 | ) {} | |
| 30 | ||
| 31 | /** | |
| 32 | * @template T of App | |
| 33 | * @param ReflectionClass<T> $app | |
| 34 | * @param array<string, mixed> $options | |
| 35 | * @return array<string, mixed> | |
| 36 | * @throws HarnessException | |
| 37 | */ | |
| 38 | public function resolveArguments(ReflectionClass $app, array $options): array | |
| 39 | { | |
| 40 | $parameters = $app->getConstructor()?->getParameters() ?? []; | |
| 41 | $parameterNames = array_map(static fn(ReflectionParameter $parameter): string => $parameter->getName(), $parameters); | |
| 42 | ||
| 43 | foreach (array_keys($options) as $option) { | |
| 44 | if (!\in_array($option, $parameterNames, true)) { | |
| 45 | throw HarnessException::createForReason(HarnessError::OptionUnknown, option: $option, class: $app->getName()); | |
| 46 | } | |
| 47 | } | |
| 48 | ||
| 49 | $arguments = []; | |
| 50 | ||
| 51 | foreach ($parameters as $parameter) { | |
| 52 | $arguments[$parameter->getName()] = \array_key_exists($parameter->getName(), $options) | |
| 53 | ? $options[$parameter->getName()] | |
| 54 | : $this->resolveParameter($app, $parameter); | |
| 55 | } | |
| 56 | ||
| 57 | return $arguments; | |
| 58 | } | |
| 59 | ||
| 60 | /** | |
| 61 | * @template T of App | |
| 62 | * @param ReflectionClass<T> $app | |
| 63 | * @throws HarnessException | |
| 64 | */ | |
| 65 | private function resolveParameter(ReflectionClass $app, ReflectionParameter $parameter): mixed | |
| 66 | { | |
| 67 | $type = $parameter->getType(); | |
| 68 | $typeName = $type instanceof ReflectionNamedType ? $type->getName() : null; | |
| 69 | $store = $this->findStoreFor($parameter, $typeName); | |
| 70 | ||
| 71 | if ($store !== null) { | |
| 72 | return $store; | |
| 73 | } | |
| 74 | ||
| 75 | if ($typeName !== null && isset($this->servicesByType[$typeName])) { | |
| 76 | return $this->servicesByType[$typeName]; | |
| 77 | } | |
| 78 | ||
| 79 | if ($typeName !== null && self::isBuiltOnHaContext($typeName)) { | |
| 80 | return new $typeName($this->ha); | |
| 81 | } | |
| 82 | ||
| 83 | if ($parameter->isDefaultValueAvailable()) { | |
| 84 | return $parameter->getDefaultValue(); | |
| 85 | } | |
| 86 | ||
| 87 | if ($type?->allowsNull() === true) { | |
| 88 | return null; | |
| 89 | } | |
| 90 | ||
| 91 | if ($type instanceof ReflectionNamedType && $type->isBuiltin()) { | |
| 92 | throw HarnessException::createForReason(HarnessError::OptionMissing, class: $app->getName(), option: $parameter->getName()); | |
| 93 | } | |
| 94 | ||
| 95 | throw HarnessException::createForReason(HarnessError::ParameterUnresolvable, class: $app->getName(), parameter: $parameter->getName(), type: (string) $type); | |
| 96 | } | |
| 97 | ||
| 98 | private function findStoreFor(ReflectionParameter $parameter, ?string $typeName): ?object | |
| 99 | { | |
| 100 | if ($parameter->getAttributes(GlobalStore::class) !== []) { | |
| 101 | $global = $this->stores->openGlobalStore(); | |
| 102 | ||
| 103 | return $typeName === Store::class ? $global : new ReadOnlyStore($global); | |
| 104 | } | |
| 105 | ||
| 106 | $peer = ($parameter->getAttributes(PeerStore::class)[0] ?? null)?->newInstance(); | |
| 107 | ||
| 108 | return $peer === null ? null : new ReadOnlyStore($this->stores->openAppStore(new AppId($peer->appId))); | |
| 109 | } | |
| 110 | ||
| 111 | // The generated Entities and Services roots take the app's HaContext and nothing else. | |
| 112 | private static function isBuiltOnHaContext(string $typeName): bool | |
| 113 | { | |
| 114 | if (!class_exists($typeName)) { | |
| 115 | return false; | |
| 116 | } | |
| 117 | ||
| 118 | $required = array_values(array_filter( | |
| 119 | new ReflectionClass($typeName)->getConstructor()?->getParameters() ?? [], | |
| 120 | static fn(ReflectionParameter $parameter): bool => !$parameter->isOptional(), | |
| 121 | )); | |
| 122 | $type = ($required[0] ?? null)?->getType(); | |
| 123 | ||
| 124 | return \count($required) === 1 && $type instanceof ReflectionNamedType && $type->getName() === HaContext::class; | |
| 125 | } | |
| 126 | } |