Passed
Push — main ( 6c8636...b48ef0 )
by Michael
12:25
created

Pipeline::pipe()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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