Pipes

One mock to rule them all

Introduction

Read-process-write pipeline, where each triple is a stage. Enforced on framework level; don’t inject any object with side-effects. The side-effect factory produces promises that can be resolved concurrently when wanted.

Yield as async/await. Add new IO and return to logic. Yield always waits. Yield array of promises is concurrent.

Race-condition if IO 1 takes longer than IO 2 and IO 2 depends on 1 to be finished.

Redux-saga. Amphp.

No mocking needed. Only mock the result of the resolved side-effects.

A pipeline is an array of:

  • Commands objects (promises)
  • Filters
  • Callables (processing and push write commands using yield)

Even in Haskell there’s no framework that makes sure your business logic isn’t polluted with side-effects.

public function revertAdmin(int $userId = 1, IO $io): array
{
    return [
        $io->db->queryOne('SELECT * FROM users WHERE id = :id', [':id' => $userId]),
        new FilterEmpty($io->stdout->printline('Found no such user')),
        function (array $user) use ($io) {
            yield $io->stdout->printline('Yay, found user!');
            $becomeAdmin = $user['is_admin'] ? 0 : 1;
            $affectedRows = yield $io->db->query(
                sprintf(
                    'UPDATE users SET is_admin = %d WHERE id = %d',
                    $becomeAdmin,
                    $user['id']
                )
            );
            if ($becomeAdmin === 1) {
                yield $io->stdout->printline('User is now admin');
            } else {
                yield $io->stdout->printline('User is no longer admin');
            }
        },
    ];
}

MVC

The model-view-controller pattern is based on the faulty assumption that’s there a difference reading from a browser than it is reading from a database. The design patter is cutting along the wrong lines.