Container::resolveContextualDependency()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
3
/**
4
 * This file is part of the alphaz Framework.
5
 *
6
 * @author Muhammad Umer Farooq (Malik) <[email protected]>
7
 *
8
 * @link https://github.com/alphazframework/framework
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 *  file that was distributed with this source code.
12
 * @since 1.0.0
13
 *
14
 * @license MIT
15
 */
16
17
namespace alphaz\Container;
18
19
use alphaz\Data\Arrays;
20
21
class Container implements ContainerContract
22
{
23
    /**
24
     * Registered type hints.
25
     *
26
     * @since 1.0.0
27
     *
28
     * @var array
29
     */
30
    protected $hints = [];
31
32
    /**
33
     * Aliases.
34
     *
35
     * @since 1.0.0
36
     *
37
     * @var array
38
     */
39
    protected $aliases = [];
40
41
    /**
42
     * Singleton instances.
43
     *
44
     * @since 1.0.0
45
     *
46
     * @var array
47
     */
48
    protected $instances = [];
49
50
    /**
51
     * Contextual dependencies.
52
     *
53
     * @since 1.0.0
54
     *
55
     * @var array
56
     */
57
    protected $contextualDependencies = [];
58
59
    /**
60
     * Instance replacers.
61
     *
62
     * @since 1.0.0
63
     *
64
     * @var array
65
     */
66
    protected $replacers = [];
67
68
    /**
69
     * Parse the hint parameter.
70
     *
71
     * @param string|array $hint Type hint or array contaning both type hint and alias
72
     *
73
     * @since 1.0.0
74
     *
75
     * @return string
76
     */
77
    protected function parseHint($hint): string
78
    {
79
        if (is_string($hint) && preg_match('/a-zA-Z/i', $hint)) {
80
            return $hint;
81
        } elseif (Arrays::isReallyArray($hint)) {
82
            list($hint, $alias) = $hint;
83
            $this->aliases[$alias] = $hint;
84
85
            return $hint;
86
        }
87
88
        throw new \InvalidArgumentException("The {$hint} parameter should be array or string ".gettype($hint).' given', 500);
89
    }
90
91
    /**
92
     * Register a type hint.
93
     *
94
     * @param string|array    $hint      Type hint or array contaning both type hint and alias.
95
     * @param string|\Closure $class     Class name or closure.
96
     * @param bool            $singleton Should we return the same instance every time?
97
     *
98
     * @since 1.0.0
99
     *
100
     * @return void
101
     */
102
    public function register($hint, $class, bool $singleton = false)
103
    {
104
        if ($class instanceof $hint[0]) {
105
            $this->hints[$this->parseHint($hint)] = ['class' => $class, 'singleton' => $singleton];
106
        } else {
107
            // If not an instance of a class then throw an exception.
108
            throw new \InvalidArgumentException("Claass should be valid instance of {$hint[0]}.", 500);
109
        }
110
    }
111
112
    /**
113
     * Register a type hint and return the same instance every time.
114
     *
115
     * @param string|array    $hint  Type hint or array contaning both type hint and alias.
116
     * @param string|\Closure $class Class name or closure.
117
     *
118
     * @since 1.0.0
119
     *
120
     * @return 3.0.0
0 ignored issues
show
Documentation Bug introduced by
The doc comment 3.0.0 at position 0 could not be parsed: Unknown type name '3.0.0' at position 0 in 3.0.0.
Loading history...
121
     */
122
    public function registerSingleton($hint, $class)
123
    {
124
        $this->register($hint, $class, true);
125
    }
126
127
    /**
128
     * Register a singleton instance.
129
     *
130
     * @param string|array $hint     Type hint or array contaning both type hint and alias.
131
     * @param object       $instance Class instance.
132
     *
133
     * @since 1.0.0
134
     *
135
     * @return void
136
     */
137
    public function registerInstance($hint, $instance)
138
    {
139
        $this->instances[$this->parseHint($hint)] = $instance;
140
    }
141
142
    /**
143
     * Registers a contextual dependency.
144
     *
145
     * @param string $class          Class.
146
     * @param string $interface      Interface.
147
     * @param string $implementation Implementation.
148
     *
149
     * @since 1.0.0
150
     *
151
     * @return void
152
     */
153
    public function registerContextualDependency($class, string $interface, string $implementation)
154
    {
155
        $this->contextualDependencies[$class][$interface] = $implementation;
156
    }
157
158
    /**
159
     * Return the name based on its alias.
160
     *
161
     * @param string $alias Alias.
162
     *
163
     * @since 1.0.0
164
     *
165
     * @return string
166
     */
167
    protected function resolveAlias(string $alias)
168
    {
169
        return $this->aliases[$alias] ?? $alias;
170
    }
171
172
    /**
173
     * Resolves a type hint.
174
     *
175
     * @param string $hint Type hint
176
     *
177
     * @since 1.0.0
178
     *
179
     * @return string|\Closure
180
     */
181
    protected function resolveHint($hint)
182
    {
183
        return $this->hints[$hint]['class'] ?? $hint;
184
    }
185
186
    /**
187
     * Resolves a contextual dependency.
188
     *
189
     * @param string $class     Class.
190
     * @param string $interface Interface.
191
     *
192
     * @since 1.0.0
193
     *
194
     * @return string
195
     */
196
    protected function resolveContextualDependency(string $class, string $interface): string
197
    {
198
        return $this->contextualDependencies[$class][$interface] ?? $interface;
199
    }
200
201
    /**
202
     * Merges the provided parameters with the reflection parameters into one array.
203
     *
204
     * @param array $reflectionParameters Reflection parameters.
205
     * @param array $providedParameters   Provided parameters.
206
     *
207
     * @since 1.0.0
208
     *
209
     * @return array
210
     */
211
    protected function mergeParameters(array $reflectionParameters, array $providedParameters): array
0 ignored issues
show
Unused Code introduced by
The parameter $providedParameters is not used and could be removed. ( Ignorable by Annotation )

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

211
    protected function mergeParameters(array $reflectionParameters, /** @scrutinizer ignore-unused */ array $providedParameters): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
212
    {
213
        $assocReflectionParameters = [];
214
        foreach ($reflectionParameters as $value) {
215
            $assocReflectionParameters[$value->getName()] = $value;
216
        }
217
        $assocProvidedParameters = [];
218
        foreach ($reflectionParameters as $key => $value) {
219
            $assocProvidedParameters[$key] = $value;
220
        }
221
222
        return array_replace($assocReflectionParameters, $assocProvidedParameters);
223
    }
224
225
    /**
226
     * Returns the name of function.
227
     *
228
     * @param \ReflectionParameter $parameter ReflectionParameter instance.
229
     *
230
     * @since 1.0.0
231
     *
232
     * @return string
233
     */
234
    protected function getDeclaringFunction(\ReflectionParameter $parameter): string
235
    {
236
        $declaringFunction = $parameter->getDeclaringFunction();
237
        $class = $parameter->getDeclaringClass();
238
239
        if ($declaringFunction->isClosure()) {
240
            return 'Closure';
241
        } elseif ($class === null) {
242
            return $declaringFunction->getName();
243
        }
244
245
        return $class->getName().'::'.$declaringFunction->getName();
246
    }
247
248
    /**
249
     * Resolve a parameter.
250
     *
251
     * @param \ReflectionParameter  $parameter ReflectionParameter instance.
252
     * @param \ReflectionClass|null $class     ReflectionClass instance.
253
     *
254
     * @since 1.0.0
255
     *
256
     * @return mixed
257
     */
258
    protected function resolveParameter(\ReflectionParameter $parameter, \ReflectionClass $class = null)
259
    {
260
        //Try to get the class name.
261
        if ($parameterClass = $parameter->getClass() !== null) {
262
            $parameterClassName = $parameterClass->getName();
263
            if ($class !== null) {
264
                $parameterClassName = $this->resolveContextualDependency($class->getName(), $parameterClassName);
265
            }
266
267
            return $this->get($parameterClassName);
268
269
            //Detetmine Parameter has default value? yes, use it.
270
        } elseif ($parameter->isDefaultValueAvailable()) {
271
            return $parameter->getDefaultValue();
272
        }
273
274
        throw new \RuntimeException('Unable to resolve the parameter', 500);
275
    }
276
277
    /**
278
     * Resolve parameters.
279
     *
280
     * @param array                 $reflectionParameters Reflection parameters.
281
     * @param array                 $providedParameters   Provided Parameters.
282
     * @param \ReflectionClass|null $class                ReflectionClass instance.
283
     *
284
     * @since 1.0.0
285
     *
286
     * @return array
287
     */
288
    protected function resolveParameters(array $reflectionParameters, array $providedParameters, \ReflectionClass $class = null): array
289
    {
290
        //Merge the parameter in to one array.
291
        $parameters = $this->mergeParameters($reflectionParameters, $providedParameters);
292
293
        foreach ($parameters as $key => $parameter) {
294
            //Determine either the parameter instance of \ReflectionParameter?
295
            if ($parameter instanceof \ReflectionParameter) {
296
                $parameters[$key] = $this->resolveParameter($parameter, $class);
297
            }
298
        }
299
300
        return array_values($parameters);
301
    }
302
303
    /**
304
     * Creates a class instance using closure.
305
     *
306
     * @param closure $factory    Closuare.
0 ignored issues
show
Bug introduced by
The type alphaz\Container\closure was not found. Did you mean closure? If so, make sure to prefix the type with \.
Loading history...
307
     * @param array   $parameters Constructor parameters.
308
     *
309
     * @since 1.0.0
310
     *
311
     * @return object
312
     */
313
    public function closureFactory(\Closure $factory, array $parameters)
314
    {
315
        $instance = $factory(...array_merge($this, $parameters));
0 ignored issues
show
Bug introduced by
$this of type alphaz\Container\Container is incompatible with the type array expected by parameter $arrays of array_merge(). ( Ignorable by Annotation )

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

315
        $instance = $factory(...array_merge(/** @scrutinizer ignore-type */ $this, $parameters));
Loading history...
316
        //Determine closure return valid object.
317
        if (is_object($instance) !== false) {
318
            return $instance;
319
        }
320
321
        throw new \Exception('Instance should be an object', 500);
322
    }
323
324
    /**
325
     * Creates a class instance using reflection.
326
     *
327
     * @param mixed $class      Class name.
328
     * @param array $parameters Constructor parameters.
329
     *
330
     * @since 1.0.0
331
     *
332
     * @return object
333
     */
334
    public function reflectionFactory($class, array $parameters = [])
335
    {
336
        $class = new \ReflectionClass($class);
337
338
        //Determine the class is really class?
339
        if ($class->isInstantiable() === true) {
340
            //Get the class construct.
341
            $constructor = $class->getConstructor();
342
            if (null === $constructor) {
343
                //No construct just return an object.
344
                return $class->newInstance();
345
            }
346
347
            //Get Construct parameters.
348
            $constructorParameters = $constructor->getParameters();
349
350
            return $class->newInstanceArgs($this->resolveParameters($constructorParameters, $parameters, $class));
351
        }
352
353
        throw new \Exception('Class is not instantiable', 500);
354
    }
355
356
    /**
357
     * Creates a class instance.
358
     *
359
     * @param string|\Closure $class      Class name or closure.
360
     * @param array           $parameters Constructor parameters.
361
     *
362
     * @since 1.0.0
363
     *
364
     * @return object
365
     */
366
    public function factory($class, array $parameters = [])
367
    {
368
        //If Closure calls to closure method.
369
        if ($class instanceof \Closure) {
370
            $instance = $this->closureFactory($class, $parameters);
371
        } else {
372
            //If reflection calls to reflection.
373
            $instance = $this->reflectionFactory($class, $parameters);
374
        }
375
376
        return $instance;
377
    }
378
379
    /**
380
     * Checks if a class is registered in the container.
381
     *
382
     * @param string $class Class name.
383
     *
384
     * @since 1.0.0
385
     *
386
     * @return bool
387
     */
388
    public function has(string $class): bool
389
    {
390
        $class = $this->resolveAlias($class);
391
392
        return isset($this->hints[$class]) || isset($this->instances[$class]);
393
    }
394
395
    /**
396
     * Returns TRUE if a class has been registered as a singleton and FALSE if not.
397
     *
398
     * @param string $class Class name.
399
     *
400
     * @since 1.0.0
401
     *
402
     * @return bool
403
     */
404
    public function isSingleton(string $class): bool
405
    {
406
        $class = $this->resolveAlias($class);
407
408
        return (isset($this->hints[$class]) || isset($this->instances[$class])) && $this->hints[$class]['singleton'] === true;
409
    }
410
411
    /**
412
     * Returns a class instance.
413
     *
414
     * @param string $class      Class name.
415
     * @param array  $parameters Constructor parameters.
416
     *
417
     * @since 1.0.0
418
     *
419
     * @return object
420
     */
421
    public function get($class, array $parameters = [])
422
    {
423
        $class = $this->resolveAlias($class);
424
425
        //If instance? return it.
426
        if (isset($this->instances[$class])) {
427
            return $this->instances[$class];
428
        }
429
430
        $instance = $this->factory($this->resolveHint($class), $parameters);
431
432
        //If singleton store to new instance.
433
        if (isset($this->hints[$class]) && $this->hints[$class]['singleton']) {
434
            $this->instances[$class] = $instance;
435
        }
436
437
        return $instance;
438
    }
439
440
    /**
441
     * Execute a callable and inject its dependencies.
442
     *
443
     * @param callable $callable   Callable.
444
     * @param array    $parameters Parameters.
445
     *
446
     * @since 1.0.0
447
     *
448
     * @return object
449
     */
450
    public function exec(callable $callable, array $parameters = [])
451
    {
452
        $reflection = new \ReflectionFunction($callable);
453
454
        return $callable(...$this->resolveParameters($reflection->getParameters(), $parameters));
455
    }
456
}
457