Yaml::buildDefinitions()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 39
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 21
nc 5
nop 2
dl 0
loc 39
ccs 22
cts 22
cp 1
crap 5
rs 9.2728
c 0
b 0
f 0
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 10
    public function __construct(
55
        Types $types = null,
56
        ServiceArguments $arguments = null,
57
        Constructors $constructors = null,
58
        PathResolver $pathResolver = null
59
    ) {
60 10
        $this->resolver = new OptionsResolver;
61 10
        $this->resolver->setRequired(['expose', 'services']);
62 10
        $this->resolver->setDefined(['arguments', 'dependencies']);
63 10
        $this->resolver->setAllowedTypes('arguments', 'array');
64 10
        $this->resolver->setAllowedTypes('dependencies', 'array');
65 10
        $this->resolver->setAllowedTypes('expose', 'array');
66 10
        $this->resolver->setAllowedTypes('services', 'array');
67 10
        $this->resolver->setDefault('arguments', []);
68 10
        $this->resolver->setDefault('dependencies', []);
69 10
        $this->types = $types ?? new Types;
70 10
        $this->arguments = $arguments ?? new ServiceArguments;
71 10
        $this->constructors = $constructors ?? new Constructors;
72 10
        $this->resolvePath = $pathResolver ?? new Delegate(
0 ignored issues
show
Bug Best Practice introduced by
The property resolvePath does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
73 10
            new Composer,
74 10
            new Relative
75
        );
76 10
        $this->stacks = new Map(Name::class, Sequence::class);
77 10
    }
78
79 9
    public function __invoke(PathInterface $definition): Services
80
    {
81 9
        $data = Lib::parseFile((string) $definition);
82 9
        $data = $this->resolver->resolve($data);
83
84 9
        $dependencies = $this->buildDependencies(
85 9
            $definition,
86 9
            $data['dependencies']
87
        );
88
89 8
        $this->stacks = $this->stacks->clear();
90
91 8
        $arguments = $this->buildArguments($data['arguments']);
92 8
        $definitions = $this->buildDefinitions(
93 8
            Stream::of('string'),
94 8
            $data['services']
95
        );
96
97 7
        $exposed = Map::of(
98 7
            'string',
99 7
            'string',
100 7
            array_keys($data['expose']),
101 7
            array_values($data['expose'])
102
        );
103
104 7
        $services = new Services(
105 7
            $arguments,
106 7
            $dependencies,
107 7
            ...$definitions->values()
108
        );
109 7
        $services = $this->buildStacks($services);
110
111
        return $exposed
112 7
            ->map(static function(string $as, string $name): Pair {
113 7
                return new Pair(
114 7
                    $as,
115 7
                    (string) Str::of($name)->substring(1) //remove the $ sign
116
                );
117 7
            })
118 7
            ->reduce(
119 7
                $services,
120 7
                static function(Services $services, string $as, string $name): Services {
121 7
                    return $services->expose(
122 7
                        new Name($name),
123 7
                        new Name($as)
124
                    );
125 7
                }
126
            );
127
    }
128
129 8
    private function buildArguments(array $definitions): Arguments
130
    {
131 8
        $arguments = [];
132
133 8
        foreach ($definitions as $name => $type) {
134 7
            $arguments[] = $this->buildArgument($name, Str::of($type)->trim());
135
        }
136
137 8
        return new Arguments(...$arguments);
138
    }
139
140 7
    private function buildArgument(string $name, Str $type): Argument
141
    {
142 7
        if (!$type->matches(self::ARGUMENT_PATTERN)) {
143
            throw new DomainException;
144
        }
145
146 7
        $components = $type->capture(self::ARGUMENT_PATTERN);
147
148 7
        $argument = new Argument(
149 7
            new Name($name),
150 7
            $this->types->load(
151
                $components
152 7
                    ->get('type')
153 7
                    ->pregReplace(
154 7
                        self::ARGUMENT_DEFAULT_PATTERN,
155 7
                        ''
156
                    )
157
            )
158
        );
159
160
        if (
161 7
            $components->contains('optional') &&
162 7
            !$components->get('optional')->empty()
163
        ) {
164 7
            $argument = $argument->makeOptional();
165
        }
166
167 7
        if ($type->matches(self::ARGUMENT_DEFAULT_PATTERN)) {
168 7
            $argument = $argument->defaultsTo(new Name(
169
                (string) $type
170 7
                    ->capture(self::ARGUMENT_DEFAULT_PATTERN)
171 7
                    ->get('default')
172
            ));
173
        }
174
175 7
        return $argument;
176
    }
177
178 8
    private function buildDefinitions(Stream $namespace, array $definitions): Map
179
    {
180 8
        $services = new Map('string', Service::class);
181
182 8
        foreach ($definitions as $key => $value) {
183 8
            $key = Str::of((string) $key);
184
185 8
            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 8
            if ($key->matches(self::STACK_NAME)) {
193 7
                $this->registerStack($namespace, $key, $value);
194
195 7
                continue;
196
            }
197
198 8
            if (!$key->matches(self::SERVICE_NAME)) {
199 8
                $services = $services->merge(
200 8
                    $this->buildDefinitions(
201 8
                        $namespace->add((string) $key),
202 8
                        $value
203
                    )
204
                );
205
206 7
                continue;
207
            }
208
209 8
            $service = $this->buildService($namespace, $key, $value);
210 8
            $services = $services->put(
211 8
                (string) $service->name(),
212 8
                $service
213
            );
214
        }
215
216 7
        return $services;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $services could return the type Innmind\Immutable\MapInterface which includes types incompatible with the type-hinted return Innmind\Immutable\Map. Consider adding an additional type-check to rule them out.
Loading history...
217
    }
218
219 8
    private function buildService(
220
        Stream $namespace,
221
        Str $name,
222
        array $arguments
223
    ): Service {
224 8
        $components = $name->capture(self::SERVICE_NAME);
225
226 8
        foreach ($arguments as &$argument) {
227 7
            $argument = $this->arguments->load($argument);
228
        }
229
230 8
        return new Service(
231 8
            new Name(
232
                (string) $namespace
233 8
                    ->add((string) $components->get('name'))
234 8
                    ->join('.')
235
            ),
236 8
            $this->constructors->load($components->get('constructor')->trim('  ')), //space and non breaking space
237 8
            ...$arguments
238
        );
239
    }
240
241 7
    private function registerStack(Stream $namespace, Str $key, array $stack): void
242
    {
243
        $name = (string) $namespace
244 7
            ->add((string) $key->capture(self::STACK_NAME)->get('name'))
245 7
            ->join('.');
246
247 7
        $this->stacks = $this->stacks->put(
248 7
            new Name($name),
249 7
            Sequence::of(...$stack)->map(static function(string $name): Name {
250 7
                return new Name(
251 7
                    (string) Str::of($name)->substring(1) //remove the $ sign
252
                );
253 7
            })
254
        );
255 7
    }
256
257 7
    private function buildStacks(Services $services): Services
258
    {
259 7
        return $this->stacks->reduce(
260 7
            $services,
261 7
            function(Services $services, Name $name, Sequence $stack): Services {
262 7
                return $services->stack($name, ...$stack);
0 ignored issues
show
Bug introduced by
The call to Innmind\Compose\Services::stack() has too few arguments starting with lower. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

262
                return $services->/** @scrutinizer ignore-call */ stack($name, ...$stack);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
263 7
            }
264
        );
265
    }
266
267 9
    private function buildDependencies(
268
        PathInterface $origin,
269
        array $dependencies
270
    ): Dependencies {
271 9
        $deps = [];
272
273 9
        foreach ($dependencies as $name => $parameters) {
274 8
            $deps[] = $this->buildDependency($origin, $name, $parameters);
275
        }
276
277 8
        return new Dependencies(...$deps);
278
    }
279
280 8
    private function buildDependency(
281
        PathInterface $origin,
282
        string $name,
283
        array $parameters
284
    ): Dependency {
285 8
        $name = Str::of($name);
286
287 8
        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 7
        $components = $name->capture(self::DEPENDENCY_NAME);
295 7
        $services = $this(($this->resolvePath)(
296 7
            $origin,
297 7
            new Path((string) $components->get('path'))
298
        ));
299 7
        $params = [];
300
301 7
        foreach ($parameters as $param => $value) {
302 7
            $params[] = Parameter::fromValue(new Name($param), $value);
303
        }
304
305 7
        return new Dependency(
306 7
            new Name((string) $components->get('name')),
307 7
            $services,
308 7
            ...$params
309
        );
310
    }
311
}
312