Passed
Pull Request — master (#32)
by Jitendra
01:51
created

Extension::set()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 3
1
<?php
2
3
namespace PhalconExt\Di;
4
5
/**
6
 * An extension to phalcon di.
7
 *
8
 * @author  Jitendra Adhikari <[email protected]>
9
 * @license MIT
10
 *
11
 * @link    https://github.com/adhocore/phalcon-ext
12
 */
13
trait Extension
14
{
15
    /** @var array Names of services currently resolving */
16
    protected $resolving = [];
17
18
    /** @var array Original services backup */
19
    protected $original  = [];
20
21
    /** @var array The aliases of services */
22
    protected $aliases   = [];
23
24
    public function set($service, $definition, $shared = false)
25
    {
26
        if ('' !== $alias = $this->inferAlias($definition)) {
27
            $this->aliases[$alias] = $service;
28
        }
29
30
        return parent::set($service, $definition, $shared);
31
    }
32
33
    abstract public function get($service, $parameters = null);
34
35
    abstract public function has($service);
36
37
    abstract public function remove($service);
38
39
    /**
40
     * Get names/aliases/class names of registered services.
41
     *
42
     * @return array
43
     */
44
    public function services(): array
45
    {
46
        return \array_merge(
47
            \array_keys($this->_services),
48
            \array_keys($this->aliases)
49
        );
50
    }
51
52
    /**
53
     * Resolve all the dependencies for a class FQCN and instantiate it.
54
     *
55
     * @param string $class      FQCN
56
     * @param array  $parameters Parameters
57
     *
58
     * @return object
59
     */
60
    public function resolve(string $class, array $parameters = [])
61
    {
62
        if ($this->has($class)) {
63
            return $this->get($class, $parameters);
64
        }
65
66
        if ($this->aliases[$class] ?? null) {
67
            return $this->get($this->aliases[$class]);
68
        }
69
70
        if ($this->resolving[$class] ?? null) {
71
            throw new \RuntimeException('Cyclic dependency for class: ' . $class);
72
        }
73
74
        $this->resolving[$class] = true;
75
        $this->set($class, $resolved = $this->instantiate($class, $parameters), true);
76
        unset($this->resolving[$class]);
77
78
        return $resolved;
79
    }
80
81
    /**
82
     * Instantiate class FQCN by constructor injecting the dependencies from the DI.
83
     *
84
     * @param string $class      FQCN
85
     * @param array  $parameters Parameters
86
     *
87
     * @return object
88
     */
89
    protected function instantiate(string $class, array $parameters = [])
90
    {
91
        $reflector = new \ReflectionClass($class);
92
93
        if (!$reflector->isInstantiable()) {
94
            throw new \RuntimeException('Cannot instantiate class: ' . $class);
95
        }
96
97
        if (!$reflector->getConstructor()) {
98
            return $reflector->newInstance();
99
        }
100
101
        if ([] === $dependencies = $reflector->getConstructor()->getParameters()) {
102
            return $reflector->newInstance();
103
        }
104
105
        return $reflector->newInstanceArgs(
106
            $this->resolveDependencies($dependencies, $parameters)
107
        );
108
    }
109
110
    /**
111
     * Resolve dependencies of a class.
112
     *
113
     * @param array $dependencies
114
     * @param array $parameters
115
     *
116
     * @return mixed
117
     */
118
    protected function resolveDependencies(array $dependencies, array $parameters)
119
    {
120
        $resolved = [];
121
122
        foreach ($dependencies as $dependency) {
123
            $name = $dependency->name;
124
125
            // Already available in parameters.
126
            if (isset($parameters[$name])) {
127
                $resolved[] = $parameters[$name];
128
            }
129
            // Already registered in DI.
130
            elseif ($this->has($name)) {
131
                $resolved[] = $this->get($name);
132
            } else {
133
                $resolved[] = $this->resolveDependency($dependency);
134
            }
135
        }
136
137
        return $resolved;
138
    }
139
140
    /**
141
     * Resolve a dependency.
142
     *
143
     * @param \ReflectionParameter $dependency
144
     *
145
     * @return mixed
146
     */
147
    protected function resolveDependency(\ReflectionParameter $dependency)
148
    {
149
        $allowsNull = $dependency->allowsNull();
150
151
        // Is a class and needs to be resolved OR is nullable.
152
        if ($subClass = $dependency->getClass()) {
153
            return $this->resolveSubClassOrNullable($subClass->name, $allowsNull);
154
        }
155
        if ($allowsNull) {
156
            return null;
157
        }
158
159
        // Use default value.
160
        if ($dependency->isOptional()) {
161
            return $dependency->getDefaultValue();
162
        }
163
164
        throw new \RuntimeException('Cannot resolve dependency: $' . $dependency->name);
165
    }
166
167
    /**
168
     * Resolve subClass or nullable.
169
     *
170
     * @param string $subClass
171
     * @param bool   $allowsNull
172
     *
173
     * @return mixed
174
     */
175
    protected function resolveSubClassOrNullable(string $subClass, bool $allowsNull = false)
176
    {
177
        try {
178
            return $this->resolve($subClass);
179
        } catch (\Throwable $e) {
180
            if (!$allowsNull) {
181
                throw $e;
182
            }
183
        }
184
185
        return null;
186
    }
187
188
    /**
189
     * Replace services with another one. Great for test mocks.
190
     *
191
     * @param array $services
192
     *
193
     * @return self
194
     */
195
    public function replace(array $services): self
196
    {
197
        foreach ($services as $name => $definition) {
198
            if ($this->has($name)) {
199
                $this->original[$name] = $this->get($name);
200
                $this->remove($name);
201
            }
202
203
            $this->set($name, $definition);
204
        }
205
206
        return $this;
207
    }
208
209
    /**
210
     * Restore given service or all.
211
     *
212
     * @param array|null $name
213
     *
214
     * @return self
215
     */
216
    public function restore(array $names = null): self
217
    {
218
        foreach ($names ?? \array_keys($this->original) as $name) {
219
            if ($this->has($name)) {
220
                $this->remove($name);
221
                $this->set($name, $this->original[$name], true);
222
            }
223
224
            unset($this->original[$name]);
225
        }
226
227
        return $this;
228
    }
229
230
    /**
231
     * Register aliases for services.
232
     *
233
     * @param array $aliases
234
     *
235
     * @return self
236
     */
237
    public function registerAliases(array $aliases = []): self
238
    {
239
        $this->aliases += $aliases;
240
241
        foreach ($this->_services as $name => $service) {
242
            if ('' !== $alias = $this->inferAlias($service->getDefinition())) {
243
                $this->aliases[$alias] = $name;
244
            }
245
        }
246
247
        return $this;
248
    }
249
250
    /**
251
     * Infer alias of service definition.
252
     *
253
     * @param mixed $definition
254
     *
255
     * @return string
256
     */
257
    protected function inferAlias($definition): string
258
    {
259
        if (\is_object($definition) && !$definition instanceof \Closure) {
260
            return \get_class($definition);
261
        }
262
263
        return \is_string($definition) ? $definition : '';
264
    }
265
}
266