Kevin Eaton

All together

Name each hub, describe it, fill it with rules, stack the hubs in a manager, and send the post through. Closures and rules mix freely. skipIf() skips a hub's rules when it doesn't apply (each is logged as skipped), and then() runs after a hub's last rule:

<?php

use Keaton\PipeDream\PipeHub;
use Keaton\PipeDream\PipeManager;

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

$envelope = PipeManager::create([
    PipeHub::create()
        ->name('Tidy')
        ->description('Make the title presentable')
        ->addPipes([TidyTitle::class]),

    PipeHub::create()
        ->name('Check')
        ->description('Is the title any good?')
        ->addPipes([CountWords::class, CheckTitleLength::class])
        ->then(fn ($post) => $post->readingTime = ceil($post->get('words') / 200).' min'),

    PipeHub::create()
        ->name('Publish')
        ->description('Give it a slug and publish it, unless it is a draft')
        ->skipIf(fn ($post) => $post->status === 'draft')
        ->addPipes([
            SlugifyTitle::class,
            fn ($post) => $post->status = 'published',
        ]),
])
    ->name('Publish a post')
    ->subject($post)
    ->sendIt();

foreach ($envelope->logs() as $record) {
    if ($record['data'] === 'Sending subject through all pipes') {
        echo $record['context']['name'], PHP_EOL;
    } elseif ($record['type'] === 'returnValue') {
        echo '  ', $record['key'], ': ', $record['data'], PHP_EOL;
    }
}

echo $post->status, ', ', $post->readingTime, ' read', PHP_EOL;
Tidy
  TidyTitle: 1
Check
  CountWords: 1
  CheckTitleLength: 1
Publish
  SlugifyTitle: 1
  Closure@together.php:26: published
published, 1 min read

The flow reads like a table of contents, and the log tells the same story. With 'status' => 'draft', the Publish hub's pipes are logged as skipped instead, and the post stays a draft.