Passed
Push — main ( 400a4b...42ad95 )
by Michael
05:02 queued 01:57
created

Pipeline::run()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 6
ccs 5
cts 5
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace MichaelRubel\EnhancedPipeline;
6
7
use Closure;
8
use Illuminate\Container\Container as ContainerConcrete;
9
use Illuminate\Contracts\Container\Container;
10
use Illuminate\Contracts\Pipeline\Pipeline as PipelineContract;
11
use Illuminate\Support\Traits\Conditionable;
12
use MichaelRubel\EnhancedPipeline\Events\PipeExecutionFinished;
13
use MichaelRubel\EnhancedPipeline\Events\PipeExecutionStarted;
14
use MichaelRubel\EnhancedPipeline\Traits\HasDatabaseTransactions;
15
use MichaelRubel\EnhancedPipeline\Traits\HasEvents;
16
use RuntimeException;
17
use Throwable;
18
19
class Pipeline implements PipelineContract
20
{
21
    use Conditionable, HasDatabaseTransactions, HasEvents;
22
23
    /**
24
     * The container implementation.
25
     *
26
     * @var \Illuminate\Contracts\Container\Container|null
27
     */
28
    protected $container;
29
30
    /**
31
     * The object being passed through the pipeline.
32
     *
33
     * @var mixed
34
     */
35
    protected $passable;
36
37
    /**
38
     * The callback to be executed on failure pipeline.
39
     *
40
     * @var Closure|null
41
     */
42
    protected $onFailure;
43
44
    /**
45
     * The array of class pipes.
46
     *
47
     * @var array
48
     */
49
    protected $pipes = [];
50
51
    /**
52
     * The method to call on each pipe.
53
     *
54
     * @var string
55
     */
56
    protected $method = 'handle';
57
58
    /**
59
     * Create a new class instance.
60
     *
61
     * @return void
62
     */
63 30
    public function __construct(?Container $container = null)
64
    {
65 30
        $this->container = $container;
66
    }
67
68
    /**
69
     * Create a new class instance.
70
     */
71 11
    public static function make(?Container $container = null): Pipeline
72
    {
73 11
        if (! $container) {
74 11
            $container = ContainerConcrete::getInstance();
75
        }
76
77 11
        return $container->make(static::class);
78
    }
79
80
    /**
81
     * Set the object being sent through the pipeline.
82
     *
83
     * @param  mixed  $passable
84
     * @return $this
85
     */
86 29
    public function send($passable)
87
    {
88 29
        $this->passable = $passable;
89
90 29
        return $this;
91
    }
92
93
    /**
94
     * Set the array of pipes.
95
     *
96
     * @param  array|mixed  $pipes
97
     * @return $this
98
     */
99 28
    public function through($pipes)
100
    {
101 28
        $this->pipes = is_array($pipes) ? $pipes : func_get_args();
102
103 28
        return $this;
104
    }
105
106
    /**
107
     * Push additional pipes onto the pipeline.
108
     *
109
     * @param  array|mixed  $pipes
110
     * @return $this
111
     */
112 2
    public function pipe($pipes)
113
    {
114 2
        array_push($this->pipes, ...(is_array($pipes) ? $pipes : func_get_args()));
115
116 2
        return $this;
117
    }
118
119
    /**
120
     * Set the method to call on the pipes.
121
     *
122
     * @param  string  $method
123
     * @return $this
124
     */
125 3
    public function via($method)
126
    {
127 3
        $this->method = $method;
128
129 3
        return $this;
130
    }
131
132
    /**
133
     * Run the pipeline with a final destination callback.
134
     *
135
     * @return mixed
136
     */
137 29
    public function then(Closure $destination)
138
    {
139
        try {
140 29
            $this->beginTransaction();
141
142 29
            $pipeline = array_reduce(
143 29
                array_reverse($this->pipes()),
144 29
                $this->carry(),
145 29
                $this->prepareDestination($destination)
146 29
            );
147
148 29
            return $pipeline($this->passable);
149 8
        } catch (Throwable $e) {
150 8
            $this->rollbackTransaction();
151
152 8
            if ($this->onFailure) {
153 4
                return ($this->onFailure)($this->passable, $e);
154
            }
155
156 4
            return $this->handleException($this->passable, $e);
157
        }
158
    }
159
160
    /**
161
     * Run the pipeline and return the result.
162
     *
163
     * @return mixed
164
     */
165 16
    public function thenReturn()
166
    {
167 16
        return $this->then(function ($passable) {
168 8
            return $passable;
169 16
        });
170
    }
171
172
    /**
173
     * Get the final piece of the Closure onion.
174
     *
175
     * @return \Closure
176
     */
177 29
    protected function prepareDestination(Closure $destination)
178
    {
179 29
        return function ($passable) use ($destination) {
180 17
            return $destination($passable);
181 29
        };
182
    }
183
184
    /**
185
     * Get a Closure that represents a slice of the application onion.
186
     *
187
     * @return \Closure
188
     */
189 29
    protected function carry()
190
    {
191 29
        return function ($stack, $pipe) {
192 29
            return function ($passable) use ($stack, $pipe) {
193 29
                $this->fireEvent(PipeExecutionStarted::class, $pipe, $passable);
194
195 29
                if (is_callable($pipe)) {
196
                    // If the pipe is a callable, then we will call it directly, but otherwise we
197
                    // will resolve the pipes out of the dependency container and call it with
198
                    // the appropriate method and arguments, returning the results back out.
199 21
                    $result = $pipe($passable, $stack);
200
201 18
                    $this->fireEvent(PipeExecutionFinished::class, $pipe, $passable);
202
203 18
                    return $result;
204 19
                } elseif (! is_object($pipe)) {
205 18
                    [$name, $parameters] = $this->parsePipeString($pipe);
206
207
                    // If the pipe is a string we will parse the string and resolve the class out
208
                    // of the dependency injection container. We can then build a callable and
209
                    // execute the pipe function giving in the parameters that are required.
210 18
                    $pipe = $this->getContainer()->make($name);
211
212 16
                    $parameters = array_merge([$passable, $stack], $parameters);
213
                } else {
214
                    // If the pipe is already an object we'll just make a callable and pass it to
215
                    // the pipe as-is. There is no need to do any extra parsing and formatting
216
                    // since the object we're given was already a fully instantiated object.
217 1
                    $parameters = [$passable, $stack];
218
                }
219
220 17
                $carry = method_exists($pipe, $this->method)
221 16
                                ? $pipe->{$this->method}(...$parameters)
222 1
                                : $pipe(...$parameters);
223
224 14
                $this->fireEvent(PipeExecutionFinished::class, $pipe, $passable);
225
226 14
                return $this->handleCarry($carry);
227 29
            };
228 29
        };
229
    }
230
231
    /**
232
     * Parse full pipe string to get name and parameters.
233
     *
234
     * @param  string  $pipe
235
     * @return array
236
     */
237 18
    protected function parsePipeString($pipe)
238
    {
239 18
        [$name, $parameters] = array_pad(explode(':', $pipe, 2), 2, []);
240
241 18
        if (is_string($parameters)) {
242 1
            $parameters = explode(',', $parameters);
243
        }
244
245 18
        return [$name, $parameters];
246
    }
247
248
    /**
249
     * Get the array of configured pipes.
250
     *
251
     * @return array
252
     */
253 29
    protected function pipes()
254
    {
255 29
        return [
256 29
            ...$this->pipes,
257 29
            function ($passable, $next) {
258 17
                $this->commitTransaction();
259
260 17
                return $next($passable);
261 29
            },
262 29
        ];
263
    }
264
265
    /**
266
     * Get the container instance.
267
     *
268
     * @return \Illuminate\Contracts\Container\Container
269
     *
270
     * @throws \RuntimeException
271
     */
272 18
    protected function getContainer()
273
    {
274 18
        if (! $this->container) {
275 1
            throw new RuntimeException('A container instance has not been passed to the Pipeline.');
276
        }
277
278 17
        return $this->container;
279
    }
280
281
    /**
282
     * Set the container instance.
283
     *
284
     * @return $this
285
     */
286 1
    public function setContainer(Container $container)
287
    {
288 1
        $this->container = $container;
289
290 1
        return $this;
291
    }
292
293
    /**
294
     * Set callback to be executed on failure pipeline.
295
     *
296
     *
297
     * @return $this
298
     */
299 4
    public function onFailure(Closure $callback)
300
    {
301 4
        $this->onFailure = $callback;
302
303 4
        return $this;
304
    }
305
306
    /**
307
     * Run a single pipe.
308
     */
309 4
    public function run(string $pipe, mixed $data = true): mixed
310
    {
311 4
        return $this
312 4
            ->send($data)
313 4
            ->through([$pipe])
314 4
            ->thenReturn();
315
    }
316
317
    /**
318
     * Handle the value returned from each pipe before passing it to the next.
319
     *
320
     * @param  mixed  $carry
321
     * @return mixed
322
     */
323 14
    protected function handleCarry($carry)
324
    {
325 14
        return $carry;
326
    }
327
328
    /**
329
     * Handle the given exception.
330
     *
331
     * @param  mixed  $passable
332
     * @return mixed
333
     *
334
     * @throws \Throwable
335
     */
336 4
    protected function handleException($passable, Throwable $e)
337
    {
338 4
        throw $e;
339
    }
340
}
341