Kevin Eaton

Rules: pipes as classes

A closure is fine for a small step. A PipeRule class gives a step a name, a home and a test. The hub runs the public method named after the class. Inside it, $this->info('…') writes a line to the log against the rule, and returning self::SUCCESS records that the step passed (the other results, and $this->error() / $this->fatal(), come in Controlling the flow below):

<?php

use Keaton\PipeDream\PipeRule;

class TidyTitle extends PipeRule
{
    public static $name = 'Tidy the title';
    public static $description = 'Trim it, collapse the spaces, title-case it';

    public function tidyTitle($post)
    {
        $post->title = ucwords(strtolower(preg_replace('/\s+/', ' ', trim($post->title))));

        $this->info("Title is now \"{$post->title}\"");

        return self::SUCCESS;
    }
}

The method is called through Laravel's container, so dependency injection just works: ask for a class and you get it. Here the rule asks for Slugger, a plain class of yours:

<?php

class Slugger
{
    public function slug(string $text): string
    {
        return trim(preg_replace('/[^a-z0-9]+/', '-', strtolower($text)), '-');
    }
}

And the rule that uses it:

<?php

use Keaton\PipeDream\PipeRule;

class SlugifyTitle extends PipeRule
{
    public static $name = 'Slugify the title';
    public static $description = 'Make the URL slug';

    public function slugifyTitle($post, Slugger $slugger)
    {
        $post->slug = $slugger->slug($post->title);

        return self::SUCCESS;
    }
}

The first parameter receives the envelope, whatever you call it (leave it untyped, or type it PipeEnvelope; a different class type there is resolved from the container instead). Inside a rule, $this->words = 3 and $this->words set and read a shared value on it ($this->setWordsResult(3) and $this->getWordsResult() do the same):

<?php

use Keaton\PipeDream\PipeRule;

class CountWords extends PipeRule
{
    public static $name = 'Count the words';
    public static $description = 'Remember how long the title is';

    public function countWords($post)
    {
        $this->words = str_word_count($post->title);

        return self::SUCCESS;
    }
}

A rule can also decide that it has nothing to do and skip itself. For a one-word title, this one records PipeRule::SKIP (4) and logs its reason. It also uses $this->error(), which records a failure (PipeRule::FAIL) with its message and, by default, lets the hub carry on to the next rule:

<?php

use Keaton\PipeDream\PipeRule;

class CheckTitleLength extends PipeRule
{
    public static $name = 'Check the title length';
    public static $description = 'Long titles get cut off in feeds';

    public function checkTitleLength($post)
    {
        if ($this->words < 2) {
            return $this->skip('A one-word title is fine');
        }

        if ($this->words > 12) {
            $this->error("The title has {$this->words} words");
        }

        return self::SUCCESS;
    }
}

Use them like closures, by class name:

<?php

use Keaton\PipeDream\PipeHub;

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

$envelope = PipeHub::create()
    ->addPipes([
        TidyTitle::class,
        CountWords::class,
        CheckTitleLength::class,
        SlugifyTitle::class,
    ])
    ->sendIt($post);

echo $post->slug, ' (', $envelope->get('words'), ' words)', PHP_EOL;

foreach ($envelope->results() as $result) {
    echo $result->rule(), ': ', $result->result(), PHP_EOL;
}
hello-pipe-dream (3 words)
TidyTitle: 1
CountWords: 1
CheckTitleLength: 1
SlugifyTitle: 1

1 is PipeRule::SUCCESS.