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