StepChainBuilder::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 6
cts 6
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 4
crap 1
1
<?php
2
namespace PSB\Core\Pipeline;
3
4
5
use PSB\Core\Exception\PipelineBuildingException;
6
7
class StepChainBuilder
8
{
9
    /**
10
     * @var string
11
     */
12
    private $rootContextClass;
13
14
    /**
15
     * @var StepRegistration[]
16
     */
17
    private $additions = [];
18
19
    /**
20
     * @var StepReplacement[]
21
     */
22
    private $replacements = [];
23
24
    /**
25
     * @var StepRemoval[]
26
     */
27
    private $removals = [];
28
29
    /**
30
     * @param string             $rootContextClass
31
     * @param StepRegistration[] $additions
32
     * @param StepReplacement[]  $replacements
33
     * @param StepRemoval[]      $removals
34
     */
35 13
    public function __construct($rootContextClass, array $additions, array $replacements, array $removals)
36
    {
37 13
        $this->rootContextClass = $rootContextClass;
38 13
        $this->additions = $additions;
39 13
        $this->replacements = $replacements;
40 13
        $this->removals = $removals;
41 13
    }
42
43
    /**
44
     * @return StepRegistration[]
45
     *
46
     * @throws PipelineBuildingException
47
     */
48 11
    public function build()
49
    {
50 11
        $this->assertAdditionsAreUnique();
51
52
        /** @var StepRegistration[] $registrations */
53 10
        $registrations = [];
54
55 10
        foreach ($this->additions as $addition) {
56 10
            $registrations[$addition->getStepId()] = $addition;
57
        }
58
59 10
        $this->assertReplacementsAreValid($registrations);
60
61 9
        foreach ($this->replacements as $replacement) {
62
            $registrations[$replacement->getIdToReplace()]->replaceWith($replacement);
63
        }
64
65 9
        $this->assertRemovalsAreValid($registrations);
66 8
        $this->assertRemovalsDoNotAffectDependencies($registrations);
67
68 7
        foreach ($this->removals as $removal) {
69
            if (isset($registrations[$removal->getIdToRemove()])) {
70
                unset($registrations[$removal->getIdToRemove()]);
71
            }
72
        }
73
74 7
        if (empty($registrations)) {
75
            return [];
76
        }
77
78 7
        $stages = $this->groupRegistrationsByStage($registrations);
79
80 7
        $this->assertContextStageExists($this->rootContextClass, $stages);
81 6
        $currentStage = $stages[$this->rootContextClass];
82 6
        $stageName = $this->rootContextClass;
83
84 6
        $finalOrder = [];
85 6
        $stageNumber = 1;
86 6
        while ($currentStage) {
87 6
            $stageSteps = $this->getStepsFromStage($currentStage);
88 6
            $finalOrder = array_merge($finalOrder, $this->sort($stageSteps));
89
90 6
            $stageConnectors = $this->getConnectorsFromStage($currentStage);
91
92 6
            $this->assertThereIsAtMostOneConnectorPerStage($stageName, $stageConnectors);
93 5
            $this->assertThereIsAtLeastOneConnectorPerIntermediaryStage(
94 5
                $stageNumber,
95 5
                count($stages),
96 5
                $stageName,
97 5
                $stageConnectors
98
            );
99
100 4
            $currentStage = null;
101
102
            /** @var StepRegistration $stageConnector */
103 4
            $stageConnector = reset($stageConnectors);
104 4
            if ($stageConnector) {
105 4
                $finalOrder[] = $stageConnector;
106
107 4
                if (!$this->isTerminator($stageConnector)) {
108
                    /** @var StageConnectorInterface $connectorClass */
109 4
                    $connectorClass = $stageConnector->getStepFqcn();
110 4
                    $stageName = $connectorClass::getNextStageContextClass();
111 4
                    $this->assertContextStageExists($stageName, $stages);
112
113 3
                    $currentStage = $stages[$stageName];
114
                }
115
            }
116
117 3
            $stageNumber++;
118
        }
119
120 3
        return $finalOrder;
121
    }
122
123
    /**
124
     * @param StepRegistration[] $registrations
125
     *
126
     * @return array
127
     */
128 7
    private function groupRegistrationsByStage(array $registrations)
129
    {
130 7
        $stages = [];
131 7
        foreach ($registrations as $registration) {
132
            /** @var PipelineStepInterface $stepClass */
133 7
            $stepClass = $registration->getStepFqcn();
134 7
            $stageName = $stepClass::getStageContextClass();
135
136 7
            if (!isset($stages[$stageName])) {
137 7
                $stages[$stageName] = [];
138
            }
139
140 7
            $stages[$stageName][] = $registration;
141
        }
142
143 7
        return $stages;
144
    }
145
146
    /**
147
     * @param StepRegistration[] $currentStage
148
     * @param bool               $isConnector
149
     *
150
     * @return StepRegistration[]
151
     */
152 6
    private function getStepsFromStage(array $currentStage, $isConnector = false)
153
    {
154 6
        $steps = [];
155 6
        foreach ($currentStage as $step) {
156 6
            if (!$isConnector && !$this->isImplementing($step->getStepFqcn(), StageConnectorInterface::class)) {
157 6
                $steps[$step->getStepId()] = $step;
158
            }
159
160 6
            if ($isConnector && $this->isImplementing($step->getStepFqcn(), StageConnectorInterface::class)) {
161 5
                $steps[$step->getStepId()] = $step;
162
            }
163
        }
164
165 6
        return $steps;
166
    }
167
168
    /**
169
     * @param StepRegistration[] $currentStage
170
     *
171
     * @return StepRegistration[]
172
     */
173 6
    private function getConnectorsFromStage(array $currentStage)
174
    {
175 6
        return $this->getStepsFromStage($currentStage, true);
176
    }
177
178
    /**
179
     * @param StepRegistration $stageConnector
180
     *
181
     * @return bool
182
     */
183 4
    private function isTerminator($stageConnector)
184
    {
185 4
        return $this->isImplementing($stageConnector->getStepFqcn(), PipelineTerminatorInterface::class);
186
    }
187
188
    /**
189
     * @param StepRegistration[] $stageSteps
190
     *
191
     * @return StepRegistration[]
192
     */
193 6
    private function sort(array $stageSteps)
194
    {
195 6
        if (empty($stageSteps)) {
196
            return [];
197
        }
198
199 6
        $dependencyGraphBuilder = new StepDependencyGraphBuilder($stageSteps);
200 6
        return $dependencyGraphBuilder->build()->sort();
201
    }
202
203
    /**
204
     * @param string $fqcn
205
     * @param string $fqin
206
     *
207
     * @return bool
208
     */
209 6
    private function isImplementing($fqcn, $fqin)
210
    {
211 6
        $interfaces = class_implements($fqcn, true);
212 6
        return isset($interfaces[$fqin]);
213
    }
214
215
    /**
216
     * @throws PipelineBuildingException
217
     */
218 11
    private function assertAdditionsAreUnique()
219
    {
220
        /** @var StepRegistration[] $registrations */
221 11
        $registrations = [];
222 11
        foreach ($this->additions as $addition) {
223 11
            if (isset($registrations[$addition->getStepId()])) {
224 1
                $existingStepClass = $registrations[$addition->getStepId()]->getStepFqcn();
225 1
                throw new PipelineBuildingException(
226 1
                    "Step registration with id '{$addition->getStepId()}' is already registered for step '$existingStepClass'."
227
                );
228
            }
229 11
            $registrations[$addition->getStepId()] = $addition;
230
        }
231 10
    }
232
233
    /**
234
     * @param StepRegistration[] $registrations
235
     *
236
     * @throws PipelineBuildingException
237
     */
238 10
    private function assertReplacementsAreValid(array $registrations)
239
    {
240 10
        foreach ($this->replacements as $replacement) {
241 1
            if (!isset($registrations[$replacement->getIdToReplace()])) {
242 1
                throw new PipelineBuildingException(
243 1
                    "You can only replace an existing step registration, '{$replacement->getIdToReplace()}' registration does not exist."
244
                );
245
            }
246
        }
247 9
    }
248
249
    /**
250
     * @param StepRegistration[] $registrations
251
     *
252
     * @throws PipelineBuildingException
253
     */
254 9
    private function assertRemovalsAreValid(array $registrations)
255
    {
256 9
        foreach ($this->removals as $removal) {
257 2
            if (!isset($registrations[$removal->getIdToRemove()])) {
258 1
                throw new PipelineBuildingException(
259 1
                    "You cannot remove step registration with id '{$removal->getIdToRemove()}', registration does not exist."
260
                );
261
            }
262
        }
263 8
    }
264
265
    /**
266
     * @param StepRegistration[] $registrations
267
     *
268
     * @throws PipelineBuildingException
269
     */
270 8
    private function assertRemovalsDoNotAffectDependencies(array $registrations)
271
    {
272 8
        $removalIds = [];
273 8
        foreach ($this->removals as $removal) {
274 1
            $removalIds[$removal->getIdToRemove()] = 0;
275
        }
276
277 8
        foreach ($registrations as $registration) {
278
            /** @var StepRegistrationDependency $dependency */
279 8
            foreach (array_merge($registration->getBefores(), $registration->getAfters()) as $dependency) {
280 2
                if (isset($removalIds[$dependency->getDependsOnId()])) {
281 1
                    throw new PipelineBuildingException(
282 1
                        "You cannot remove step registration with id '{$dependency->getDependsOnId()}', registration with id '{$registration->getStepId()}' depends on it."
283
                    );
284
                }
285
            }
286
        }
287 7
    }
288
289
    /**
290
     * @param string $contextClass
291
     * @param array  $stages
292
     *
293
     * @throws PipelineBuildingException
294
     */
295 7
    private function assertContextStageExists($contextClass, $stages)
296
    {
297 7
        if (!isset($stages[$contextClass])) {
298 2
            throw new PipelineBuildingException(
299 2
                "Can't find any steps/connectors for stage '$contextClass'."
300
            );
301
        }
302 6
    }
303
304
    /**
305
     * @param string             $stageName
306
     * @param StepRegistration[] $stageConnectors
307
     *
308
     */
309 6
    private function assertThereIsAtMostOneConnectorPerStage($stageName, $stageConnectors)
310
    {
311 6
        if (count($stageConnectors) > 1) {
312 1
            $connectorClasses = [];
313 1
            foreach ($stageConnectors as $connector) {
314 1
                $connectorClasses[] = $connector->getStepFqcn();
315
            }
316 1
            $connectors = implode(',', $connectorClasses);
317
318 1
            throw new PipelineBuildingException(
319 1
                "Multiple stage connectors found for stage '$stageName'. Please remove one of: $connectors."
320
            );
321
        }
322 5
    }
323
324
    /**
325
     * @param int                $stageNumber
326
     * @param int                $stageCount
327
     * @param string             $stageName
328
     * @param StepRegistration[] $stageConnectors
329
     *
330
     */
331 5
    private function assertThereIsAtLeastOneConnectorPerIntermediaryStage(
332
        $stageNumber,
333
        $stageCount,
334
        $stageName,
335
        $stageConnectors
336
    ) {
337 5
        if ($stageNumber < $stageCount && count($stageConnectors) == 0) {
338 1
            throw new PipelineBuildingException("No stage connector found for stage '$stageName'.");
339
        }
340 4
    }
341
}
342
343