Completed
Push — master ( 84dd97...00884c )
by Taosikai
13:27
created

Container.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
        $this->instance($this);
0 ignored issues
show
$this is of type this<Slince\Di\Container>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
48
    }
49
50
    /**
51
     * Add a Definition class
52
     * @param string $name
53
     * @param string $class
54
     * @return ClassDefinition
55
     */
56
    public function define($name, $class)
57
    {
58
        $definition = new ClassDefinition($class);
59
        $this->definitions[$name] = $definition;
60
        return $definition;
61
    }
62
63
    /**
64
     * Bind an callable to the container with its name
65
     * @param string $name
66
     * @param mixed $creation A invalid callable
67
     * @throws ConfigException
68
     * @return $this
69
     */
70
    public function call($name, $creation)
71
    {
72
        if (!is_callable($creation)) {
73
            throw new ConfigException(sprintf("Call expects a valid callable or executable class::method string"));
74
        }
75
        $this->definitions[$name] = $creation;
76
        return $this;
77
    }
78
79
    /**
80
     * Bind an instance to the container with its name
81
     * ```
82
     * $container->instance('user', new User());
83
     * //Or just give instance
84
     * $container->instance(new User());
85
     *
86
     * ```
87
     * @param string $name
88
     * @param object $instance
89
     * @throws ConfigException
90
     * @return $this
91
     */
92
    public function instance($name, $instance = null)
93
    {
94
        if (func_num_args() == 1) {
95
            if (!is_object($name)) {
96
                throw new ConfigException(sprintf("Instance expects a valid object"));
97
            }
98
            $instance = $name;
99
            $name = get_class($instance);
100
        }
101
        $this->definitions[$name] = $instance;
102
        $this->share($name);
103
        return $this;
104
    }
105
106
    /**
107
     * Binds an interface or abstract class to its implementation;
108
     * It's also be used to bind a service name to an existing class
109
     * @param string $name
110
     * @param string $implementation
111
     * @param string|array $context the specified context to bind
112
     * @throws ConfigException
113
     * @return $this
114
     */
115
    public function bind($name, $implementation, $context = null)
116
    {
117
        if (is_null($context)) {
118
            $this->define($name, $implementation);
119
        } else {
120
            if (is_array($context)) {
121
                list($contextClass, $contextMethod) = $context;
122
            } else {
123
                $contextClass = $context;
124
                $contextMethod = 'general';
125
            }
126
            isset($this->contextBindings[$contextClass][$contextMethod])
127
                || ($this->contextBindings[$contextClass][$contextMethod] = []);
128
            $this->contextBindings[$contextClass][$contextMethod][$name] = $implementation;
129
        }
130
        return $this;
131
    }
132
133
    /**
134
     * Share the service by given name
135
     * @param string $name
136
     * @return $this
137
     */
138
    public function share($name)
139
    {
140
        $this->shares[$name] = null;
141
        return $this;
142
    }
143
144
    /**
145
     * Add a definition to the container
146
     * ```
147
     * //Add an instance like "instance" method
148
     * $container->set('student', new Student());
149
     *
150
     * //Add a callable definition
151
     * $container->set('student', 'StudentFactory::create');
152
     * $container->set('student', function(){
153
     *     return new Student();
154
     * });
155
     *
156
     * //Add an instance of "Slince\Di\Definition"
157
     * $container->set('student', new Definition('Foo\Bar\StudentClass', [
158
     *      'gender' => 'boy',
159
     *      'school' => new Reference('school')
160
     * ], [
161
     *     'setAge' => [18]
162
     * ], [
163
     *     'father' => 'James',
164
     *     'mather' => 'Sophie'
165
     * ]));
166
     *
167
     * //Add a class definition
168
     * $container->set('student', Foo\Bar\StudentClass);
169
     * ```
170
     * @param string $name
171
     * @param mixed $definition
172
     * @throws ConfigException
173
     * @return $this
174
     */
175
    public function set($name, $definition)
176
    {
177
        if (is_callable($definition)) {
178
            $this->call($name, $definition);
179
        } elseif (is_object($definition)) {
180
            $this->instance($name, $definition);
181
        } elseif (is_string($definition)) {
182
            $this->define($name, $definition);
183
        } else {
184
            throw new ConfigException(sprintf("Unexpected object definition type '%s'", gettype($definition)));
185
        }
186
        return $this;
187
    }
188
189
    /**
190
     * Get a service instance by specified name
191
     * @param string $name
192
     * @param array $arguments
193
     * @return object
194
     */
195
    public function get($name, $arguments = [])
196
    {
197
        //If service is singleton, return instance directly.
198
        if (isset($this->shares[$name])) {
199
            return $this->shares[$name];
200
        }
201
        //If there is no matching definition, creates an definition automatically
202
        if (!isset($this->definitions[$name])) {
203
            if (class_exists($name)) {
204
                $this->bind($name, $name);
205
            } else {
206
                throw new NotFoundException(sprintf('There is no definition for "%s"', $name));
207
            }
208
        }
209
        $instance = $this->createInstanceFromDefinition($this->definitions[$name], $arguments);
210
        //If the service be set as singleton mode, stores its instance
211
        if (array_key_exists($name, $this->shares)) {
212
            $this->shares[$name] = $instance;
213
        }
214
        return $instance;
215
    }
216
217
    /**
218
     * {@inheritdoc}
219
     */
220
    public function has($name)
221
    {
222
        if (isset($this->shares[$name])) {
223
            return true;
224
        }
225
        if (!isset($this->definitions[$name]) && class_exists($name)) {
226
            $this->bind($name, $name);
227
        }
228
        return isset($this->definitions[$name]);
229
    }
230
231
    /**
232
     * Gets all global parameters
233
     * @return array
234
     */
235
    public function getParameters()
236
    {
237
        return $this->parameters->toArray();
238
    }
239
240
    /**
241
     * Sets array of parameters
242
     * @param array $parameterStore
243
     */
244
    public function setParameters(array $parameterStore)
245
    {
246
        $this->parameters->setParameters($parameterStore);
247
    }
248
249
    /**
250
     * Add some parameters
251
     * @param array $parameters
252
     */
253
    public function addParameters(array $parameters)
254
    {
255
        $this->parameters->addParameters($parameters);
256
    }
257
258
    /**
259
     * Sets a parameter with its name and value
260
     * @param $name
261
     * @param mixed $value
262
     */
263
    public function setParameter($name, $value)
264
    {
265
        $this->parameters->setParameter($name, $value);
266
    }
267
268
    /**
269
     * Gets a parameter by given name
270
     * @param $name
271
     * @param mixed $default
272
     * @return mixed
273
     */
274
    public function getParameter($name, $default = null)
275
    {
276
        return $this->parameters->getParameter($name, $default);
277
    }
278
279
    /**
280
     * Resolves all arguments for the function or method.
281
     * @param \ReflectionFunctionAbstract $method
282
     * @param array $arguments
283
     * @param array $contextBindings The context bindings for the function
284
     * @throws DependencyInjectionException
285
     * @return array
286
     */
287
    public function resolveFunctionArguments(\ReflectionFunctionAbstract $method, array $arguments, array $contextBindings = [])
288
    {
289
        $functionArguments = [];
290
        $arguments = $this->resolveParameters($arguments);
291
        //Checks whether the position is numeric
292
        $isNumeric = !empty($arguments) && is_numeric(key($arguments));
293
        foreach ($method->getParameters() as $parameter) {
294
            $index = $isNumeric ? $parameter->getPosition() : $parameter->name;
295
            //If the dependency is provided directly
296
            if (isset($arguments[$index])) {
297
                $functionArguments[] = $arguments[$index];
298
            } elseif (($dependency = $parameter->getClass()) != null) {
299
                $dependencyName = $dependency->name;
300
                //Use the new dependency if the dependency name has been replaced in array of context bindings
301
                isset($contextBindings[$dependencyName]) && $dependencyName = $contextBindings[$dependencyName];
302
                try {
303
                    $functionArguments[] = $this->get($dependencyName);
304
                } catch (NotFoundException $exception) {
305
                    if ($parameter->isOptional()) {
306
                        $functionArguments[] = $parameter->getDefaultValue();
307
                    } else {
308
                        throw $exception;
309
                    }
310
                }
311
            } elseif ($parameter->isOptional()) {
312
                $functionArguments[] = $parameter->getDefaultValue();
313
            } else {
314
                throw new DependencyInjectionException(sprintf(
315
                    'Missing required parameter "%s" when calling "%s"',
316
                    $parameter->name,
317
                    $method->getName()
318
                ));
319
            }
320
        }
321
        return $functionArguments;
322
    }
323
324
    /**
325
     * Gets all context bindings for the class and method
326
     * [
327
     *     'User' => [
328
     *          'original' => 'SchoolInterface'
329
     *          'bind' => 'MagicSchool',
330
     *     ]
331
     * ]
332
     * @param string $contextClass
333
     * @param string $contextMethod
334
     * @return array
335
     */
336
    public function getContextBindings($contextClass, $contextMethod)
337
    {
338
        if (!isset($this->contextBindings[$contextClass])) {
339
            return [];
340
        }
341
        $contextBindings = isset($this->contextBindings[$contextClass]['general'])
342
            ? $this->contextBindings[$contextClass]['general'] : [];
343
        if (isset($this->contextBindings[$contextClass][$contextMethod])) {
344
            $contextBindings = array_merge($contextBindings, $this->contextBindings[$contextClass][$contextMethod]);
345
        }
346
        return $contextBindings;
347
    }
348
349
    protected function createInstanceFromDefinition($definition, array $arguments)
350
    {
351
        if (is_callable($definition)) {
352
            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...
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
        } else {
363
            $instance = $definition;
364
        }
365
        return $instance;
366
    }
367
368
    protected function getClassDefinitionResolver()
369
    {
370
        if (!is_null($this->classDefinitionResolver)) {
371
            return $this->classDefinitionResolver;
372
        }
373
        return $this->classDefinitionResolver = new ClassDefinitionResolver($this);
374
    }
375
376
    /**
377
     * Resolves array of parameters
378
     * @param array $parameters
379
     * @return array
380
     */
381
    protected function resolveParameters($parameters)
382
    {
383
        return array_map(function ($parameter) {
384
            if (is_string($parameter)) {
385
                $parameter = $this->formatParameter($parameter);
386
            } elseif ($parameter instanceof Reference) {
387
                $parameter = $this->get($parameter->getName());
388
            } elseif (is_array($parameter)) {
389
                $parameter = $this->resolveParameters($parameter);
390
            }
391
            return $parameter;
392
        }, $parameters);
393
    }
394
395
    /**
396
     * Formats parameter value
397
     * @param string $value
398
     * @return string
399
     * @throws DependencyInjectionException
400
     */
401
    protected function formatParameter($value)
402
    {
403
        //%xx% return the parameter
404
        if (preg_match("#^%([^%\s]+)%$#", $value, $match)) {
405
            $key = $match[1];
406
            if ($parameter = $this->parameters->getParameter($key)) {
407
                return $parameter;
408
            }
409
            throw new DependencyInjectionException(sprintf("Parameter [%s] is not defined", $key));
410
        }
411
        //"fool%bar%baz"
412
        return preg_replace_callback("#%([^%\s]+)%#", function ($matches) {
413
            $key = $matches[1];
414
            if ($parameter = $this->parameters->getParameter($key)) {
415
                return $parameter;
416
            }
417
            throw new DependencyInjectionException(sprintf("Parameter [%s] is not defined", $key));
418
        }, $value);
419
    }
420
}
421