Completed
Push — feat-events-tags ( 033a2d...8b9dcd )
by Julien
02:32
created

Container   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 470
Duplicated Lines 0 %

Coupling/Cohesion

Dependencies 10

Test Coverage

Coverage 74.22%

Importance

Changes 24
Bugs 11 Features 7
Metric Value
wmc 58
c 24
b 11
f 7
cbo 10
dl 0
loc 470
ccs 95
cts 128
cp 0.7422
rs 4.8387

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A set() 0 18 2
C get() 0 54 10
A getDefinitionData() 0 13 2
C iniProperties() 0 22 7
A getProperty() 0 10 3
A setProperty() 0 13 3
A propertizeString() 0 8 1
A unregister() 0 11 2
A isShared() 0 10 2
A has() 0 4 1
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 hasInDelegate() 0 11 3
A getFromDelegate() 0 11 3
D search() 0 31 9
A searchQueryToRegex() 0 14 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\Events\AfterServiceLoadedEvent;
36
use Fwk\Di\Events\AfterServiceRegisteredEvent;
37
use Fwk\Di\Events\BeforeServiceLoadedEvent;
38
use Fwk\Di\Events\BeforeServiceRegisteredEvent;
39
use Fwk\Di\Exceptions\DefinitionNotFoundException;
40
use Fwk\Di\Exceptions\SearchException;
41
use Fwk\Events\Dispatcher;
42
use \ArrayAccess;
43
use Interop\Container\ContainerInterface;
44
use \SplObjectStorage;
45
46
/**
47
 * Container
48
 * 
49
 * THE Dependency Injection Container.
50
 *
51
 * @category Container
52
 * @package  Fwk\Di
53
 * @author   Julien Ballestracci <[email protected]>
54
 * @license  http://www.opensource.org/licenses/bsd-license.php  BSD License
55
 * @link     http://www.nitronet.org/fwk
56
 */
57
class Container extends Dispatcher implements ArrayAccess, ContainerInterface
58
{
59
    /**
60
     * The objects store
61
     * @var array
62
     */
63
    protected $store = array();
64
65
    /**
66
     * Informations about stored objects
67
     * @var array
68
     */
69
    protected $storeData = array();
70
    
71
    /**
72
     * Container Properties
73
     * @var array
74
     */
75
    protected $properties = array();
76
77
    /**
78
     * Properties Keys (cached)
79
     * @var array
80
     */
81
    protected $propertiesMap = array();
82
83
    /**
84
     * Delegates Containers
85
     * @var SplObjectStorage
86
     */
87
    protected $delegates;
88
    
89
    /**
90
     * Constructor
91
     * 
92
     * @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...
93 40
     */
94
    public function __construct()
95 40
    {
96 40
        $this->set('self', $this, true);
97 40
        $this->delegates = new SplObjectStorage();
98
    }
99
    
100
    /**
101
     * Registers a definition
102
     * 
103
     * @param string  $name       Identifier
104
     * @param mixed   $definition Definition, callable or value
105
     * @param boolean $shared     Should the instance be "shared" (singleton)
106
     * @param array   $data       Meta-data associated with this definition
107
     * 
108
     * @return Container 
109 40
     */
110
    public function set($name, $definition, $shared = false, 
111
        array $data = array()
112 40
    ) {
113 40
        $event = new BeforeServiceRegisteredEvent($this, $name, $definition, $data);
114
        $this->notify($event);
115 40
116
        if ($event->isStopped()) {
117
            return $this;
118
        }
119 40
120 40
        $data = array_merge(array('__fwk_di_shared'   => $shared), $data);
121 40
        $this->store[$name] = $definition;
122
        $this->storeData[$name] = $data;
123 40
124
        $this->notify(new AfterServiceRegisteredEvent($this, $name, $definition, $data));
125 40
126
        return $this;
127
    }
128
    
129
    /**
130
     * Load and returns a definition
131
     * 
132
     * @param string $name Identifier
133
     * 
134
     * @throws Exceptions\DefinitionNotFoundException if $name isn't a valid identifier
135
     * @return mixed
136 26
     */
137
    public function get($name)
138 26
    {
139
        if ($name instanceof Reference) {
140
            $name = $name->getName();
141
        }
142 26
        
143 6
        if (!$this->has($name)) {
144
            return $this->getFromDelegate($name);
145
        }
146 20
        
147 20
        $data       =& $this->storeData[$name];
148 20
        if ($data['__fwk_di_shared'] === true
149 20
            && isset($data['__fwk_di_shared_inst'])
150 4
        ) {
151
            return $data['__fwk_di_shared_inst'];
152
        }
153 20
154 20
        $definition = $this->store[$name];
155
        $event      = new BeforeServiceLoadedEvent($this, $name, $definition, $data);
156 20
157
        $this->notify($event);
158
159 20
        // the event is stopped
160 1
        if ($event->isStopped()) {
161
            $return = $event->getReturnValue();
162 1
163
            if ($data['__fwk_di_shared'] === true) {
164
                $this->storeData[$name]['__fwk_di_shared_inst'] = $return;
165
            }
166 1
167
            return $return;
168
        }
169 19
170
        $this->storeData[$name] = $data = $event->getDefinitionData();
171 19
172 7
        if ($definition instanceof InvokableInterface) {
173 19
            $return = $definition->invoke($this, $name);
174 7
        } elseif (is_callable($definition)) {
175 7
            $return = call_user_func_array($definition, array($this));
176 6
        } else {
177
            $return = $definition;
178
        }
179 19
        
180 4
        if ($data['__fwk_di_shared'] === true) {
181 4
            $this->storeData[$name]['__fwk_di_shared_inst'] = $return;
182
        }
183 19
184 19
        $afterEvent = new AfterServiceLoadedEvent($this, $name, $definition, $data, $return);
185
        $this->notify($afterEvent);
186 19
187
        $this->storeData[$name] = $afterEvent->getDefinitionData();
188 19
189
        return $return;
190
    }
191
192
    /**
193
     * Returns data associated with the given definition (without internals).
194
     *
195
     * @param string $name Service name
196
     *
197
     * @throws DefinitionNotFoundException
198 1
     */
199
    public function getDefinitionData($name)
200 1
    {
201
        if (!$this->has($name)) {
202
            throw new DefinitionNotFoundException($name);
203
        }
204 1
205 1
        $data = $this->storeData[$name];
206 1
        $keys = array_filter(array_keys($data), function($key) {
207 1
            return (strpos($key, '__fwk_di', 0) === false);
208
        });
209 1
210
        return array_intersect_key($data, array_flip($keys));
211
    }
212
    
213
    /**
214
     * Loads properties from an INI file as definitions. 
215
     * Theses properties can then be referenced like @propName in other 
216
     * definitions.
217
     * 
218
     * @param string      $iniFile  Path/to/file.ini
219
     * @param null|string $category The INI category to be parsed
220
     * 
221
     * @return Container
222
     * @throws Exception
223 5
     */
224
    public function iniProperties($iniFile, $category = null)
225 5
    {
226
        if (!is_file($iniFile) || !is_readable($iniFile)) {
227
            throw new Exception('INI file not found/readable: '. $iniFile);
228
        }
229 5
        
230 5
        $props = parse_ini_file($iniFile, ($category !== null));
231
        if ($category !== null) {
232
            $props = (isset($props[$category]) ? $props[$category] : false);
233
        }
234 5
        
235
        if (!is_array($props)) {
236
            throw new Exception("No properties found in: $iniFile [$category]");
237
        }
238 5
        
239 5
        foreach ($props as $key => $prop) {
240 5
            $this->properties[$key] = str_replace(':packageDir', dirname($iniFile), $prop);
241 5
            $this->propertiesMap[$key] = ":". $key;
242
        }
243 5
        
244
        return $this;
245
    }
246
    
247
    /**
248
     * Returns a property (or $default if not defined) 
249
     * 
250
     * @param string $propName The property name
251
     * @param mixed  $default  Default value if the property is not defined
252
     * 
253
     * @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...
254 3
     */
255
    public function getProperty($propName, $default = null)
256 3
    {
257 3
        return (array_key_exists($propName, $this->properties) ? 
258 1
            $this->propertizeString($this->properties[$propName]) : 
259 1
            (is_string($default) ?
260
                $this->propertizeString($default) :
261
                $default
262 3
            )
263
        );
264
    }
265
    
266
    /**
267
     * Defines a property.
268
     * 
269
     * If the $value is null, the property will be unset.
270
     * 
271
     * It recommended to store only strings as property values. Register a
272
     * new Di definition for any other type.
273
     * 
274
     * @param string      $propName Property name
275
     * @param null|string $value    The prop value
276
     * 
277
     * @return Container
278 7
     */
279
    public function setProperty($propName, $value = null)
280 7
    {
281
        if (array_key_exists($propName, $this->properties) && $value === null) {
282
            unset($this->properties[$propName]);
283
            unset($this->propertiesMap[$propName]);
284
            return $this;
285
        }
286 7
        
287 7
        $this->properties[(string)$propName] = (string)$value;
288
        $this->propertiesMap[(string)$propName] = ":". (string)$propName;
289 7
        
290
        return $this;
291
    }
292
    
293
    
294
    /**
295
     * Transform properties references to their respective value
296
     * 
297
     * @param string $str String to be transformed
298
     * 
299
     * @return string
300 21
     */
301
    public function propertizeString($str)
302 21
    {
303 21
        return str_replace(
304 21
            array_values($this->propertiesMap),
305
            array_values($this->properties),
306 21
            $str
307
        );
308
    }
309
    
310
    /**
311
     * Unregisters a definition
312
     * 
313
     * @param string $name Identifier
314
     * 
315
     * @throws Exceptions\DefinitionNotFoundException if $name isn't a valid identifier
316
     * @return boolean true on success
317 3
     */
318
    public function unregister($name)
319 3
    {
320 1
        if (!$this->has($name)) {
321
            throw new Exceptions\DefinitionNotFoundException($name);
322
        }
323 2
324 2
        unset($this->storeData[$name]);
325
        unset($this->store[$name]);
326 2
        
327
        return true;
328
    }
329
    
330
    /**
331
     * Tells if a definition has been flagged has "shared" (singleton)
332
     * 
333
     * @param string $name Identifier
334
     * 
335
     * @throws Exceptions\DefinitionNotFoundException if $name isn't a valid identifier
336
     * @return boolean
337 9
     */
338
    public function isShared($name)
339 9
    {
340 1
        if (!$this->has($name)) {
341
            throw new Exceptions\DefinitionNotFoundException($name);
342
        }
343 8
        
344
        $data = $this->storeData[$name];
345 8
        
346
        return (bool)$data['__fwk_di_shared'];
347
    }
348
    
349
    /**
350
     * Tells if a definition exists at $offset
351
     * 
352
     * @param string $name Identifier
353
     * 
354
     * @return boolean
355 29
     */
356
    public function has($name)
357 29
    {
358
        return array_key_exists($name, $this->store);
359
    }
360
    
361
    /**
362
     * Tells if a definition is registered at $offset
363
     * 
364
     * @param string $offset Identifier
365
     * 
366
     * @return boolean
367
     */
368
    public function offsetExists($offset)
369
    {
370
        return $this->has($offset);
371
    }
372
    
373
    /**
374
     * Loads and returns a definition 
375
     * 
376
     * @param string $offset Identifier
377
     * 
378
     * @return mixed
379
     */
380
    public function offsetGet($offset)
381
    {
382
        return $this->get($offset);
383
    }
384
    
385
    /**
386
     * Registers a definition
387
     * 
388
     * @param string $offset Identifier
389
     * @param mixed  $value  Definition
390
     * 
391
     * @return Container
392 16
     */
393
    public function offsetSet($offset, $value)
394 16
    {
395
        return $this->set($offset, $value);
396
    }
397
    
398
    /**
399
     * Unregisters a Definition
400
     * 
401
     * @param string $offset Identifier
402
     * 
403
     * @return boolean
404
     */
405
    public function offsetUnset($offset)
406
    {
407
        return $this->unregister($offset);
408
    }
409
410
    /**
411
     * Adds a delegate/backup Container.
412
     *
413
     * @param ContainerInterface $container
414
     *
415
     * @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...
416
     */
417
    public function delegate(ContainerInterface $container)
418
    {
419
        if ($this->delegates->contains($container)) {
420
            return $this;
421
        }
422
423
        $this->delegates->attach($container);
424
425
        return $this;
426
    }
427
428
    /**
429
     * Tells if a service is in a delegated Container
430
     *
431
     * @param string $name
432
     *
433
     * @return boolean
434
     */
435
    public function hasInDelegate($name)
436
    {
437
        foreach ($this->delegates as $container) {
438
            /** @var ContainerInterface $container */
439
            if ($container->has($name)) {
440
                return true;
441
            }
442
        }
443
444
        return false;
445
    }
446
447
    /**
448
     * Loads a definition from the first delegated Container having in (FIFO)
449
     *
450
     * @param string $name Service identifier
451
     *
452
     * @throws Exceptions\DefinitionNotFoundException when the service is not found
453
     * @return mixed
454 6
     */
455
    public function getFromDelegate($name)
456 6
    {
457
        foreach ($this->delegates as $container) {
458
            /** @var ContainerInterface $container */
459
            if ($container->has($name)) {
460
                return $container->get($name);
461 6
            }
462
        }
463 6
464
        throw new Exceptions\DefinitionNotFoundException($name);
465
    }
466
467
    /**
468
     * @param array $query
469
     *
470
     * @return array
471
     */
472
    public function search(array $query)
473
    {
474
        $results = array();
475
        $collection = $this->storeData;
476
        $queryValuesCache = array();
477
478
        foreach ($collection as $definition => $data) {
479
            foreach ($query as $key => $queryValue) {
480
                if (!array_key_exists($key, $data)) {
481
                    continue;
482
                }
483
484
                if (!is_string($data[$key]) || !is_string($queryValue)) {
485
                    if ($data[$key] === $queryValue) {
486
                        $results[$definition] = $data;
487
                    }
488
                    continue;
489
                }
490
491
                if (!isset($queryValuesCache[$key])) {
492
                    $queryValuesCache[$key] = $this->searchQueryToRegex($queryValue);
493
                }
494
495
                if (preg_match($queryValuesCache[$key], $data[$key])) {
496
                    $results[$definition] = $data;
497
                }
498
            }
499
        }
500
501
        return $results;
502
    }
503
504
    /**
505
     * Transforms a wildcard to a regex
506
     *
507
     * @param string $value
508
     *
509
     * @return string
510
     * @throws SearchException
511
     */
512
    protected function searchQueryToRegex($value)
513
    {
514
        $original = $value;
515
        $value = $this->propertizeString($value);
516
        if (!is_string($value)) {
517
            throw new SearchException("Invalid Query: '$original' because of a non-string value.");
518
        }
519
520
        if (empty($value)) {
521
            return "/(.+){1,}/";
522
        }
523
524
        return '/^'. str_replace(array('?', '*'), array('(.+){1}', '(.+){1,}'), $value) .'$/';
525
    }
526
}