Completed
Push — master ( 1e2100...b879f8 )
by Taosikai
14:23
created

Container::has()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.2
c 0
b 0
f 0
cc 4
eloc 6
nc 3
nop 1
1
<?php
2
/**
3
 * slince dependency injection component
4
 * @author Tao <[email protected]>
5
 */
6
namespace Slince\Di;
7
8
use Psr\Container\ContainerInterface;
9
use Slince\Di\Exception\ConfigException;
10
use Slince\Di\Exception\DependencyInjectionException;
11
use Slince\Di\Exception\NotFoundException;
12
13
class Container implements ContainerInterface
14
{
15
    /**
16
     * Array of singletons
17
     * @var array
18
     */
19
    protected $shares = [];
20
21
    /**
22
     * Array pf definitions, support instance,callable,Definition, class
23
     * @var array
24
     */
25
    protected $definitions = [];
26
27
    /**
28
     * Array of interface bindings
29
     * @var array
30
     */
31
    protected $contextBindings = [];
32
33
    /**
34
     * Array of parameters
35
     * @var ParameterStore
36
     */
37
    protected $parameters;
38
39
    /**
40
     * @var ClassDefinitionResolver
41
     */
42
    protected $classDefinitionResolver;
43
44
    public function __construct()
45
    {
46
        $this->parameters = new ParameterStore();
47
    }
48
49
    /**
50
     * Add a Definition class
51
     * @param string $name
52
     * @param string $class
53
     * @return ClassDefinition
54
     */
55
    public function define($name, $class)
56
    {
57
        $definition = new ClassDefinition($class);
58
        $this->definitions[$name] = $definition;
59
        return $definition;
60
    }
61
62
    /**
63
     * Bind an callable to the container with its name
64
     * @param string $name
65
     * @param mixed $creation A invalid callable
66
     * @throws ConfigException
67
     * @return $this
68
     */
69
    public function call($name, $creation)
70
    {
71
        if (!is_callable($creation)) {
72
            throw new ConfigException(sprintf("Delegate expects a valid callable or executable class::method string at Argument 2"));
73
        }
74
        $this->definitions[$name] = $creation;
75
        return $this;
76
    }
77
78
    /**
79
     * Bind an instance to the container with its name
80
     * ```
81
     * $container->instance('user', new User());
82
     * //Or just give instance
83
     * $container->instance(new User());
84
     *
85
     * ```
86
     * @param string $name
87
     * @param object $instance
88
     * @throws ConfigException
89
     * @return $this
90
     */
91
    public function instance($name, $instance = null)
92
    {
93
        if (func_num_args() == 1) {
94
            if (!is_object($name)) {
95
                throw new ConfigException(sprintf("Instance expects a valid object"));
96
            }
97
            $instance = $name;
98
            $name = get_class($instance);
99
        }
100
        $this->definitions[$name] = $instance;
101
        $this->share($name);
102
        return $this;
103
    }
104
105
    /**
106
     * Binds an interface or abstract class to its implementation;
107
     * It's also be used to bind a service name to an existing class
108
     * @param string $name
109
     * @param string $implementation
110
     * @param string|array $context the specified context to bind
111
     * @throws ConfigException
112
     * @return $this
113
     */
114
    public function bind($name, $implementation, $context = null)
115
    {
116
        if (is_null($context)) {
117
            $this->define($name, $implementation);
118
        } else {
119
            if (is_array($context)) {
120
                list($contextClass, $contextMethod) = $context;
121
            } elseif (strpos($context, '::') !== false) {
122
                list($contextClass, $contextMethod) = explode('::', $context);
123
            } else {
124
                $contextClass = $context;
125
                $contextMethod = 'general';
126
            }
127
            isset($this->contextBindings[$contextClass][$contextMethod])
128
                || ($this->contextBindings[$contextClass][$contextMethod] = []);
129
            $this->contextBindings[$contextClass][$contextMethod][$name] = $implementation;
130
        }
131
        return $this;
132
    }
133
134
    /**
135
     * Add a definition to the container
136
     * ```
137
     * //Add an instance like "instance" method
138
     * $container->set('student', new Student());
139
     *
140
     * //Add a callable definition
141
     * $container->set('student', 'StudentFactory::create');
142
     * $container->set('student', function(){
143
     *     return new Student();
144
     * });
145
     *
146
     * //Add an instance of "Slince\Di\Definition"
147
     * $container->set('student', new Definition('Foo\Bar\StudentClass', [
148
     *      'gender' => 'boy',
149
     *      'school' => new Reference('school')
150
     * ], [
151
     *     'setAge' => [18]
152
     * ], [
153
     *     'father' => 'James',
154
     *     'mather' => 'Sophie'
155
     * ]));
156
     *
157
     * //Add a class definition
158
     * $container->set('student', Foo\Bar\StudentClass);
159
     * ```
160
     * @param string $name
161
     * @param mixed $definition
162
     * @throws ConfigException
163
     * @return $this
164
     */
165
    public function set($name, $definition)
166
    {
167
        if (is_callable($definition)) {
168
            $this->call($name, $definition);
169
        } elseif (is_object($definition)) {
170
            $this->instance($name, $definition);
171
        } elseif (is_string($definition)) {
172
            $this->define($name, $definition);
173
        } else {
174
            throw new ConfigException(sprintf("Unexpected object definition type '%s'", gettype($definition)));
175
        }
176
        return $this;
177
    }
178
179
    /**
180
     * Share the service by given name
181
     * @param string $name
182
     * @return $this
183
     */
184
    public function share($name)
185
    {
186
        $this->shares[$name] = null;
187
        return $this;
188
    }
189
190
    /**
191
     * Get a service instance by specified name
192
     * @param string $name
193
     * @param array $arguments
194
     * @return object
195
     */
196
    public function get($name, $arguments = [])
197
    {
198
        //If service is singleton, return instance directly.
199
        if (isset($this->shares[$name])) {
200
            return $this->shares[$name];
201
        }
202
        //If there is no matching definition, creates an definition automatically
203
        if (!isset($this->definitions[$name])) {
204
            if (class_exists($name)) {
205
                $this->bind($name, $name);
206
            } else {
207
                throw new NotFoundException(sprintf('There is no definition for "%s"', $name));
208
            }
209
        }
210
        $instance = $this->createInstanceFromDefinition($this->definitions[$name], $arguments);
211
        //If the service be set as singleton mode, stores its instance
212
        if (array_key_exists($name, $this->shares)) {
213
            $this->shares[$name] = $instance;
214
        }
215
        return $instance;
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221
    public function has($name)
222
    {
223
        if (isset($this->shares[$name])) {
224
            return true;
225
        }
226
        if (!isset($this->definitions[$name]) && class_exists($name)) {
227
            $this->bind($name, $name);
228
        }
229
        return isset($this->definitions[$name]);
230
    }
231
232
    /**
233
     * Gets all global parameters
234
     * @return array
235
     */
236
    public function getParameters()
237
    {
238
        return $this->parameters->toArray();
239
    }
240
241
    /**
242
     * Sets array of parameters
243
     * @param array $parameterStore
244
     */
245
    public function setParameters(array $parameterStore)
246
    {
247
        $this->parameters->setParameters($parameterStore);
248
    }
249
250
    /**
251
     * Add some parameters
252
     * @param array $parameters
253
     */
254
    public function addParameters(array $parameters)
255
    {
256
        $this->parameters->addParameters($parameters);
257
    }
258
259
    /**
260
     * Sets a parameter with its name and value
261
     * @param $name
262
     * @param mixed $value
263
     */
264
    public function setParameter($name, $value)
265
    {
266
        $this->parameters->setParameter($name, $value);
267
    }
268
269
    /**
270
     * Gets a parameter by given name
271
     * @param $name
272
     * @param mixed $default
273
     * @return mixed
274
     */
275
    public function getParameter($name, $default = null)
276
    {
277
        return $this->parameters->getParameter($name, $default);
278
    }
279
280
    /**
281
     * Resolves all arguments for the function or method.
282
     * @param \ReflectionFunctionAbstract $method
283
     * @param array $arguments
284
     * @param array $contextBindings The context bindings for the function
285
     * @throws DependencyInjectionException
286
     * @return array
287
     */
288
    public function resolveFunctionArguments(\ReflectionFunctionAbstract $method, array $arguments, array $contextBindings = [])
289
    {
290
        $functionArguments = [];
291
        $arguments = $this->resolveParameters($arguments);
292
        //Checks whether the position is numeric
293
        $isNumeric = !empty($arguments) && is_numeric(key($arguments));
294
        foreach ($method->getParameters() as $parameter) {
295
            $index = $isNumeric ? $parameter->getPosition() : $parameter->name;
296
            //If the dependency is provided directly
297
            if (isset($arguments[$index])) {
298
                $functionArguments[] = $arguments[$index];
299
            } elseif (($dependency = $parameter->getClass()) != null) {
300
                $dependencyName = $dependency->name;
301
                //Use the new dependency if the dependency name has been replaced in array of context bindings
302
                isset($contextBindings[$dependencyName]) && $dependencyName = $contextBindings[$dependencyName];
303
                try {
304
                    $functionArguments[] = $this->get($dependencyName);
305
                } catch (DependencyInjectionException $exception) {
306
                    if ($parameter->isOptional()) {
307
                        $functionArguments[] = $parameter->getDefaultValue();
308
                    } else {
309
                        throw $exception;
310
                    }
311
                }
312
            } elseif ($parameter->isOptional()) {
313
                $functionArguments[] = $parameter->getDefaultValue();
314
            } else {
315
                throw new DependencyInjectionException(sprintf(
316
                    'Missing required parameter "%s" when calling "%s"',
317
                    $parameter->name,
318
                    $method->getName()
319
                ));
320
            }
321
        }
322
        return $functionArguments;
323
    }
324
325
    /**
326
     * Gets all context bindings for the class and method
327
     * [
328
     *     'User' => [
329
     *          'original' => 'SchoolInterface'
330
     *          'bind' => 'MagicSchool',
331
     *     ]
332
     * ]
333
     * @param string $contextClass
334
     * @param string $contextMethod
335
     * @return array
336
     */
337
    public function getContextBindings($contextClass, $contextMethod)
338
    {
339
        if (!isset($this->contextBindings[$contextClass])) {
340
            return [];
341
        }
342
        $contextBindings = isset($this->contextBindings[$contextClass]['general'])
343
            ? $this->contextBindings[$contextClass]['general'] : [];
344
        if (isset($this->contextBindings[$contextClass][$contextMethod])) {
345
            $contextBindings = array_merge($contextBindings, $this->contextBindings[$contextClass][$contextMethod]);
346
        }
347
        return $contextBindings;
348
    }
349
350
    protected function createInstanceFromDefinition($definition, array $arguments)
351
    {
352
        if (is_callable($definition)) {
353
            if ($arguments && ($definition instanceof \Closure || is_string($definition))) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $arguments of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
354
                $arguments['container'] = $this;
355
                $arguments = $this->resolveFunctionArguments(
356
                    new \ReflectionFunction($definition),
357
                    $arguments
358
                );
359
            }
360
            $arguments = $arguments ?: [$this];
361
            $instance = call_user_func_array($definition, $arguments);
362
        } elseif ($definition instanceof ClassDefinition) {
363
            $instance = $this->getClassDefinitionResolver()->resolve($definition, $arguments);
364
        } elseif (is_object($definition)) {
365
            $instance = $definition;
366
        } else {
367
            throw new ConfigException(sprintf("Unexpected object definition type '%s'", gettype($definition)));
368
        }
369
        return $instance;
370
    }
371
372
    protected function getClassDefinitionResolver()
373
    {
374
        if (!is_null($this->classDefinitionResolver)) {
375
            return $this->classDefinitionResolver;
376
        }
377
        return $this->classDefinitionResolver = new ClassDefinitionResolver($this);
378
    }
379
380
    /**
381
     * Resolves array of parameters
382
     * @param array $parameters
383
     * @return array
384
     */
385
    protected function resolveParameters($parameters)
386
    {
387
        return array_map(function ($parameter) {
388
            if (is_string($parameter)) {
389
                $parameter = $this->formatParameter($parameter);
390
            } elseif ($parameter instanceof Reference) {
391
                $parameter = $this->get($parameter->getName());
392
            } elseif (is_array($parameter)) {
393
                $parameter = $this->resolveParameters($parameter);
394
            }
395
            return $parameter;
396
        }, $parameters);
397
    }
398
399
    /**
400
     * Formats parameter value
401
     * @param $value
402
     * @return mixed
403
     * @throws DependencyInjectionException
404
     */
405
    protected function formatParameter($value)
406
    {
407
        //%xx% return the parameter
408
        if (preg_match("#^%([^%\s]+)%$#", $value, $match)) {
409
            $key = $match[1];
410
            if ($parameter = $this->parameters->getParameter($key)) {
411
                return $parameter;
412
            }
413
            throw new DependencyInjectionException(sprintf("Parameter [%s] is not defined", $key));
414
        }
415
        //"fool%bar%baz"
416
        return preg_replace_callback("#%([^%\s]+)%#", function ($matches) {
417
            $key = $matches[1];
418
            if ($parameter = $this->parameters->getParameter($key)) {
419
                return $parameter;
420
            }
421
            throw new DependencyInjectionException(sprintf("Parameter [%s] is not defined", $key));
422
        }, $value);
423
    }
424
}
425