Completed
Push — master ( e299e4...94cf65 )
by Taosikai
15:49 queued 02:33
created

Container.php (3 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
/*
4
 * This file is part of the slince/di package.
5
 *
6
 * (c) Slince <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Slince\Di;
13
14
use Slince\Di\Exception\ConfigException;
15
use Slince\Di\Exception\DependencyInjectionException;
16
use Slince\Di\Exception\NotFoundException;
17
use Interop\Container\ContainerInterface;
18
19
class Container implements ContainerInterface
20
{
21
    /**
22
     * Array of singletons
23
     * @var array
24
     */
25
    protected $shares = [];
26
27
    /**
28
     * Array pf definitions, support instance,callable,Definition, class
29
     * @var array
30
     */
31
    protected $definitions = [];
32
33
    /**
34
     * Array of interface bindings
35
     * @var array
36
     */
37
    protected $contextBindings = [];
38
39
    /**
40
     * Array of parameters
41
     * @var ParameterStore
42
     */
43
    protected $parameters;
44
45
    /**
46
     * @var ClassDefinitionResolver
47
     */
48
    protected $classDefinitionResolver;
49
50
    public function __construct()
51
    {
52
        $this->parameters = new ParameterStore();
53
        $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...
54
    }
55
56
    /**
57
     * Add a Definition class
58
     * @param string $name
59
     * @param string $class
60
     * @return ClassDefinition
61
     */
62
    public function define($name, $class)
63
    {
64
        $definition = new ClassDefinition($class);
0 ignored issues
show
Deprecated Code introduced by
The class Slince\Di\ClassDefinition has been deprecated.

This class, trait or interface has been deprecated.

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