Passed
Push — develop ( ad347c...f7c91f )
by Baptiste
02:04
created

Yaml::buildArguments()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
crap 2
1
<?php
2
declare(strict_types = 1);
3
4
namespace Innmind\Compose\Loader;
5
6
use Innmind\Compose\{
7
    Loader,
8
    Loader\PathResolver\Delegate,
9
    Loader\PathResolver\Composer,
10
    Loader\PathResolver\Relative,
11
    Services,
12
    Arguments,
13
    Dependencies,
14
    Definition\Argument,
15
    Definition\Argument\Types,
16
    Definition\Name,
17
    Definition\Service,
18
    Definition\Service\Arguments as ServiceArguments,
19
    Definition\Service\Constructors,
20
    Definition\Dependency,
21
    Definition\Dependency\Parameter,
22
    Exception\DomainException
23
};
24
use Innmind\Url\{
25
    PathInterface,
26
    Path
27
};
28
use Innmind\Immutable\{
29
    Str,
30
    Stream,
31
    Map,
32
    Pair,
33
    Sequence
34
};
35
use Symfony\Component\{
36
    Yaml\Yaml as Lib,
37
    OptionsResolver\OptionsResolver
38
};
39
40
final class Yaml implements Loader
41
{
42
    private const ARGUMENT_PATTERN = '~^(?<optional>\?)?(?<type>.+)( \?\? \$.+)?$~';
43
    private const ARGUMENT_DEFAULT_PATTERN = '~( \?\? \$(?<default>.+))$~';
44
    private const SERVICE_NAME = "~^(?<name>[a-zA-Z0-9]+)[\s ](?<constructor>.+)$~"; //split on space or non breaking space
45
    private const STACK_NAME = "~^(?<name>[a-zA-Z0-9]+)[\s ]stack$~"; //split on space or non breaking space
46
    private const DEPENDENCY_NAME = "~^(?<name>[a-zA-Z0-9]+)[\s ](?<path>.+)$~"; //split on space or non breaking space
47
48
    private $resolver;
49
    private $types;
50
    private $arguments;
51
    private $constructors;
52
    private $stacks;
53
54 6
    public function __construct(
55
        Types $types = null,
56
        ServiceArguments $arguments = null,
57
        Constructors $constructors = null,
58
        PathResolver $pathResolver = null
59
    ) {
60 6
        $this->resolver = new OptionsResolver;
61 6
        $this->resolver->setRequired(['expose', 'services']);
62 6
        $this->resolver->setDefined(['arguments', 'dependencies']);
63 6
        $this->resolver->setAllowedTypes('arguments', 'array');
64 6
        $this->resolver->setAllowedTypes('dependencies', 'array');
65 6
        $this->resolver->setAllowedTypes('expose', 'array');
66 6
        $this->resolver->setAllowedTypes('services', 'array');
67 6
        $this->resolver->setDefault('arguments', []);
68 6
        $this->resolver->setDefault('dependencies', []);
69 6
        $this->types = $types ?? new Types;
70 6
        $this->arguments = $arguments ?? new ServiceArguments;
71 6
        $this->constructors = $constructors ?? new Constructors;
72 6
        $this->resolvePath = $pathResolver ?? new Delegate(
73 6
            new Composer,
74 6
            new Relative
75
        );
76 6
        $this->stacks = new Map(Name::class, Sequence::class);
77 6
    }
78
79 5
    public function __invoke(PathInterface $definition): Services
80
    {
81 5
        $data = Lib::parseFile((string) $definition);
82 5
        $data = $this->resolver->resolve($data);
83
84 5
        $dependencies = $this->buildDependencies(
85 5
            $definition,
86 5
            $data['dependencies']
87
        );
88
89 4
        $this->stacks = $this->stacks->clear();
90
91 4
        $arguments = $this->buildArguments($data['arguments']);
92 4
        $definitions = $this->buildDefinitions(
93 4
            Stream::of('string'),
94 4
            $data['services']
95
        );
96
97 3
        $exposed = Map::of(
98 3
            'string',
99 3
            'string',
100 3
            array_keys($data['expose']),
101 3
            array_values($data['expose'])
102
        );
103
104 3
        $services = new Services(
105 3
            $arguments,
106 3
            $dependencies,
107 3
            ...$definitions->values()
108
        );
109 3
        $services = $this->buildStacks($services);
110
111
        return $exposed
112 3
            ->map(static function(string $as, string $name): Pair {
113 3
                return new Pair(
114 3
                    $as,
115 3
                    (string) Str::of($name)->substring(1) //remove the $ sign
116
                );
117 3
            })
118 3
            ->reduce(
119 3
                $services,
120 3
                static function(Services $services, string $as, string $name): Services {
121 3
                    return $services->expose(
122 3
                        new Name($name),
123 3
                        new Name($as)
124
                    );
125 3
                }
126
            );
127
    }
128
129 4
    private function buildArguments(array $definitions): Arguments
130
    {
131 4
        $arguments = [];
132
133 4
        foreach ($definitions as $name => $type) {
134 3
            $arguments[] = $this->buildArgument($name, Str::of($type)->trim());
135
        }
136
137 4
        return new Arguments(...$arguments);
138
    }
139
140 3
    private function buildArgument(string $name, Str $type): Argument
141
    {
142 3
        if (!$type->matches(self::ARGUMENT_PATTERN)) {
143
            throw new DomainException;
144
        }
145
146 3
        $components = $type->capture(self::ARGUMENT_PATTERN);
147
148 3
        $argument = new Argument(
149 3
            new Name($name),
150 3
            $this->types->load(
151
                $components
152 3
                    ->get('type')
153 3
                    ->pregReplace(
154 3
                        self::ARGUMENT_DEFAULT_PATTERN,
155 3
                        ''
156
                    )
157
            )
158
        );
159
160
        if (
161 3
            $components->contains('optional') &&
162 3
            !$components->get('optional')->empty()
163
        ) {
164 3
            $argument = $argument->makeOptional();
165
        }
166
167 3
        if ($type->matches(self::ARGUMENT_DEFAULT_PATTERN)) {
168 3
            $argument = $argument->defaultsTo(new Name(
169
                (string) $type
170 3
                    ->capture(self::ARGUMENT_DEFAULT_PATTERN)
171 3
                    ->get('default')
172
            ));
173
        }
174
175 3
        return $argument;
176
    }
177
178 4
    private function buildDefinitions(Stream $namespace, array $definitions): Map
179
    {
180 4
        $services = new Map('string', Service::class);
181
182 4
        foreach ($definitions as $key => $value) {
183 4
            $key = Str::of((string) $key);
184
185 4
            if (!is_array($value)) {
186 1
                throw new DomainException(sprintf(
187 1
                    'The key %s doesn\'t contain a valid structure',
188 1
                    $namespace->join('.')
189
                ));
190
            }
191
192 4
            if ($key->matches(self::STACK_NAME)) {
193 3
                $this->registerStack($namespace, $key, $value);
194
195 3
                continue;
196
            }
197
198 4
            if (!$key->matches(self::SERVICE_NAME)) {
199 4
                $services = $services->merge(
200 4
                    $this->buildDefinitions(
201 4
                        $namespace->add((string) $key),
202 4
                        $value
203
                    )
204
                );
205
206 3
                continue;
207
            }
208
209 4
            $service = $this->buildService($namespace, $key, $value);
210 4
            $services = $services->put(
211 4
                (string) $service->name(),
212 4
                $service
213
            );
214
        }
215
216 3
        return $services;
217
    }
218
219 4
    private function buildService(
220
        Stream $namespace,
221
        Str $name,
222
        array $arguments
223
    ): Service {
224 4
        $components = $name->capture(self::SERVICE_NAME);
225
226 4
        foreach ($arguments as &$argument) {
227 3
            $argument = $this->arguments->load($argument);
228
        }
229
230 4
        return new Service(
231 4
            new Name(
232
                (string) $namespace
233 4
                    ->add((string) $components->get('name'))
234 4
                    ->join('.')
235
            ),
236 4
            $this->constructors->load($components->get('constructor')->trim('  ')), //space and non breaking space
237 4
            ...$arguments
238
        );
239
    }
240
241 3
    private function registerStack(Stream $namespace, Str $key, array $stack): void
242
    {
243
        $name = (string) $namespace
244 3
            ->add((string) $key->capture(self::STACK_NAME)->get('name'))
245 3
            ->join('.');
246
247 3
        $this->stacks = $this->stacks->put(
248 3
            new Name($name),
249 3
            Sequence::of(...$stack)->map(static function(string $name): Name {
250 3
                return new Name(
251 3
                    (string) Str::of($name)->substring(1) //remove the $ sign
252
                );
253 3
            })
254
        );
255 3
    }
256
257 3
    private function buildStacks(Services $services): Services
258
    {
259 3
        return $this->stacks->reduce(
260 3
            $services,
261 3
            function(Services $services, Name $name, Sequence $stack): Services {
262 3
                return $services->stack($name, ...$stack);
263 3
            }
264
        );
265
    }
266
267 5
    private function buildDependencies(
268
        PathInterface $origin,
269
        array $dependencies
270
    ): Dependencies {
271 5
        $deps = [];
272
273 5
        foreach ($dependencies as $name => $parameters) {
274 4
            $deps[] = $this->buildDependency($origin, $name, $parameters);
275
        }
276
277 4
        return new Dependencies(...$deps);
278
    }
279
280 4
    private function buildDependency(
281
        PathInterface $origin,
282
        string $name,
283
        array $parameters
284
    ): Dependency {
285 4
        $name = Str::of($name);
286
287 4
        if (!$name->matches(self::DEPENDENCY_NAME)) {
288 1
            throw new DomainException(sprintf(
289 1
                'Invalid dependency name %s',
290 1
                $name
291
            ));
292
        }
293
294 3
        $components = $name->capture(self::DEPENDENCY_NAME);
295 3
        $services = $this(($this->resolvePath)(
296 3
            $origin,
297 3
            new Path((string) $components->get('path'))
298
        ));
299 3
        $params = [];
300
301 3
        foreach ($parameters as $param => $value) {
302 3
            $params[] = Parameter::fromValue(new Name($param), $value);
303
        }
304
305 3
        return new Dependency(
306 3
            new Name((string) $components->get('name')),
307 3
            $services,
308 3
            ...$params
309
        );
310
    }
311
}
312