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
![]() |
|||||
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
|
|||||
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
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
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. ![]() |
|||||
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 |