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
3declare(strict_types=1);
4
5namespace Stewart\Testing\App;
6
7use ReflectionClass;
8use ReflectionNamedType;
9use ReflectionParameter;
10use Stewart\Contracts\App;
11use Stewart\Contracts\App\AppId;
12use Stewart\Contracts\HaContext;
13use Stewart\Contracts\Store\GlobalStore;
14use Stewart\Contracts\Store\PeerStore;
15use Stewart\Contracts\Store\Store;
16use Stewart\Store\ReadOnlyStore;
17use Stewart\Store\Stores;
18use Stewart\Testing\Exception\HarnessError;
19use Stewart\Testing\Exception\HarnessException;
20
21/** @internal */
22final 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}