Completed
Push — develop ( 251d34...03436e )
by Paul
02:15
created

Container::resolve()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace PhpUnitGen\Container;
4
5
use PhpUnitGen\Exception\ContainerException;
6
use PhpUnitGen\Exception\NotFoundException;
7
use Psr\Container\ContainerInterface;
8
use Respect\Validation\Validator;
9
10
/**
11
 * Class Container.
12
 *
13
 * @author     Paul Thébaud <[email protected]>.
14
 * @copyright  2017-2018 Paul Thébaud <[email protected]>.
15
 * @license    https://opensource.org/licenses/MIT The MIT license.
16
 * @link       https://github.com/paul-thebaud/phpunit-generator
17
 * @since      Class available since Release 2.0.0.
18
 */
19
class Container implements ContainerInterface
20
{
21
    /**
22
     * @var string[] $autoResolvable All objects resolvable automatically.
23
     */
24
    private static $autoResolvable = [];
25
26
    /**
27
     * @var object[] $instances All objects instances.
28
     */
29
    private static $instances = [];
30
31
    /**
32
     * Add to available services an instance of an object.
33
     *
34
     * @param string $id       The service identifier.
35
     * @param object $instance An object instance.
36
     */
37
    public function setInstance(string $id, object $instance): void
38
    {
39
        self::$instances[$id] = $instance;
40
    }
41
42
    /**
43
     * Add to available services a class which can be construct by the container resolve method.
44
     *
45
     * @param string      $id    The service identifier.
46
     * @param string|null $class The class name, null if it can $id as a class name.
47
     */
48
    public function set(string $id, string $class = null): void
49
    {
50
        self::$autoResolvable[$id] = $class ?? $id;
51
    }
52
53
    /**
54
     * Add to available services all classes and aliases of the autoResolvable.
55
     * Using this method is the same as using set method on each rows.
56
     *
57
     * @param array $autoResolvable
58
     */
59
    public function addAutoResolvableArray(array $autoResolvable): void
60
    {
61
        self::$autoResolvable = array_merge(self::$autoResolvable, $autoResolvable);
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67
    public function get($id): object
68
    {
69
        if (! Validator::stringType()->validate($id)) {
70
            throw new ContainerException('Identifier is not a string.');
71
        }
72
        return $this->resolveInstance($id);
73
    }
74
75
    /**
76
     * {@inheritdoc}
77
     */
78
    public function has($id): bool
79
    {
80
        try {
81
            $this->get($id);
82
        } catch (NotFoundException $exception) {
83
            return false;
84
        } catch (ContainerException $exception) {
85
            return false;
86
        }
87
        return true;
88
    }
89
90
    /**
91
     * Try to retrieve a service instance from the instances array.
92
     *
93
     * @param string $id The service identifier.
94
     *
95
     * @return object The service.
96
     *
97
     * @throws ContainerException If the service identifier is not a string.
98
     */
99
    private function resolveInstance(string $id): object
100
    {
101
        if (Validator::key($id)->validate(self::$instances)) {
102
            return self::$instances[$id];
103
        }
104
        return $this->resolveAutomaticResolvable($id);
105
    }
106
107
    /**
108
     * Try to retrieve a service instance from the automatic resolvable array.
109
     *
110
     * @param string $id The service identifier.
111
     *
112
     * @return object The service.
113
     *
114
     * @throws ContainerException If the service identifier is not a string.
115
     */
116
    private function resolveAutomaticResolvable(string $id): object
117
    {
118
        if (Validator::key($id)->validate(self::$autoResolvable)) {
119
            return self::$instances[$id] = $this->autoResolve(self::$autoResolvable[$id]);
120
        }
121
        return $this->autoResolve($id);
122
    }
123
124
    /**
125
     * Try to automatically create a service.
126
     *
127
     * @param string $class The service class.
128
     *
129
     * @return object The built instance.
130
     *
131
     * @throws ContainerException If the service cannot be constructed.
132
     */
133
    private function autoResolve(string $class): object
134
    {
135
        if (! class_exists($class)) {
136
            throw new ContainerException(sprintf('Class "%s" does not exists.', $class));
137
        }
138
139
        $reflection = new \ReflectionClass($class);
140
141
        if (! $reflection->isInstantiable()) {
142
            throw new ContainerException(sprintf('Class "%s" is not instantiable.', $class));
143
        }
144
        return $this->buildInstance($reflection);
145
    }
146
147
    /**
148
     * Build a new instance of a class from reflection class and auto-resolved constructor arguments.
149
     *
150
     * @param \ReflectionClass $reflection The reflection class.
151
     *
152
     * @return object The built instance.
153
     *
154
     * @throws ContainerException If the class constructor is not public.
155
     */
156
    private function buildInstance(\ReflectionClass $reflection): object
157
    {
158
        if (($constructor = $reflection->getConstructor()) === null) {
159
            return $reflection->newInstance();
160
        }
161
        if (! $constructor->isPublic()) {
162
            throw new ContainerException(
163
                sprintf('Class "%s" has no public constructor.', $reflection->getName())
164
            );
165
        }
166
        $constructorParameters = [];
167
        foreach ($constructor->getParameters() as $parameter) {
168
            $constructorParameters[] = $this->get($parameter->getClass()->getName());
169
        }
170
        return $reflection->newInstanceArgs($constructorParameters);
171
    }
172
}
173