Completed
Push — master ( aa3af0...5ad1e8 )
by Arnold
03:08
created

Container::assertType()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 7
nc 3
nop 2
dl 0
loc 10
ccs 5
cts 5
cp 1
crap 7
rs 8.8333
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Jasny\Container;
4
5
use Interop\Container\ContainerInterface as InteropContainer;
6
use Psr\Container\ContainerInterface as Psr11Container;
7
use Jasny\Container\Exception\NotFoundException;
8
use Jasny\Container\Exception\NoSubContainerException;
9
10
use function Jasny\expect_type;
11
use Psr\Container\ContainerInterface;
12
13
/**
14
 * This class is a minimalist dependency injection container.
15
 * It has compatibility with container-interop's ContainerInterface and delegate-lookup feature.
16
 */
17
class Container implements InteropContainer, AutowireContainerInterface
18
{
19
    /**
20
     * The delegate lookup.
21
     *
22
     * @var Psr11Container
23
     */
24
    protected $delegateLookupContainer;
25
26
    /**
27
     * The array of closures defining each entry of the container.
28
     *
29
     * @var \Closure[]
30
     */
31
    protected $callbacks;
32
33
    /**
34
     * The array of entries once they have been instantiated.
35
     *
36
     * @var mixed[]
37
     */
38
    protected $instances = [];
39
40
41
    /**
42
     * Class constructor
43
     *
44
     * @param iterable<\Closure> $entries                 Entries must be passed as an array of anonymous functions.
45
     * @param Psr11Container     $delegateLookupContainer Optional delegate lookup container.
46
     */
47
    public function __construct(iterable $entries, Psr11Container $delegateLookupContainer = null)
48 12
    {
49
        $this->callbacks = $entries instanceof \Traversable ? iterator_to_array($entries) : $entries;
0 ignored issues
show
Documentation Bug introduced by
It seems like $entries instanceof Trav...ay($entries) : $entries can also be of type iterable. However, the property $callbacks is declared as type Closure[]. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
50 12
        $this->delegateLookupContainer = $delegateLookupContainer ?: $this;
51 12
    }
52 12
53
54
    /**
55
     * Get an instance from the container.
56
     *
57
     * @param string $identifier
58
     * @return mixed
59
     * @throws NotFoundException  if entry isn't defined
60
     */
61 11
    public function get($identifier)
62
    {
63 11
        expect_type($identifier, 'string');
64
65 11
        if (array_key_exists($identifier, $this->instances)) {
66 2
            return $this->instances[$identifier];
67
        }
68
69 11
        if (!isset($this->callbacks[$identifier])) {
70 2
            return $this->getSub($identifier);
71
        }
72
73 11
        $instance = $this->callbacks[$identifier]($this->delegateLookupContainer);
74 2
        $this->assertType($instance, $identifier);
75
76
        $this->instances[$identifier] = $instance;
77 9
78
        return $instance;
79 9
    }
80 1
81 1
    /**
82
     * Check if the container has an entry.
83
     *
84 8
     * @param string $identifier
85
     * @return bool
86 8
     */
87
    public function has($identifier): bool
88
    {
89
        expect_type($identifier, 'string');
90
91
        return isset($this->callbacks[$identifier]) || $this->hasSub($identifier);
92
    }
93
94
    /**
95
     * Instantiate a new object, autowire dependencies.
96 2
     *
97
     * @param string $class
98 2
     * @param mixed  ...$args
99
     * @return object
100 2
     */
101 1
    public function autowire(string $class, ...$args)
102
    {
103
        return $this->get('Jasny\Autowire\AutowireInterface')->instantiate($class, ...$args);
104 1
    }
105
106
107
    /**
108
     * Check the type of the instance if the identifier is an interface or class name.
109
     *
110
     * @param mixed  $instance
111
     * @param string $identifier
112
     * @throws \TypeError
113
     */
114 2
    protected function assertType($instance, string $identifier): void
115
    {
116 2
        if (
117
            ctype_upper($identifier[0]) &&
118 2
            strpos($identifier, '.') === false &&
119 1
            (class_exists($identifier) || interface_exists($identifier)) &&
120
            !is_a($instance, $identifier)
121
        ) {
122 2
            $type = (is_object($instance) ? get_class($instance) . ' ' : '') . gettype($instance);
123
            throw new \TypeError("Entry is a $type, which does not implement $identifier");
124
        }
125
    }
126
127
128
    /**
129
     * Get an instance from a subcontainer.
130
     *
131
     * @param string $identifier
132 1
     * @return mixed
133
     * @throws NotFoundException
134 1
     */
135 1
    protected function getSub(string $identifier)
136
    {
137
        [$subcontainer, $containerId, $subId] = $this->findSubcontainer($identifier);
138
139
        if (!isset($subcontainer)) {
140
            throw new NotFoundException("Entry \"$identifier\" is not defined.");
141
        }
142
143
        if (!$subcontainer instanceof ContainerInterface) {
144
            throw new NoSubContainerException("Entry \"$containerId\" is not a PSR-11 compatible container");
145
        }
146 3
147
148 3
        return $subcontainer->get($subId);
149
    }
150
151
    /**
152
     * Get an instance from a subcontainer.
153
     *
154
     * @param string $identifier
155
     * @return bool
156
     */
157
    protected function hasSub(string $identifier): bool
158
    {
159
        [$subcontainer, , $subId] = $this->findSubcontainer($identifier);
160
161
        return isset($subcontainer) && $subcontainer instanceof ContainerInterface && $subcontainer->has($subId);
162
    }
163
164
    /**
165
     * Find an subcontainer iterating through the identifier parts.
166
     *
167
     * @param string $identifier
168
     * @return array  [subcontainer, subidentifier]
169
     */
170
    protected function findSubcontainer(string $identifier): array
171
    {
172
        $containerId = null;
173
        $subcontainer = null;
174
        $parts = explode('.', $identifier);
175
        $subParts = [];
176
177
        while ($parts !== []) {
178
            array_unshift($subParts, array_pop($parts));
179
            $containerId = join('.', $parts);
180
181
            if (isset($this->callbacks[$containerId])) {
182
                $subcontainer = $this->get($containerId);
183
                break;
184
            }
185
        }
186
187
        return isset($subcontainer) ? [$subcontainer, $containerId, join('.', $subParts)] : [null, null, null];
188
    }
189
}
190