Container   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 193
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 36
eloc 64
c 5
b 0
f 0
dl 0
loc 193
rs 9.52

12 Methods

Rating   Name   Duplication   Size   Complexity  
A closure() 0 3 1
A has() 0 3 1
A singleton() 0 3 1
A transient() 0 3 1
A empty() 0 3 1
A resolveParameters() 0 22 6
C get() 0 31 13
A call() 0 10 3
A isAbstract() 0 9 2
A instantiate() 0 14 4
A delete() 0 3 1
A catch() 0 7 2
1
<?php
2
3
namespace MiladRahimi\PhpContainer;
4
5
use Closure;
6
use MiladRahimi\PhpContainer\Exceptions\ContainerException;
7
use MiladRahimi\PhpContainer\Types\Closure as TClosure;
8
use MiladRahimi\PhpContainer\Types\Transient;
9
use MiladRahimi\PhpContainer\Types\Singleton;
10
use Psr\Container\ContainerInterface;
11
use ReflectionClass;
12
use ReflectionException;
13
use ReflectionFunction;
14
use ReflectionMethod;
15
16
class Container implements ContainerInterface
17
{
18
    /**
19
     * Binding repository
20
     *
21
     * @var Transient[]|Singleton[]
22
     */
23
    private array $repository = [];
24
25
    /**
26
     * Empty the container
27
     */
28
    public function empty()
29
    {
30
        $this->repository = [];
31
    }
32
33
    /**
34
     * Bind a transient dependency
35
     */
36
    public function transient(string $id, $concrete)
37
    {
38
        $this->repository[$id] = new Transient($concrete);
39
    }
40
41
    /**
42
     * Bind a singleton dependency
43
     */
44
    public function singleton(string $id, $concrete)
45
    {
46
        $this->repository[$id] = new Singleton($concrete);
47
    }
48
49
    /**
50
     * Bind a closure dependency
51
     */
52
    public function closure($id, Closure $closure)
53
    {
54
        $this->repository[$id] = new TClosure($closure);
55
    }
56
57
    /**
58
     * Returns true if the container can return an entry for the given identifier.
59
     * Returns false otherwise.
60
     */
61
    public function has(string $id): bool
62
    {
63
        return isset($this->repository[$id]);
64
    }
65
66
    /**
67
     * Check if the given class is abstract or not
68
     *
69
     * @throws ContainerException
70
     */
71
    protected function isAbstract(string $class): bool
72
    {
73
        try {
74
            $reflection = new ReflectionClass($class);
75
        } catch (ReflectionException $e) {
76
            throw new ContainerException("Reflection error for $class", 0, $e);
77
        }
78
79
        return $reflection->isAbstract();
80
    }
81
82
    /**
83
     * Finds an entry of the container by its identifier and returns it.
84
     *
85
     * @throws ContainerException
86
     */
87
    public function get(string $id)
88
    {
89
        if (!isset($this->repository[$id])) {
90
            if (class_exists($id) && !$this->isAbstract($id)) {
91
                return $this->instantiate($id);
92
            }
93
94
            throw new ContainerException("Cannot find $id in the container.");
95
        }
96
97
        $binding = $this->repository[$id];
98
99
        if ($binding instanceof Singleton && $instance = $binding->getInstance()) {
100
            return $instance;
101
        }
102
103
        $concrete = $binding->getConcrete();
104
105
        if (is_string($concrete) && class_exists($concrete)) {
106
            $concrete = $this->instantiate($concrete);
107
        } elseif (is_callable($concrete) && !($binding instanceof TClosure)) {
108
            $concrete = $this->call($concrete);
109
        } elseif (is_object($concrete) && $binding instanceof Transient) {
110
            return clone $concrete;
111
        }
112
113
        if ($binding instanceof Singleton) {
114
            $this->repository[$id]->setInstance($concrete);
115
        }
116
117
        return $concrete;
118
    }
119
120
    /**
121
     * Catch an entry of the container by its identifier without resolving nested dependencies.
122
     *
123
     * @throws ContainerException
124
     */
125
    public function catch(string $id)
126
    {
127
        if (!isset($this->repository[$id])) {
128
            throw new ContainerException("Cannot find $id in the container.");
129
        }
130
131
        return $this->repository[$id]->getConcrete();
132
    }
133
134
    /**
135
     * Get rid of the given binding
136
     */
137
    public function delete($id): void
138
    {
139
        unset($this->repository[$id]);
140
    }
141
142
    /**
143
     * Instantiate the given class
144
     *
145
     * @throws ContainerException
146
     */
147
    public function instantiate(string $class)
148
    {
149
        try {
150
            $reflection = new ReflectionClass($class);
151
152
            $parameters = [];
153
            if ($reflection->hasMethod('__construct')) {
154
                $method = $reflection->getMethod('__construct');
155
                $parameters = $this->resolveParameters($method->getParameters());
156
            }
157
158
            return count($parameters) == 0 ? new $class : $reflection->newInstanceArgs($parameters);
159
        } catch (ReflectionException $e) {
160
            throw new ContainerException('Reflection failed for ' . $class, 0, $e);
161
        }
162
    }
163
164
    /**
165
     * Call the concrete callable
166
     *
167
     * @throws ContainerException
168
     */
169
    public function call($callable)
170
    {
171
        try {
172
            $reflection = is_array($callable)
173
                ? new ReflectionMethod($callable[0], $callable[1])
174
                : new ReflectionFunction($callable);
175
176
            return call_user_func_array($callable, $this->resolveParameters($reflection->getParameters()));
177
        } catch (ReflectionException $e) {
178
            throw new ContainerException('Reflection failed.', 0, $e);
179
        }
180
    }
181
182
    /**
183
     * Resolve dependencies of the given function parameters
184
     *
185
     * @throws ContainerException
186
     */
187
    private function resolveParameters(array $reflectedParameters = []): array
188
    {
189
        $parameters = [];
190
191
        foreach ($reflectedParameters as $parameter) {
192
            if (isset($this->repository['$' . $parameter->getName()])) {
193
                $parameters[] = $this->catch('$' . $parameter->getName());
194
            } elseif (
195
                $parameter->getType() &&
196
                (
197
                    class_exists($parameter->getType()->getName()) ||
198
                    interface_exists($parameter->getType()->getName())
199
                )
200
            ) {
201
                $parameters[] = $this->get($parameter->getType()->getName());
202
            } else {
203
                $defaultValue = $parameter->getDefaultValue();
204
                $parameters[] = $defaultValue;
205
            }
206
        }
207
208
        return $parameters;
209
    }
210
}
211