Passed
Push — develop ( 51b9ff...6cf518 )
by Paul
05:56
created

Container::resolveClass()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 3
eloc 6
c 2
b 1
f 0
nc 3
nop 1
dl 0
loc 9
rs 10
1
<?php
2
3
namespace GeminiLabs\Castor;
4
5
use Closure;
6
use GeminiLabs\Castor\Exceptions\BindingResolutionException;
7
use ReflectionClass;
8
use ReflectionParameter;
9
10
abstract class Container
11
{
12
    /**
13
     * The current globally available container (if any).
14
     *
15
     * @var static
16
     */
17
    protected static $instance;
18
19
    /**
20
     * The container's bound services.
21
     *
22
     * @var array
23
     */
24
    protected $services = [];
25
26
    /**
27
     * The container's bucket items.
28
     *
29
     * @var array
30
     */
31
    protected $bucket = [];
32
33
    /**
34
     * Set the globally available instance of the container.
35
     *
36
     * @return static
37
     */
38
    public static function getInstance()
39
    {
40
        if (is_null(static::$instance)) {
41
            static::$instance = new static();
42
        }
43
44
        return static::$instance;
45
    }
46
47
    /**
48
     * Bind a service to the container.
49
     *
50
     * @param string $alias
51
     * @param mixed  $concrete
52
     *
53
     * @return mixed
54
     */
55
    public function bind($alias, $concrete)
56
    {
57
        $this->services[$alias] = $concrete;
58
    }
59
60
    /**
61
     * Resolve the given type from the container.
62
     * Allow unbound aliases that omit the root namespace
63
     * i.e. 'Controller' translates to 'GeminiLabs\Castor\Controller'.
64
     *
65
     * @param mixed $abstract
66
     *
67
     * @return mixed
68
     */
69
    public function make($abstract)
70
    {
71
        $service = isset($this->services[$abstract])
72
            ? $this->services[$abstract]
73
            : $this->addNamespace($abstract);
74
75
        if (is_callable($service)) {
76
            return call_user_func_array($service, [$this]);
77
        }
78
        if (is_object($service)) {
79
            return $service;
80
        }
81
82
        return $this->resolve($service);
83
    }
84
85
    /**
86
     * Register a shared binding in the container.
87
     *
88
     * @param string               $abstract
89
     * @param \Closure|string|null $concrete
90
     *
91
     * @return void
92
     */
93
    public function singleton($abstract, $concrete)
94
    {
95
        $this->bind($abstract, $this->make($concrete));
96
    }
97
98
    /**
99
     * Dynamically access container bucket items.
100
     *
101
     * @param string $item
102
     *
103
     * @return mixed
104
     */
105
    public function __get($item)
106
    {
107
        return isset($this->bucket[$item])
108
            ? $this->bucket[$item]
109
            : null;
110
    }
111
112
    /**
113
     * Dynamically set container bucket items.
114
     *
115
     * @param string $item
116
     * @param mixed  $value
117
     *
118
     * @return void
119
     */
120
    public function __set($item, $value)
121
    {
122
        $this->bucket[$item] = $value;
123
    }
124
125
    /**
126
     * Prefix the current namespace to the abstract if absent.
127
     *
128
     * @param string $abstract
129
     *
130
     * @return string
131
     */
132
    protected function addNamespace($abstract)
133
    {
134
        if (false === strpos($abstract, __NAMESPACE__) && !class_exists($abstract)) {
135
            $abstract = __NAMESPACE__."\\$abstract";
136
        }
137
138
        return $abstract;
139
    }
140
141
    /**
142
     * @param \ReflectionParameter $parameter
143
     * @return null|\ReflectionClass|\ReflectionNamedType|\ReflectionType
144
     */
145
    protected function getClass($parameter)
146
    {
147
        if (version_compare(phpversion(), '8', '<')) {
148
            return $parameter->getClass(); // @compat PHP < 8
149
        }
150
        return $parameter->getType();
151
    }
152
153
    /**
154
     * Throw an exception that the concrete is not instantiable.
155
     *
156
     * @param string $concrete
157
     *
158
     * @return void
159
     * @throws BindingResolutionException
160
     */
161
    protected function notInstantiable($concrete)
162
    {
163
        $message = "Target [$concrete] is not instantiable.";
164
165
        throw new BindingResolutionException($message);
166
    }
167
168
    /**
169
     * Resolve a class based dependency from the container.
170
     *
171
     * @param mixed $concrete
172
     *
173
     * @return mixed
174
     * @throws BindingResolutionException
175
     */
176
    protected function resolve($concrete)
177
    {
178
        if ($concrete instanceof Closure) {
179
            return $concrete($this);
180
        }
181
182
        $reflector = new ReflectionClass($concrete);
183
184
        if (!$reflector->isInstantiable()) {
185
            return $this->notInstantiable($concrete);
186
        }
187
188
        if (is_null(($constructor = $reflector->getConstructor()))) {
189
            return new $concrete();
190
        }
191
192
        return $reflector->newInstanceArgs(
193
            $this->resolveDependencies($constructor->getParameters())
194
        );
195
    }
196
197
    /**
198
     * Resolve a class based dependency from the container.
199
     *
200
     * @return mixed
201
     * @throws BindingResolutionException
202
     */
203
    protected function resolveClass(ReflectionParameter $parameter)
204
    {
205
        try {
206
            return $this->make($this->getClass($parameter)->getName());
207
        } catch (BindingResolutionException $e) {
208
            if ($parameter->isOptional()) {
209
                return $parameter->getDefaultValue();
210
            }
211
            throw $e;
212
        }
213
    }
214
215
    /**
216
     * Resolve all of the dependencies from the ReflectionParameters.
217
     *
218
     * @return array
219
     */
220
    protected function resolveDependencies(array $dependencies)
221
    {
222
        $results = [];
223
224
        foreach ($dependencies as $dependency) {
225
            // If the class is null, the dependency is a string or some other primitive type
226
            $results[] = !is_null($class = $this->getClass($dependency))
227
                ? $this->resolveClass($dependency)
228
                : null;
229
        }
230
231
        return $results;
232
    }
233
}
234