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

Container.php (1 issue)

Severity

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))) {
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