Skip to content

Commit 5826904

Browse files
[10.x] Add pipe function to Process layer (#46527)
* [10.x] Add pipe function to Process layer * Fixing code style * Update pipe to accept fully configured processes as parameters * Fixing code style * Remove unused use statement on test file * formatting * exception message * Wrap error in process pipe with a custom ProcessPipeException * Fixed code style issues * Updated pipe to return last failed process instead of throwing exception and added test case for failed pipe run * Update pipe to handle keys for processes --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent 4b2e01a commit 5826904

File tree

5 files changed

+155
-0
lines changed

5 files changed

+155
-0
lines changed

src/Illuminate/Process/Factory.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,17 @@ public function pool(callable $callback)
271271
return new Pool($this, $callback);
272272
}
273273

274+
/**
275+
* Start defining a series of piped processes.
276+
*
277+
* @param callable $callback
278+
* @return \Illuminate\Process\Pipe
279+
*/
280+
public function pipe(callable $callback)
281+
{
282+
return new Pipe($this, $callback);
283+
}
284+
274285
/**
275286
* Run a pool of processes and wait for them to finish executing.
276287
*

src/Illuminate/Process/PendingProcess.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
use Closure;
66
use Illuminate\Process\Exceptions\ProcessTimedOutException;
77
use Illuminate\Support\Str;
8+
use Illuminate\Support\Traits\Conditionable;
89
use LogicException;
910
use RuntimeException;
1011
use Symfony\Component\Process\Exception\ProcessTimedOutException as SymfonyTimeoutException;
1112
use Symfony\Component\Process\Process;
1213

1314
class PendingProcess
1415
{
16+
use Conditionable;
17+
1518
/**
1619
* The process factory instance.
1720
*

src/Illuminate/Process/Pipe.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
namespace Illuminate\Process;
4+
5+
use InvalidArgumentException;
6+
7+
/**
8+
* @mixin \Illuminate\Process\Factory
9+
* @mixin \Illuminate\Process\PendingProcess
10+
*/
11+
class Pipe
12+
{
13+
/**
14+
* The process factory instance.
15+
*
16+
* @var \Illuminate\Process\Factory
17+
*/
18+
protected $factory;
19+
20+
/**
21+
* The callback that resolves the pending processes.
22+
*
23+
* @var callable
24+
*/
25+
protected $callback;
26+
27+
/**
28+
* The array of pending processes.
29+
*
30+
* @var array
31+
*/
32+
protected $pendingProcesses = [];
33+
34+
/**
35+
* Create a new series of piped processes.
36+
*
37+
* @param \Illuminate\Process\Factory $factory
38+
* @param callable $callback
39+
* @return void
40+
*/
41+
public function __construct(Factory $factory, callable $callback)
42+
{
43+
$this->factory = $factory;
44+
$this->callback = $callback;
45+
}
46+
47+
/**
48+
* Add a process to the pipe with a key.
49+
*
50+
* @param string $key
51+
* @return \Illuminate\Process\PendingProcess
52+
*/
53+
public function as(string $key)
54+
{
55+
return tap($this->factory->newPendingProcess(), function ($pendingProcess) use ($key) {
56+
$this->pendingProcesses[$key] = $pendingProcess;
57+
});
58+
}
59+
60+
/**
61+
* Runs the processes in the pipe.
62+
*
63+
* @param callable|null $output
64+
* @return \Illuminate\Contracts\Process\ProcessResult
65+
*/
66+
public function run(?callable $output = null)
67+
{
68+
call_user_func($this->callback, $this);
69+
70+
return collect($this->pendingProcesses)
71+
->reduce(function ($previousProcessResult, $pendingProcess, $key) use ($output) {
72+
if (! $pendingProcess instanceof PendingProcess) {
73+
throw new InvalidArgumentException('Process pipe must only contain pending processes.');
74+
}
75+
76+
if ($previousProcessResult && $previousProcessResult->failed()) {
77+
return $previousProcessResult;
78+
}
79+
80+
return $pendingProcess->when(
81+
$previousProcessResult,
82+
fn () => $pendingProcess->input($previousProcessResult->output())
83+
)->run(output: $output ? function ($type, $buffer) use ($key, $output) {
84+
$output($type, $buffer, $key);
85+
} : null);
86+
});
87+
}
88+
89+
/**
90+
* Dynamically proxy methods calls to a new pending process.
91+
*
92+
* @param string $method
93+
* @param array $parameters
94+
* @return \Illuminate\Process\PendingProcess
95+
*/
96+
public function __call($method, $parameters)
97+
{
98+
return tap($this->factory->{$method}(...$parameters), function ($pendingProcess) {
99+
$this->pendingProcesses[] = $pendingProcess;
100+
});
101+
}
102+
}

src/Illuminate/Support/Facades/Process.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
* @method static \Illuminate\Process\Factory assertDidntRun(\Closure|string $callback)
3434
* @method static \Illuminate\Process\Factory assertNothingRan()
3535
* @method static \Illuminate\Process\Pool pool(callable $callback)
36+
* @method static \Illuminate\Process\Pipe pipe(callable $callback)
3637
* @method static \Illuminate\Process\ProcessPoolResults concurrently(callable $callback, callable|null $output = null)
3738
* @method static \Illuminate\Process\PendingProcess newPendingProcess()
3839
* @method static void macro(string $name, object|callable $macro)

tests/Process/ProcessTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,44 @@ public function testRealProcessesCanUseStandardInput()
496496
$this->assertSame('foobar', $result->output());
497497
}
498498

499+
public function testProcessPipe()
500+
{
501+
if (windows_os()) {
502+
$this->markTestSkipped('Requires Linux.');
503+
}
504+
505+
$factory = new Factory;
506+
$factory->fake([
507+
'cat *' => "Hello, world\nfoo\nbar",
508+
]);
509+
510+
$pipe = $factory->pipe(function ($pipe) {
511+
$pipe->command('cat test');
512+
$pipe->command('grep -i "foo"');
513+
});
514+
515+
$this->assertSame("foo\n", $pipe->run()->output());
516+
}
517+
518+
public function testProcessPipeFailed()
519+
{
520+
if (windows_os()) {
521+
$this->markTestSkipped('Requires Linux.');
522+
}
523+
524+
$factory = new Factory;
525+
$factory->fake([
526+
'cat *' => $factory->result(exitCode: 1),
527+
]);
528+
529+
$pipe = $factory->pipe(function ($pipe) {
530+
$pipe->command('cat test');
531+
$pipe->command('grep -i "foo"');
532+
});
533+
534+
$this->assertTrue($pipe->run()->failed());
535+
}
536+
499537
public function testFakeInvokedProcessOutputWithLatestOutput()
500538
{
501539
$factory = new Factory;

0 commit comments

Comments
 (0)