Passed
Branch develop (e0de4e)
by Baptiste
01:53
created

Yaml::buildStacks()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
crap 1
1
<?php
2
declare(strict_types = 1);
3
4
namespace Innmind\Compose\Loader;
5
6
use Innmind\Compose\{
7
    Loader,
8
    Definitions,
9
    Arguments,
10
    Definition\Argument,
11
    Definition\Argument\Types,
12
    Definition\Name,
13
    Definition\Service,
14
    Definition\Service\Arguments as ServiceArguments,
15
    Definition\Service\Constructors,
16
    Exception\DomainException
17
};
18
use Innmind\Url\PathInterface;
19
use Innmind\Immutable\{
20
    Str,
21
    Stream,
22
    Map,
23
    Pair,
24
    Sequence
25
};
26
use Symfony\Component\{
27
    Yaml\Yaml as Lib,
28
    OptionsResolver\OptionsResolver
29
};
30
31
final class Yaml implements Loader
32
{
33
    private const ARGUMENT_PATTERN = '~^(?<optional>\?)?(?<type>.+)( \?\? \$.+)?$~';
34
    private const ARGUMENT_DEFAULT_PATTERN = '~( \?\? \$(?<default>.+))$~';
35
    private const SERVICE_NAME = "~^(?<name>[a-zA-Z0-9]+)[\s ](?<constructor>.+)$~"; //split on space or non breaking space
36
    private const STACK_NAME = "~^(?<name>[a-zA-Z0-9]+)[\s ]stack$~"; //split on space or non breaking space
37
38
    private $resolver;
39
    private $types;
40
    private $arguments;
41
    private $constructors;
42
    private $stacks;
43
44 3
    public function __construct(
45
        Types $types = null,
46
        ServiceArguments $arguments = null,
47
        Constructors $constructors = null
48
    ) {
49 3
        $this->resolver = new OptionsResolver;
50 3
        $this->resolver->setRequired(['expose', 'services']);
51 3
        $this->resolver->setDefined('arguments');
52 3
        $this->resolver->setAllowedTypes('arguments', 'array');
53 3
        $this->resolver->setAllowedTypes('expose', 'array');
54 3
        $this->resolver->setAllowedTypes('services', 'array');
55 3
        $this->resolver->setDefault('arguments', []);
56 3
        $this->types = $types ?? new Types;
57 3
        $this->arguments = $arguments ?? new ServiceArguments;
58 3
        $this->constructors = $constructors ?? new Constructors;
59 3
        $this->stacks = new Map(Name::class, Sequence::class);
60 3
    }
61
62 2
    public function __invoke(PathInterface $definition): Definitions
63
    {
64 2
        $this->stacks->clear();
65
66 2
        $data = Lib::parseFile((string) $definition);
67 2
        $data = $this->resolver->resolve($data);
68
69 2
        $arguments = $this->buildArguments($data['arguments']);
70 2
        $definitions = $this->buildDefinitions(
71 2
            Stream::of('string'),
72 2
            $data['services']
73
        );
74
75 2
        $exposed = Map::of(
76 2
            'string',
77 2
            'string',
78 2
            array_keys($data['expose']),
79 2
            array_values($data['expose'])
80
        );
81
82 2
        $definitions = new Definitions(
83 2
            $arguments,
84 2
            ...$definitions->values()
85
        );
86 2
        $definitions = $this->buildStacks($definitions);
87
88
        return $exposed
89 2
            ->map(static function(string $as, string $name): Pair {
90 2
                return new Pair(
91 2
                    $as,
92 2
                    (string) Str::of($name)->substring(1) //remove the $ sign
93
                );
94 2
            })
95 2
            ->reduce(
96 2
                $definitions,
97 2
                static function(Definitions $definitions, string $as, string $name): Definitions {
98 2
                    return $definitions->expose(
99 2
                        new Name($name),
100 2
                        new Name($as)
101
                    );
102 2
                }
103
            );
104
    }
105
106 2
    private function buildArguments(array $definitions): Arguments
107
    {
108 2
        $arguments = [];
109
110 2
        foreach ($definitions as $name => $type) {
111 2
            $arguments[] = $this->buildArgument($name, Str::of($type)->trim());
112
        }
113
114 2
        return new Arguments(...$arguments);
115
    }
116
117 2
    private function buildArgument(string $name, Str $type): Argument
118
    {
119 2
        if (!$type->matches(self::ARGUMENT_PATTERN)) {
120
            throw new DomainException;
121
        }
122
123 2
        $components = $type->capture(self::ARGUMENT_PATTERN);
124
125 2
        $argument = new Argument(
126 2
            new Name($name),
127 2
            $this->types->load(
128
                $components
129 2
                    ->get('type')
130 2
                    ->pregReplace(
131 2
                        self::ARGUMENT_DEFAULT_PATTERN,
132 2
                        ''
133
                    )
134
            )
135
        );
136
137
        if (
138 2
            $components->contains('optional') &&
139 2
            !$components->get('optional')->empty()
140
        ) {
141 2
            $argument = $argument->makeOptional();
142
        }
143
144 2
        if ($type->matches(self::ARGUMENT_DEFAULT_PATTERN)) {
145 2
            $argument = $argument->defaultsTo(new Name(
146
                (string) $type
147 2
                    ->capture(self::ARGUMENT_DEFAULT_PATTERN)
148 2
                    ->get('default')
149
            ));
150
        }
151
152 2
        return $argument;
153
    }
154
155 2
    private function buildDefinitions(Stream $namespace, array $definitions): Map
156
    {
157 2
        $services = new Map('string', Service::class);
158
159 2
        foreach ($definitions as $key => $value) {
160 2
            $key = Str::of($key);
161
162 2
            if (!is_array($value)) {
163
                throw new DomainException;
164
            }
165
166 2
            if ($key->matches(self::STACK_NAME)) {
167 2
                $this->registerStack($namespace, $key, $value);
168
169 2
                continue;
170
            }
171
172 2
            if (!$key->matches(self::SERVICE_NAME)) {
173 2
                $services = $services->merge(
174 2
                    $this->buildDefinitions(
175 2
                        $namespace->add((string) $key),
176 2
                        $value
177
                    )
178
                );
179
180 2
                continue;
181
            }
182
183 2
            $service = $this->buildService($namespace, $key, $value);
184 2
            $services = $services->put(
185 2
                (string) $service->name(),
1 ignored issue
show
Bug introduced by
(string)$service->name() of type string is incompatible with the type Innmind\Immutable\T expected by parameter $key of Innmind\Immutable\MapInterface::put(). ( Ignorable by Annotation )

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

185
                /** @scrutinizer ignore-type */ (string) $service->name(),
Loading history...
186 2
                $service
187
            );
188
        }
189
190 2
        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...
191
    }
192
193 2
    private function buildService(
194
        Stream $namespace,
195
        Str $name,
196
        array $arguments
197
    ): Service {
198 2
        $components = $name->capture(self::SERVICE_NAME);
199
200 2
        foreach ($arguments as &$argument) {
201 2
            $argument = $this->arguments->load($argument);
202
        }
203
204 2
        return new Service(
205 2
            new Name(
206
                (string) $namespace
207 2
                    ->add((string) $components->get('name'))
208 2
                    ->join('.')
209
            ),
210 2
            $this->constructors->load($components->get('constructor')->trim('  ')), //space and non breaking space
211 2
            ...$arguments
212
        );
213
    }
214
215 2
    private function registerStack(Stream $namespace, Str $key, array $stack): void
216
    {
217
        $name = (string) $namespace
218 2
            ->add((string) $key->capture(self::STACK_NAME)->get('name'))
219 2
            ->join('.');
220
221 2
        $this->stacks = $this->stacks->put(
222 2
            new Name($name),
223 2
            Sequence::of(...$stack)->map(static function(string $name): Name {
224 2
                return new Name(
225 2
                    (string) Str::of($name)->substring(1) //remove the $ sign
226
                );
227 2
            })
228
        );
229 2
    }
230
231 2
    private function buildStacks(Definitions $definitions): Definitions
232
    {
233 2
        return $this->stacks->reduce(
234 2
            $definitions,
235 2
            function(Definitions $definitions, Name $name, Sequence $stack): Definitions {
236 2
                return $definitions->stack($name, ...$stack);
0 ignored issues
show
Bug introduced by
The call to Innmind\Compose\Definitions::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

236
                return $definitions->/** @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...
Bug introduced by
$stack is expanded, but the parameter $highest of Innmind\Compose\Definitions::stack() does not expect variable arguments. ( Ignorable by Annotation )

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

236
                return $definitions->stack($name, /** @scrutinizer ignore-type */ ...$stack);
Loading history...
237 2
            }
238
        );
239
    }
240
}
241