Completed
Push — master ( a4072e...e31914 )
by Taosikai
12:15
created

Container::bind()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.2
c 0
b 0
f 0
cc 4
eloc 13
nc 5
nop 3
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
            } else {
122
                $contextClass = $context;
123
                $contextMethod = 'general';
124
            }
125
            isset($this->contextBindings[$contextClass][$contextMethod])
126
                || ($this->contextBindings[$contextClass][$contextMethod] = []);
127
            $this->contextBindings[$contextClass][$contextMethod][$name] = $implementation;
128
        }
129
        return $this;
130
    }
131
132
    /**
133
     * Share the service by given name
134
     * @param string $name
135
     * @return $this
136
     */
137
    public function share($name)
138
    {
139
        $this->shares[$name] = null;
140
        return $this;
141
    }
142
    
143
    /**
144
     * Add a definition to the container
145
     * ```
146
     * //Add an instance like "instance" method
147
     * $container->set('student', new Student());
148
     *
149
     * //Add a callable definition
150
     * $container->set('student', 'StudentFactory::create');
151
     * $container->set('student', function(){
152
     *     return new Student();
153
     * });
154
     *
155
     * //Add an instance of "Slince\Di\Definition"
156
     * $container->set('student', new Definition('Foo\Bar\StudentClass', [
157
     *      'gender' => 'boy',
158
     *      'school' => new Reference('school')
159
     * ], [
160
     *     'setAge' => [18]
161
     * ], [
162
     *     'father' => 'James',
163
     *     'mather' => 'Sophie'
164
     * ]));
165
     *
166
     * //Add a class definition
167
     * $container->set('student', Foo\Bar\StudentClass);
168
     * ```
169
     * @param string $name
170
     * @param mixed $definition
171
     * @throws ConfigException
172
     * @return $this
173
     */
174
    public function set($name, $definition)
175
    {
176
        if (is_callable($definition)) {
177
            $this->call($name, $definition);
178
        } elseif (is_object($definition)) {
179
            $this->instance($name, $definition);
180
        } elseif (is_string($definition)) {
181
            $this->define($name, $definition);
182
        } else {
183
            throw new ConfigException(sprintf("Unexpected object definition type '%s'", gettype($definition)));
184
        }
185
        return $this;
186
    }
187
188
    /**
189
     * Get a service instance by specified name
190
     * @param string $name
191
     * @param array $arguments
192
     * @return object
193
     */
194
    public function get($name, $arguments = [])
195
    {
196
        //If service is singleton, return instance directly.
197
        if (isset($this->shares[$name])) {
198
            return $this->shares[$name];
199
        }
200
        //If there is no matching definition, creates an definition automatically
201
        if (!isset($this->definitions[$name])) {
202
            if (class_exists($name)) {
203
                $this->bind($name, $name);
204
            } else {
205
                throw new NotFoundException(sprintf('There is no definition for "%s"', $name));
206
            }
207
        }
208
        $instance = $this->createInstanceFromDefinition($this->definitions[$name], $arguments);
209
        //If the service be set as singleton mode, stores its instance
210
        if (array_key_exists($name, $this->shares)) {
211
            $this->shares[$name] = $instance;
212
        }
213
        return $instance;
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219
    public function has($name)
220
    {
221
        if (isset($this->shares[$name])) {
222
            return true;
223
        }
224
        if (!isset($this->definitions[$name]) && class_exists($name)) {
225
            $this->bind($name, $name);
226
        }
227
        return isset($this->definitions[$name]);
228
    }
229
230
    /**
231
     * Gets all global parameters
232
     * @return array
233
     */
234
    public function getParameters()
235
    {
236
        return $this->parameters->toArray();
237
    }
238
239
    /**
240
     * Sets array of parameters
241
     * @param array $parameterStore
242
     */
243
    public function setParameters(array $parameterStore)
244
    {
245
        $this->parameters->setParameters($parameterStore);
246
    }
247
248
    /**
249
     * Add some parameters
250
     * @param array $parameters
251
     */
252
    public function addParameters(array $parameters)
253
    {
254
        $this->parameters->addParameters($parameters);
255
    }
256
257
    /**
258
     * Sets a parameter with its name and value
259
     * @param $name
260
     * @param mixed $value
261
     */
262
    public function setParameter($name, $value)
263
    {
264
        $this->parameters->setParameter($name, $value);
265
    }
266
267
    /**
268
     * Gets a parameter by given name
269
     * @param $name
270
     * @param mixed $default
271
     * @return mixed
272
     */
273
    public function getParameter($name, $default = null)
274
    {
275
        return $this->parameters->getParameter($name, $default);
276
    }
277
278
    /**
279
     * Resolves all arguments for the function or method.
280
     * @param \ReflectionFunctionAbstract $method
281
     * @param array $arguments
282
     * @param array $contextBindings The context bindings for the function
283
     * @throws DependencyInjectionException
284
     * @return array
285
     */
286
    public function resolveFunctionArguments(\ReflectionFunctionAbstract $method, array $arguments, array $contextBindings = [])
287
    {
288
        $functionArguments = [];
289
        $arguments = $this->resolveParameters($arguments);
290
        //Checks whether the position is numeric
291
        $isNumeric = !empty($arguments) && is_numeric(key($arguments));
292
        foreach ($method->getParameters() as $parameter) {
293
            $index = $isNumeric ? $parameter->getPosition() : $parameter->name;
294
            //If the dependency is provided directly
295
            if (isset($arguments[$index])) {
296
                $functionArguments[] = $arguments[$index];
297
            } elseif (($dependency = $parameter->getClass()) != null) {
298
                $dependencyName = $dependency->name;
299
                //Use the new dependency if the dependency name has been replaced in array of context bindings
300
                isset($contextBindings[$dependencyName]) && $dependencyName = $contextBindings[$dependencyName];
301
                try {
302
                    $functionArguments[] = $this->get($dependencyName);
303
                } catch (DependencyInjectionException $exception) {
304
                    if ($parameter->isOptional()) {
305
                        $functionArguments[] = $parameter->getDefaultValue();
306
                    } else {
307
                        throw $exception;
308
                    }
309
                }
310
            } elseif ($parameter->isOptional()) {
311
                $functionArguments[] = $parameter->getDefaultValue();
312
            } else {
313
                throw new DependencyInjectionException(sprintf(
314
                    'Missing required parameter "%s" when calling "%s"',
315
                    $parameter->name,
316
                    $method->getName()
317
                ));
318
            }
319
        }
320
        return $functionArguments;
321
    }
322
323
    /**
324
     * Gets all context bindings for the class and method
325
     * [
326
     *     'User' => [
327
     *          'original' => 'SchoolInterface'
328
     *          'bind' => 'MagicSchool',
329
     *     ]
330
     * ]
331
     * @param string $contextClass
332
     * @param string $contextMethod
333
     * @return array
334
     */
335
    public function getContextBindings($contextClass, $contextMethod)
336
    {
337
        if (!isset($this->contextBindings[$contextClass])) {
338
            return [];
339
        }
340
        $contextBindings = isset($this->contextBindings[$contextClass]['general'])
341
            ? $this->contextBindings[$contextClass]['general'] : [];
342
        if (isset($this->contextBindings[$contextClass][$contextMethod])) {
343
            $contextBindings = array_merge($contextBindings, $this->contextBindings[$contextClass][$contextMethod]);
344
        }
345
        return $contextBindings;
346
    }
347
348
    protected function createInstanceFromDefinition($definition, array $arguments)
349
    {
350
        if (is_callable($definition)) {
351
            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...
352
                $arguments['container'] = $this;
353
                $arguments = $this->resolveFunctionArguments(
354
                    new \ReflectionFunction($definition),
355
                    $arguments
356
                );
357
            }
358
            $arguments = $arguments ?: [$this];
359
            $instance = call_user_func_array($definition, $arguments);
360
        } elseif ($definition instanceof ClassDefinition) {
361
            $instance = $this->getClassDefinitionResolver()->resolve($definition, $arguments);
362
        } elseif (is_object($definition)) {
363
            $instance = $definition;
364
        } else {
365
            throw new ConfigException(sprintf("Unexpected object definition type '%s'", gettype($definition)));
366
        }
367
        return $instance;
368
    }
369
370
    protected function getClassDefinitionResolver()
371
    {
372
        if (!is_null($this->classDefinitionResolver)) {
373
            return $this->classDefinitionResolver;
374
        }
375
        return $this->classDefinitionResolver = new ClassDefinitionResolver($this);
376
    }
377
378
    /**
379
     * Resolves array of parameters
380
     * @param array $parameters
381
     * @return array
382
     */
383
    protected function resolveParameters($parameters)
384
    {
385
        return array_map(function ($parameter) {
386
            if (is_string($parameter)) {
387
                $parameter = $this->formatParameter($parameter);
388
            } elseif ($parameter instanceof Reference) {
389
                $parameter = $this->get($parameter->getName());
390
            } elseif (is_array($parameter)) {
391
                $parameter = $this->resolveParameters($parameter);
392
            }
393
            return $parameter;
394
        }, $parameters);
395
    }
396
397
    /**
398
     * Formats parameter value
399
     * @param $value
400
     * @return mixed
401
     * @throws DependencyInjectionException
402
     */
403
    protected function formatParameter($value)
404
    {
405
        //%xx% return the parameter
406
        if (preg_match("#^%([^%\s]+)%$#", $value, $match)) {
407
            $key = $match[1];
408
            if ($parameter = $this->parameters->getParameter($key)) {
409
                return $parameter;
410
            }
411
            throw new DependencyInjectionException(sprintf("Parameter [%s] is not defined", $key));
412
        }
413
        //"fool%bar%baz"
414
        return preg_replace_callback("#%([^%\s]+)%#", function ($matches) {
415
            $key = $matches[1];
416
            if ($parameter = $this->parameters->getParameter($key)) {
417
                return $parameter;
418
            }
419
            throw new DependencyInjectionException(sprintf("Parameter [%s] is not defined", $key));
420
        }, $value);
421
    }
422
}
423