Kevin Eaton

pipe-dream

Pipe Dream turns a Laravel pipeline into a flow you can read, and a log you can read back: every step named, timed and recorded.

v1.0.0Read the docsGitHub · coming soonPackagist · coming soon

Pipe Dream

Pipe Dream turns a Laravel pipeline into a flow you can read, and a log you can read back: every step named, timed and recorded.

composer require keaton-php/pipe-dream

The service provider is discovered automatically. At the end of this README, a full ad-click flow and the dashboard built from its logs show where this goes.

What a pipeline is

A pipeline sends one value (the subject) through a list of steps (the pipes), in order. Each pipe gets the subject, does its bit, and passes it on. Laravel ships one, Illuminate\Pipeline\Pipeline, and the pipes can be plain closures:

<?php

use Illuminate\Pipeline\Pipeline;

$post = (object) ['title' => '  hello,   PIPE dream  '];

app(Pipeline::class)
    ->send($post)
    ->through([
        function ($post, $next) {
            $post->title = trim($post->title);

            return $next($post);
        },
        function ($post, $next) {
            $post->title = preg_replace('/\s+/', ' ', $post->title);

            return $next($post);
        },
        function ($post, $next) {
            $post->title = ucwords(strtolower($post->title));

            return $next($post);
        },
    ])
    ->thenReturn();

echo $post->title;
Hello, Pipe Dream

The same thing as a PipeHub

A PipeHub is Pipe Dream's pipeline. The same three steps:

<?php

use Keaton\PipeDream\PipeHub;

$post = (object) ['title' => '  hello,   PIPE dream  '];

PipeHub::create()
    ->addPipes([
        fn ($post) => $post->title = trim($post->title),
        fn ($post) => $post->title = preg_replace('/\s+/', ' ', $post->title),
        fn ($post) => $post->title = ucwords(strtolower($post->title)),
    ])
    ->sendIt($post);

echo $post->title;
Hello, Pipe Dream

No $next to call and nothing to return: the hub moves on to the next pipe by itself.

Same three steps, same result. So far the plain pipeline has done everything the hub did.

Now ask it what happened

Here is the difference. A plain pipeline hands you the final value and nothing else: if the title came out wrong, you are left guessing which step broke it. The hub kept notes. Ask it what happened:

<?php

use Keaton\PipeDream\PipeHub;

$post = (object) ['title' => '  hello,   PIPE dream  '];

$envelope = PipeHub::create()
    ->addPipes([
        fn ($post) => $post->title = trim($post->title),
        fn ($post) => $post->title = preg_replace('/\s+/', ' ', $post->title),
        fn ($post) => $post->title = ucwords(strtolower($post->title)),
    ])
    ->sendIt($post);

foreach ($envelope->logs() as $record) {
    printf("%-11s %-24s %s\n", $record['type'], $record['key'], $record['data']);
}
info        PipeHub:sendIt           Sending subject through all pipes
timerStart  Closure@hub-log.php:9    Executing rule Closure@hub-log.php:9
returnValue Closure@hub-log.php:9    hello,   PIPE dream
timerEnd    Closure@hub-log.php:9    Finished executing rule Closure@hub-log.php:9
timerStart  Closure@hub-log.php:10   Executing rule Closure@hub-log.php:10
returnValue Closure@hub-log.php:10   hello, PIPE dream
timerEnd    Closure@hub-log.php:10   Finished executing rule Closure@hub-log.php:10
timerStart  Closure@hub-log.php:11   Executing rule Closure@hub-log.php:11
returnValue Closure@hub-log.php:11   Hello, Pipe Dream
timerEnd    Closure@hub-log.php:11   Finished executing rule Closure@hub-log.php:11
info        PipeHub:sendIt           Finalized result!

Every pipe was timed, and what each one returned was recorded. You can see exactly where hello, PIPE dream became Hello, Pipe Dream, without adding a single line of logging to your code. $envelope->results() holds the same results, ready to check in code.

Now break it on purpose. Change the second step so it eats every space, and run the hub again:

<?php

use Keaton\PipeDream\PipeHub;

$post = (object) ['title' => '  hello,   PIPE dream  '];

$envelope = PipeHub::create()
    ->addPipes([
        fn ($post) => $post->title = trim($post->title),
        fn ($post) => $post->title = preg_replace('/\s+/', '', $post->title), // oops: '' instead of ' '
        fn ($post) => $post->title = ucwords(strtolower($post->title)),
    ])
    ->sendIt($post);

foreach ($envelope->logs() as $record) {
    if ($record['type'] === 'returnValue') {
        printf("%-30s %s\n", $record['key'], $record['data']);
    }
}
Closure@hub-log-broken.php:9   hello,   PIPE dream
Closure@hub-log-broken.php:10  hello,PIPEdream
Closure@hub-log-broken.php:11  Hello,pipedream

You don't have to guess which step ate the spaces. The log already points at the second one: it's the first line where they're gone.

That's the whole difference. A pipeline tells you the answer. A hub hands you the receipt.

And that's only the start. Once every step is a named, recorded thing, a lot more opens up:

  • Rules as classes: each pipe becomes a small, testable class with a name, so the log reads like a description of your business.
  • Stacked hubs: group steps into stages and run the stages as one flow, with one log.
  • Control flow: skip a step, stop a flow early, redirect, or catch a failure without losing the log.
  • Saved runs: store every log and query it later: which runs hit problems, and exactly where.
  • A flow you can draw: read a flow back and render it as a page, then watch real traffic move through it.

All of it builds on what you just saw. The docs pick it up from here.

Continue in the docs →