Services::build()   A
last analyzed

Complexity

Conditions 4
Paths 8

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 19
nc 8
nop 1
dl 0
loc 32
ccs 18
cts 18
cp 1
crap 4
rs 9.6333
c 0
b 0
f 0
1
<?php
2
declare(strict_types = 1);
3
4
namespace Innmind\Compose;
5
6
use Innmind\Compose\{
7
    Definition\Name,
8
    Definition\Service,
9
    Definition\Service\Argument,
10
    Definition\Service\Constructor,
11
    Definition\Dependency,
12
    Compilation,
13
    Exception\ArgumentNotProvided,
14
    Exception\ArgumentNotDefined,
15
    Exception\CircularDependency,
16
    Exception\ServiceNotFound,
17
    Exception\LogicException,
18
};
19
use Innmind\Immutable\{
20
    Sequence,
21
    MapInterface,
22
    Map,
23
    Pair,
24
    Stream,
25
    SetInterface,
26
    Set
27
};
28
29
final class Services
30
{
31
    private $definitions;
32
    private $exposed;
33
    private $arguments;
34
    private $dependencies;
35
    private $building;
36
    private $instances;
37
    private $feeding = false;
38
39 207
    public function __construct(
40
        Arguments $arguments,
41
        Dependencies $dependencies,
42
        Service ...$definitions
43
    ) {
44 207
        $this->arguments = $arguments;
45 207
        $this->dependencies = $dependencies;
46 207
        $this->definitions = Sequence::of(...$definitions)->reduce(
47 207
            new Map('string', Service::class),
48 207
            static function(Map $definitions, Service $definition): Map {
49 201
                return $definitions->put(
50 201
                    (string) $definition->name(),
51 201
                    $definition
52
                );
53 207
            }
54
        );
55 207
        $this->exposed = $this
56 207
            ->definitions
57 207
            ->filter(static function(string $name, Service $definition): bool {
58 201
                return $definition->exposed();
59 207
            })
60 207
            ->map(static function(string $name, Service $definition): Pair {
61 79
                return new Pair(
62 79
                    (string) $definition->exposedAs(),
63 79
                    $definition
64
                );
65 207
            });
66
67 207
        $this->building = Stream::of('string');
68 207
        $this->instances = new Map('string', 'object');
69 207
    }
70
71 8
    public function expose(Name $name, Name $as): self
72
    {
73 8
        $service = $this->get($name)->exposeAs($as);
74
75 8
        $self = clone $this;
76 8
        $self->definitions = $self->definitions->put(
77 8
            (string) $name,
78 8
            $service
79
        );
80 8
        $self->exposed = $self->exposed->put(
81 8
            (string) $as,
82 8
            $service
83
        );
84
85 8
        return $self;
86
    }
87
88 9
    public function stack(Name $name, Name $highest, Name $lower, Name ...$rest): self
89
    {
90 9
        $stack = Sequence::of($highest, $lower, ...$rest)->reverse();
91 9
        $stacked = Sequence::of(
92 9
            $this->decorate(
93 9
                $stack->get(1),
94 9
                $stack->first(),
95 9
                $stack->size() === 2 ? $name : null
96
            )
97
        );
98
99 9
        $stacked = $stack->drop(2)->dropEnd(1)->reduce(
100 9
            $stacked,
101 9
            function(Sequence $stacked, Name $decorator): Sequence {
102 1
                return $stacked->add(
103 1
                    $this->decorate($decorator, $stacked->last()->name())
104
                );
105 9
            }
106
        );
107
108 9
        if ($stack->size() > 2) {
109 8
            $stacked = $stacked->add(
110 8
                $this->decorate($highest, $stacked->last()->name(), $name)
111
            );
112
        }
113
114 9
        $self = clone $this;
115 9
        $self->definitions = $stacked->reduce(
116 9
            $self->definitions,
117 9
            static function(Map $definitions, Service $service): Map {
118 9
                return $definitions->put(
119 9
                    (string) $service->name(),
120 9
                    $service
121
                );
122 9
            }
123
        );
124
125 9
        return $self;
126
    }
127
128
    /**
129
     * This method must only be called from Dependencies::bind() through self::inject()
130
     *
131
     * The goal is to iteratively replace in Services the initial Dependency
132
     * instance by its resolved version, so other Dependency instances can rely
133
     * on it
134
     */
135 8
    public function feed(Name $dependency): self
136
    {
137 8
        if (!$this->feeding) {
138 2
            throw new LogicException;
139
        }
140
141 7
        $this->dependencies = $this->dependencies->feed($dependency, $this);
142
143 7
        return $this;
144
    }
145
146
    /**
147
     * @param MapInterface<string, mixed> $arguments
148
     */
149 37
    public function inject(MapInterface $arguments): self
150
    {
151
        try {
152 37
            $self = clone $this;
153 37
            $self->arguments = $self->arguments->bind($arguments);
154 37
            $self->building = $self->building->clear();
155 37
            $self->instances = $self->instances->clear();
156 37
            $self->feeding = true;
157
158 37
            return $self->dependencies->bind($self);
159
        } finally {
160 37
            $self->feeding = false;
161
        }
162
    }
163
164 114
    public function build(Name $name): object
165
    {
166 114
        $definition = $this->get($name);
167 114
        $name = $definition->name();
168
169 114
        if ($this->instances->contains((string) $name)) {
170 91
            return $this->instances->get((string) $name);
171
        }
172
173
        try {
174 114
            if ($this->building->contains((string) $name)) {
175 2
                throw new CircularDependency(
176
                    (string) $this
177 2
                        ->building
178 2
                        ->add((string) $name)
179 2
                        ->join(' -> ')
180
                );
181
            }
182
183 114
            $this->building = $this->building->add((string) $name);
184
185 114
            $service = $definition->build($this);
186
187 113
            $this->instances = $this->instances->put((string) $name, $service);
188 113
            $this->building = $this->building->dropEnd(1);
189 4
        } catch (\Throwable $e) {
190 4
            $this->building = $this->building->clear();
191
192 4
            throw $e;
193
        }
194
195 113
        return $service;
196
    }
197
198 45
    public function arguments(): Arguments
199
    {
200 45
        return $this->arguments;
201
    }
202
203 32
    public function dependencies(): Dependencies
204
    {
205 32
        return $this->dependencies;
206
    }
207
208 158
    public function has(Name $name): bool
209
    {
210 158
        if ($this->definitions->contains((string) $name)) {
211 132
            return true;
212
        }
213
214 44
        return $this->exposed->contains((string) $name);
215
    }
216
217 147
    public function get(Name $name): Service
218
    {
219
        try {
220 147
            return $this->definitions->get((string) $name);
221 43
        } catch (\Exception $e) {
222
            //pass
223
        }
224
225
        try {
226 43
            return $this->exposed->get((string) $name);
227 1
        } catch (\Exception $e) {
228
            //pass
229
        }
230
231 1
        throw new ServiceNotFound((string) $name);
232
    }
233
234
    /**
235
     * The list of exposed services name with their constructor
236
     *
237
     * @return MapInterface<Name, Constructor>
238
     */
239 10
    public function exposed(): MapInterface
240
    {
241 10
        return $this->exposed->reduce(
242 10
            new Map(Name::class, Constructor::class),
243 10
            static function(Map $exposed, string $name, Service $service): Map {
244 10
                return $exposed->put(
245 10
                    $service->exposedAs(),
246 10
                    $service->constructor()
247
                );
248 10
            }
249
        );
250
    }
251
252
    /**
253
     * @return SetInterface<Service>
254
     */
255 9
    public function all(): SetInterface
256
    {
257 9
        return Set::of(Service::class, ...$this->definitions->values());
258
    }
259
260 7
    public function compile(): Compilation\Services
261
    {
262 7
        return new Compilation\Services($this);
263
    }
264
265 9
    private function decorate(
266
        Name $decorator,
267
        Name $decorated,
268
        Name $newName = null
269
    ): Service {
270 9
        if ($this->has($decorator)) {
271 9
            return $this->get($decorator)->decorate($decorated, $newName);
272
        }
273
274 8
        return $this->dependencies->decorate($decorator, $decorated, $newName);
275
    }
276
}
277