Alexandre Gomes Gaigalas – January 22, 2026

PHP's pipe operator unlocks a very readable, functional style. Below I walk through five conceptual "levels" of using pipes: from the simple production line to composed functor objects.
You can use these as templates that you can extend upon in your own projects.
Each step transforms the value in sequence, like a factory conveyor belt.
$input = ' pipes & fun ';
$output = $input
|> html_entity_decode(...)
|> ucwords(...)
|> trim(...);
var_dump($output); // string(11) "Pipes & Fun"
Wrap operations in closures so you can parameterize behavior and reuse steps.
function add(int $x): Closure {
return static fn (int $v): int => $v + $x;
}
function multiply(int $x): Closure {
return static fn (int $v): int => $v * $x;
}
$input = 42;
$output = $input
|> add(5)
|> multiply(2);
var_dump($output); // int(94)
Compose steps into a pipeline and run it later against inputs.
This technique allows you to put the input last and compose the steps first, which opens opportunities for reusability.
function with(Closure $step): Closure {
return static fn (Closure $acc): Closure => static fn ($v) => $step($acc($v));
}
function run($input): Closure {
return static fn (Closure $pipeline) => $pipeline($input);
}
function begin(): Closure {
return static fn ($v) => $v;
}
$input = 42;
$output = begin()
|> with(add(10))
|> with(multiply(2))
|> run($input);
var_dump($output); // int(104)
Generators let you define flexible steps, collect messages, or short-circuit validations.
function stream(): Generator {
yield from [];
}
function check(Closure $fn, string $message): Closure {
return static fn (Generator $it): Generator => yield from [
...$it,
static fn ($v) => $fn($v) ? null : $message
];
}
function collect(): Closure {
return static fn (Generator $it): Closure => static function ($v) use ($it) {
$messages = [];
foreach ($it as $fn) {
$m = $fn($v);
if ($m !== null) {
$messages[] = $m;
}
}
return (static fn () => yield from $messages)();
};
}
This version not only allows you to set the input last, but also obtain a stream of responses from the pipeline.
$input = -3.14;
$output = stream()
|> check(is_int(...), 'must be an integer')
|> check(fn ($v) => $v > 0, 'must be positive')
|> collect();
foreach ($output($input) as $message) {
var_dump($message);
}
Wrap behavior in objects with __invoke for a smooth composer-friendly API.
class Check
{
public function __construct(
private Closure $fn,
private string $message,
) {}
public function __invoke(Generator $it): Generator
{
yield from [
...$it,
fn ($v) => ($this->fn)($v) ? null : $this->message
];
}
}
PHP is great for combining OOP and functional programming. The class we defined here is simple, however, you can go as far as you like with inheritance, interfaces, and traits to build a rich library of reusable pipeline components.
$input = ['arr'];
$output = stream()
|> new Check(is_int(...), 'must be an integer')
|> new Check(is_scalar(...), 'must be scalar')
|> collect();
foreach ($output($input) as $message) {
var_dump($message);
}
Use magic to make the API feel like static function calls.
class Func extends Check
{
public static function __callStatic(string $name, array $arguments): static
{
return new static($name(...), ...$arguments);
}
}
$input = ['arr'];
$output = stream()
|> Func::is_int('must be an integer')
|> Func::is_scalar('must be scalar')
|> collect();
foreach ($output($input) as $message) {
var_dump($message);
}
With every new PHP feature, there's a great opportunity to make simpler APIs that are more expressive and reusable. The pipe operator is a great example of that, enabling a functional style that integrates well with OOP. Enjoy experimenting with these levels of pipes in your own projects!