Container   C
last analyzed

Complexity

Total Complexity 57

Size/Duplication

Total Lines 428
Duplicated Lines 0 %

Coupling/Cohesion

Dependencies 12

Test Coverage

Coverage 75.97%

Importance

Changes 26
Bugs 11 Features 8
Metric Value
wmc 57
c 26
b 11
f 8
cbo 12
dl 0
loc 428
ccs 98
cts 129
cp 0.7597
rs 5.1724

18 Methods

Rating   Name   Duplication   Size   Complexity  
A offsetExists() 0 4 1
A offsetGet() 0 4 1
A offsetSet() 0 4 1
A offsetUnset() 0 4 1
A delegate() 0 10 2
A __construct() 0 7 1
B set() 0 29 6
C get() 0 50 11
C iniProperties() 0 22 7
A getProperty() 0 10 3
A setProperty() 0 13 3
A propertizeString() 0 8 1
A unregister() 0 16 4
A has() 0 10 3
A getDefinition() 0 10 3
A hasInDelegate() 0 11 3
A getFromDelegate() 0 11 3
A search() 0 13 3

How to fix   Complexity   

Complex Class

Complex classes like Container often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Container, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Fwk
4
 *
5
 * Copyright (c) 2011-2012, Julien Ballestracci <[email protected]>.
6
 * All rights reserved.
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 *
11
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
12
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
13
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
15
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
16
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
17
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
19
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
20
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
21
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
22
 * POSSIBILITY OF SUCH DAMAGE.
23
 *
24
 * PHP Version 5.3
25
 *
26
 * @category  DependencyInjection
27
 * @package   Fwk\Di
28
 * @author    Julien Ballestracci <[email protected]>
29
 * @copyright 2011-2014 Julien Ballestracci <[email protected]>
30
 * @license   http://www.opensource.org/licenses/bsd-license.php  BSD License
31
 * @link      http://www.nitronet.org/fwk
32
 */
33
namespace Fwk\Di;
34
35
use Fwk\Di\Definitions\ArrayDefinition;
36
use Fwk\Di\Definitions\CallableDefinition;
37
use Fwk\Di\Definitions\ScalarDefinition;
38
use Fwk\Di\Events\AfterServiceLoadedEvent;
39
use Fwk\Di\Events\AfterServiceRegisteredEvent;
40
use Fwk\Di\Events\BeforeServiceLoadedEvent;
41
use Fwk\Di\Events\BeforeServiceRegisteredEvent;
42
use Fwk\Di\Exceptions\SearchException;
43
use Fwk\Events\Dispatcher;
44
use \ArrayAccess;
45
use Interop\Container\ContainerInterface;
46
use \SplObjectStorage;
47
48
/**
49
 * Container
50
 * 
51
 * THE Dependency Injection Container.
52
 *
53
 * @category Container
54
 * @package  Fwk\Di
55
 * @author   Julien Ballestracci <[email protected]>
56
 * @license  http://www.opensource.org/licenses/bsd-license.php  BSD License
57
 * @link     http://www.nitronet.org/fwk
58
 */
59
class Container extends Dispatcher implements ArrayAccess, ContainerInterface
60
{
61
    /**
62
     * The objects store
63
     * @var SplObjectStorage
64
     */
65
    protected $store = array();
66
67
    /**
68
     * Shared instances store
69
     * @var SplObjectStorage
70
     */
71
    private $_sharedInstances;
0 ignored issues
show
Coding Style introduced by
$_sharedInstances does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
72
73
    /**
74
     * Container Properties
75
     * @var array
76
     */
77
    protected $properties = array();
78
79
    /**
80
     * Properties Keys (cached)
81
     * @var array
82
     */
83
    protected $propertiesMap = array();
84
85
    /**
86
     * Delegates Containers
87
     * @var SplObjectStorage
88
     */
89
    protected $delegates;
90
    
91
    /**
92
     * Constructor
93
     * 
94
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
95
     */
96 39
    public function __construct()
97
    {
98 39
        $this->store = new SplObjectStorage();
99 39
        $this->delegates = new SplObjectStorage();
100 39
        $this->_sharedInstances = new SplObjectStorage();
101 39
        $this->set('self', $this);
102 39
    }
103
    
104
    /**
105
     * Registers a definition
106
     * 
107
     * @param string  $name             Identifier
108
     * @param DefinitionInterface|mixed $definition Definition, callable or value
109
     *
110
     * @return Container
111
     */
112 39
    public function set($name, $definition)
113
    {
114 39
        if (!$definition instanceof DefinitionInterface) {
115 39
            if (is_callable($definition)) {
116 23
                $definition = CallableDefinition::factory($definition);
117 39
            } elseif (is_array($definition)) {
118
                $definition = ArrayDefinition::factory($definition);
119
            } else {
120 39
                $wasObj = is_object($definition);
121 39
                $definition = ScalarDefinition::factory($definition);
122 39
                if ($wasObj) {
123 39
                    $definition->setShared(true);
124 39
                    $this->_sharedInstances->attach($definition, $name);
125
                }
126
            }
127
        }
128
129 39
        $event = new BeforeServiceRegisteredEvent($this, $name, $definition);
130 39
        $this->notify($event);
131
132 39
        if ($event->isStopped()) {
133
            return $this;
134
        }
135
136 39
        $this->store->attach($definition, $name);
137 39
        $this->notify(new AfterServiceRegisteredEvent($this, $name, $definition));
138
139 39
        return $this;
140
    }
141
    
142
    /**
143
     * Load and returns a definition
144
     * 
145
     * @param string $name Identifier
146
     * 
147
     * @throws Exceptions\DefinitionNotFoundException if $name isn't a valid identifier
148
     * @return mixed
149
     */
150 26
    public function get($name)
151
    {
152 26
        if ($name instanceof Reference) {
153
            $name = $name->getName();
154
        }
155
        
156 26
        if (!$this->has($name)) {
157 6
            return $this->getFromDelegate($name);
158
        }
159
160 20
        foreach ($this->store as $def) {
161 20
            if ($this->store->getInfo() === $name) {
162 20
                $definition = $def;
163 20
                break;
164
            }
165
        }
166
167
        /** @var DefinitionInterface $definition */
168 20
        if ($definition->isShared()) {
0 ignored issues
show
Bug introduced by
The variable $definition does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
169 4
            foreach ($this->_sharedInstances as $inst) {
170 4
                if ($this->_sharedInstances->getInfo() === $name) {
171 4
                    return $inst;
172
                }
173
            }
174
        }
175
176 20
        $event      = new BeforeServiceLoadedEvent($this, $name, $definition);
177 20
        $this->notify($event);
178
179
        // the event has been stopped
180 20
        if ($event->isStopped()) {
181 1
            $return = $event->getReturnValue();
182
183 1
            if ($definition->isShared()) {
184
                $this->_sharedInstances->attach($return, $name);
185
            }
186
187 1
            return $return;
188
        }
189
190 19
        $return = $definition->invoke($this, $name);
191 19
        if ($definition->isShared()) {
192 4
            $this->_sharedInstances->attach($return, $name);
193
        }
194
195 19
        $afterEvent = new AfterServiceLoadedEvent($this, $name, $definition, $return);
196 19
        $this->notify($afterEvent);
197
198 19
        return $return;
199
    }
200
201
    /**
202
     * Loads properties from an INI file as definitions. 
203
     * Theses properties can then be referenced like @propName in other 
204
     * definitions.
205
     * 
206
     * @param string      $iniFile  Path/to/file.ini
207
     * @param null|string $category The INI category to be parsed
208
     * 
209
     * @return Container
210
     * @throws Exception
211
     */
212 5
    public function iniProperties($iniFile, $category = null)
213
    {
214 5
        if (!is_file($iniFile) || !is_readable($iniFile)) {
215
            throw new Exception('INI file not found/readable: '. $iniFile);
216
        }
217
        
218 5
        $props = parse_ini_file($iniFile, ($category !== null));
219 5
        if ($category !== null) {
220
            $props = (isset($props[$category]) ? $props[$category] : false);
221
        }
222
        
223 5
        if (!is_array($props)) {
224
            throw new Exception("No properties found in: $iniFile [$category]");
225
        }
226
        
227 5
        foreach ($props as $key => $prop) {
228 5
            $this->properties[$key] = str_replace(':packageDir', dirname($iniFile), $prop);
229 5
            $this->propertiesMap[$key] = ":". $key;
230
        }
231
        
232 5
        return $this;
233
    }
234
    
235
    /**
236
     * Returns a property (or $default if not defined) 
237
     * 
238
     * @param string $propName The property name
239
     * @param mixed  $default  Default value if the property is not defined
240
     * 
241
     * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use object|integer|double|null|array|boolean|string.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
242
     */
243 3
    public function getProperty($propName, $default = null)
244
    {
245 3
        return (array_key_exists($propName, $this->properties) ? 
246 3
            $this->propertizeString($this->properties[$propName]) : 
247 1
            (is_string($default) ?
248
                $this->propertizeString($default) :
249 3
                $default
250
            )
251
        );
252
    }
253
    
254
    /**
255
     * Defines a property.
256
     * 
257
     * If the $value is null, the property will be unset.
258
     * 
259
     * It recommended to store only strings as property values. Register a
260
     * new Di definition for any other type.
261
     * 
262
     * @param string      $propName Property name
263
     * @param null|string $value    The prop value
264
     * 
265
     * @return Container
266
     */
267 7
    public function setProperty($propName, $value = null)
268
    {
269 7
        if (array_key_exists($propName, $this->properties) && $value === null) {
270
            unset($this->properties[$propName]);
271
            unset($this->propertiesMap[$propName]);
272
            return $this;
273
        }
274
        
275 7
        $this->properties[(string)$propName] = (string)$value;
276 7
        $this->propertiesMap[(string)$propName] = ":". (string)$propName;
277
        
278 7
        return $this;
279
    }
280
    
281
    
282
    /**
283
     * Transform properties references to their respective value
284
     * 
285
     * @param string $str String to be transformed
286
     * 
287
     * @return string
288
     */
289 22
    public function propertizeString($str)
290
    {
291 22
        return str_replace(
292 22
            array_values($this->propertiesMap),
293 22
            array_values($this->properties),
294
            $str
295
        );
296
    }
297
    
298
    /**
299
     * Unregisters a definition
300
     * 
301
     * @param string $name Identifier
302
     * 
303
     * @throws Exceptions\DefinitionNotFoundException if $name isn't a valid identifier
304
     * @return boolean true on success
305
     */
306 3
    public function unregister($name)
307
    {
308 3
        if (!$this->has($name)) {
309 1
            throw new Exceptions\DefinitionNotFoundException($name);
310
        }
311
312 2
        $this->store->detach($this->getDefinition($name));
313 2
        foreach ($this->_sharedInstances as $inst => $defName) {
314 2
            if ($defName === $name) {
315
                $this->_sharedInstances->detach((object)$inst);
316 2
                break;
317
            }
318
        }
319
320 2
        return true;
321
    }
322
    
323
    /**
324
     * Tells if a definition exists at $offset
325
     * 
326
     * @param string $name Identifier
327
     * 
328
     * @return boolean
329
     */
330 27
    public function has($name)
331
    {
332 27
        foreach ($this->store as $def) {
333 27
            if ($this->store->getInfo() === $name) {
334 27
                return true;
335
            }
336
        }
337
338 15
        return false;
339
    }
340
341
    /**
342
     * Returns a Definition
343
     *
344
     * @param string $name Identifier
345
     *
346
     * @return DefinitionInterface
347
     * @throws Exceptions\DefinitionNotFoundException if $name isn't a valid identifier
348
     */
349 4
    public function getDefinition($name)
350
    {
351 4
        foreach ($this->store as $def) {
352 4
            if ($this->store->getInfo() === $name) {
353 4
                return $def;
354
            }
355
        }
356
357
        throw new Exceptions\DefinitionNotFoundException($name);
358
    }
359
    
360
    /**
361
     * Tells if a definition is registered at $offset
362
     * 
363
     * @param string $offset Identifier
364
     * 
365
     * @return boolean
366
     */
367
    public function offsetExists($offset)
368
    {
369
        return $this->has($offset);
370
    }
371
    
372
    /**
373
     * Loads and returns a definition 
374
     * 
375
     * @param string $offset Identifier
376
     * 
377
     * @return mixed
378
     */
379
    public function offsetGet($offset)
380
    {
381
        return $this->get($offset);
382
    }
383
    
384
    /**
385
     * Registers a definition
386
     * 
387
     * @param string $offset Identifier
388
     * @param mixed  $value  Definition
389
     * 
390
     * @return Container
391
     */
392 16
    public function offsetSet($offset, $value)
393
    {
394 16
        return $this->set($offset, $value);
395
    }
396
    
397
    /**
398
     * Unregisters a Definition
399
     * 
400
     * @param string $offset Identifier
401
     * 
402
     * @return boolean
403
     */
404
    public function offsetUnset($offset)
405
    {
406
        return $this->unregister($offset);
407
    }
408
409
    /**
410
     * Adds a delegate/backup Container.
411
     *
412
     * @param ContainerInterface $container
413
     *
414
     * @return ContainerInterface
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use Container.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
415
     */
416
    public function delegate(ContainerInterface $container)
417
    {
418
        if ($this->delegates->contains($container)) {
419
            return $this;
420
        }
421
422
        $this->delegates->attach($container);
423
424
        return $this;
425
    }
426
427
    /**
428
     * Tells if a service is in a delegated Container
429
     *
430
     * @param string $name
431
     *
432
     * @return boolean
433
     */
434
    public function hasInDelegate($name)
435
    {
436
        foreach ($this->delegates as $container) {
437
            /** @var ContainerInterface $container */
438
            if ($container->has($name)) {
439
                return true;
440
            }
441
        }
442
443
        return false;
444
    }
445
446
    /**
447
     * Loads a definition from the first delegated Container having in (FIFO)
448
     *
449
     * @param string $name Service identifier
450
     *
451
     * @throws Exceptions\DefinitionNotFoundException when the service is not found
452
     * @return mixed
453
     */
454 6
    public function getFromDelegate($name)
455
    {
456 6
        foreach ($this->delegates as $container) {
457
            /** @var ContainerInterface $container */
458
            if ($container->has($name)) {
459
                return $container->get($name);
460
            }
461
        }
462
463 6
        throw new Exceptions\DefinitionNotFoundException($name);
464
    }
465
466
    /**
467
     * Search definitions
468
     *
469
     * @param array $query Search query
470
     *
471
     * @return array<DefinitionInterface>
472
     */
473 1
    public function search(array $query)
474
    {
475 1
        $results = array();
476 1
        foreach ($this->store as $def) {
477 1
            $name = $this->store->getInfo();
478
            /** @var DefinitionInterface $def */
479 1
            if ($def->match($query, $this)) {
480 1
                $results[$name] = $def;
481
            }
482
        }
483
484 1
        return $results;
485
    }
486
}