Audit trails with tap / sideEffect

Add logging, audit records, or debug output to a pipeline without breaking the data flow. `sideEffect` runs a callable for its effect and passes the value through unchanged — drop it anywhere in a compose or pipe.

Functions used

The problem

You’ve got a nice pipeline that transforms data. Now you need to observe what’s going through it — log intermediate values, record an audit trail, count how often a step matches — without destroying the pipeline shape.

// Before — clean.
$transform = F\compose(
    $shape,
    $enrich,
    $persist
);

The imperative option is to break the chain, assign to a temporary, log, pass along. That defeats the point of the pipeline.

Drop in a sideEffect

sideEffect($fn) wraps any callable into a new callable that calls $fn($value) for the side effect, then returns $value unchanged. Invisible to the pipeline, visible to you.

use PinkCrab\FunctionConstructors\GeneralFunctions as F;

$logger   = F\sideEffect(fn($v) => error_log('after shape: ' . json_encode($v)));
$auditor  = F\sideEffect(fn($v) => AuditLog::record('order.transformed', $v));

$transform = F\compose(
    $shape,
    $logger,          // fires during the pipeline, value untouched
    $enrich,
    $auditor,         // records into your audit table
    $persist
);

Exactly the same output as before. The logger and auditor are pure side channels.

Use cases

Debug a single run

$debug = F\sideEffect(function ($v) {
    dump($v);     // or var_dump, or whatever your debug helper is
});

$result = F\pipe(
    $rawData,
    $clean,
    $debug,        // shows what $clean produced
    $enrich,
    $debug         // shows what $enrich produced
);

Add, rerun, remove. The rest of the pipeline never knows.

Metrics

$tickStats = F\sideEffect(fn($row) => Metrics::increment('orders.processed'));

$transform = F\compose($shape, $tickStats, $persist);

Audit record on every successful transform

$auditTransformed = F\sideEffect(function ($order) {
    AuditLog::write([
        'action'   => 'order.transformed',
        'order_id' => $order['id'],
        'at'       => now(),
    ]);
});

$process = F\compose($shape, $validate, $enrich, $auditTransformed, $persist);

Why this is cleaner than inline logging

A note on purity

sideEffect is the ONE place this library explicitly endorses impurity. The callable you pass in can do anything — write to disk, call an API, mutate state. Use it knowingly; everything else in a pipeline should stay pure.