Completed
Push — develop ( f8d1c0...c6d734 )
by Baptiste
02:31
created

Yaml   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 180
Duplicated Lines 0 %

Test Coverage

Coverage 96.81%

Importance

Changes 0
Metric Value
wmc 18
dl 0
loc 180
ccs 91
cts 94
cp 0.9681
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
B buildArgument() 0 36 5
A __construct() 0 15 1
B buildDefinitions() 0 30 4
A __invoke() 0 23 1
A expose() 0 23 3
A buildArguments() 0 9 2
A buildService() 0 19 2
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
};
25
use Symfony\Component\{
26
    Yaml\Yaml as Lib,
27
    OptionsResolver\OptionsResolver
28
};
29
30
final class Yaml implements Loader
31
{
32
    private const ARGUMENT_PATTERN = '~^(?<optional>\?)?(?<type>.+)( \?\? \$.+)?$~';
33
    private const ARGUMENT_DEFAULT_PATTERN = '~( \?\? \$(?<default>.+))$~';
34
    private const SERVICE_NAME = "~^(?<name>[a-zA-Z0-9]+)[\s ](?<constructor>.+)$~"; //split on space or non breaking space
35
36
    private $resolver;
37
    private $types;
38
    private $arguments;
39
    private $constructors;
40
41 3
    public function __construct(
42
        Types $types,
43
        ServiceArguments $arguments,
44
        Constructors $constructors
45
    ) {
46 3
        $this->resolver = new OptionsResolver;
47 3
        $this->resolver->setRequired(['expose', 'services']);
48 3
        $this->resolver->setDefined('arguments');
49 3
        $this->resolver->setAllowedTypes('arguments', 'array');
50 3
        $this->resolver->setAllowedTypes('expose', 'array');
51 3
        $this->resolver->setAllowedTypes('services', 'array');
52 3
        $this->resolver->setDefault('arguments', []);
53 3
        $this->types = $types;
54 3
        $this->arguments = $arguments;
55 3
        $this->constructors = $constructors;
56 3
    }
57
58 2
    public function __invoke(PathInterface $definition): Definitions
59
    {
60 2
        $data = Lib::parseFile((string) $definition);
61 2
        $data = $this->resolver->resolve($data);
62
63 2
        $arguments = $this->buildArguments($data['arguments']);
64 2
        $definitions = $this->buildDefinitions(
65 2
            Stream::of('string'),
66 2
            $data['services']
67
        );
68 2
        $definitions = $this->expose(
69 2
            $definitions,
70 2
            Map::of(
71 2
                'string',
72 2
                'string',
73 2
                array_values($data['expose']),
74 2
                array_keys($data['expose'])
75
            )
76
        );
77
78 2
        return new Definitions(
79 2
            $arguments,
80 2
            ...$definitions->values()
81
        );
82
    }
83
84 2
    private function buildArguments(array $definitions): Arguments
85
    {
86 2
        $arguments = [];
87
88 2
        foreach ($definitions as $name => $type) {
89 2
            $arguments[] = $this->buildArgument($name, Str::of($type)->trim());
90
        }
91
92 2
        return new Arguments(...$arguments);
93
    }
94
95 2
    private function buildArgument(string $name, Str $type): Argument
96
    {
97 2
        if (!$type->matches(self::ARGUMENT_PATTERN)) {
98
            throw new DomainException;
99
        }
100
101 2
        $components = $type->capture(self::ARGUMENT_PATTERN);
102
103 2
        $argument = new Argument(
104 2
            new Name($name),
105 2
            $this->types->load(
106
                $components
107 2
                    ->get('type')
108 2
                    ->pregReplace(
109 2
                        self::ARGUMENT_DEFAULT_PATTERN,
110 2
                        ''
111
                    )
112
            )
113
        );
114
115
        if (
116 2
            $components->contains('optional') &&
117 2
            !$components->get('optional')->empty()
118
        ) {
119 2
            $argument = $argument->makeOptional();
120
        }
121
122 2
        if ($type->matches(self::ARGUMENT_DEFAULT_PATTERN)) {
123 2
            $argument = $argument->defaultsTo(new Name(
124
                (string) $type
125 2
                    ->capture(self::ARGUMENT_DEFAULT_PATTERN)
126 2
                    ->get('default')
127
            ));
128
        }
129
130 2
        return $argument;
131
    }
132
133 2
    private function buildDefinitions(Stream $namespace, array $definitions): Map
134
    {
135 2
        $services = new Map('string', Service::class);
136
137 2
        foreach ($definitions as $key => $value) {
138 2
            $key = Str::of($key);
139
140 2
            if (!is_array($value)) {
141
                throw new DomainException;
142
            }
143
144 2
            if (!$key->matches(self::SERVICE_NAME)) {
145 2
                $services = $services->merge(
146 2
                    $this->buildDefinitions(
147 2
                        $namespace->add((string) $key),
148 2
                        $value
149
                    )
150
                );
151
152 2
                continue;
153
            }
154
155 2
            $service = $this->buildService($namespace, $key, $value);
156 2
            $services = $services->put(
157 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

157
                /** @scrutinizer ignore-type */ (string) $service->name(),
Loading history...
158 2
                $service
159
            );
160
        }
161
162 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...
163
    }
164
165 2
    private function buildService(
166
        Stream $namespace,
167
        Str $name,
168
        array $arguments
169
    ): Service {
170 2
        $components = $name->capture(self::SERVICE_NAME);
171
172 2
        foreach ($arguments as &$argument) {
173 2
            $argument = $this->arguments->load($argument);
174
        }
175
176 2
        return new Service(
177 2
            new Name(
178
                (string) $namespace
179 2
                    ->add((string) $components->get('name'))
180 2
                    ->join('.')
181
            ),
182 2
            $this->constructors->load($components->get('constructor')->trim('  ')), //space and non breaking space
183 2
            ...$arguments
184
        );
185
    }
186
187
    private function expose(Map $definitions, Map $expose): Map
188
    {
189
        $expose = $expose
190 2
            ->foreach(static function(string $service): void {
191 2
                if (!Str::of($service)->matches('~^\$~')) {
192
                    throw new DomainException;
193
                }
194 2
            })
195 2
            ->map(static function(string $service, string $alias): Pair {
196 2
                return new Pair(
197 2
                    (string) Str::of($service)->substring(1), //remove the $ sign
198 2
                    $alias
199
                );
200 2
            });
201
202 2
        return $definitions->map(static function(string $name, Service $service) use ($expose): Service {
203 2
            if ($expose->contains($name)) {
204 2
                $service = $service->exposeAs(new Name(
205 2
                    $expose->get($name)
206
                ));
207
            }
208
209 2
            return $service;
210 2
        });
211
    }
212
}
213