Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Piper Laravel Package

spatie/piper

Piper is a pipe operator-first PHP utility library that ports Laravel’s array and string helpers into standalone functions. Chain filter, map, join, lower, replace, and more with the |> operator, mixing array and string primitives in one clean pipeline.

View on GitHub
Deep Wiki
Context7

Getting Started

PHP 8.5 is the hard gate — verify it before you read another line. |> is a PHP 8.5 language feature; without it the package cannot execute and composer require spatie/piper will fail platform resolution. Check four places, in this order: local php -v, config.platform.php in composer.json, your CI matrix, and the base image your deploy target runs. Static analysis is the fourth-and-a-half check: PHPStan/Psalm must themselves run on 8.5 or they'll fail to parse the operator before they ever analyse your code. If your fleet isn't on 8.5, stop here — this is a package you adopt later, not a package you work around.

Installation is one command with nothing to register. No service provider, no config file, no publishable assets, no facade. It's a bag of namespaced functions.

composer require spatie/piper

Learn the calling convention before anything else. Every helper is curried: you pass the configuration arguments and get back a Closure that expects the subject. That is precisely what makes it pipe-shaped — |> evaluates the right-hand side, and if it's callable it invokes it with the left-hand value.

filter(fn ($post) => $post->views > 1000);       // Closure
$posts |> filter(fn ($post) => $post->views > 1000);  // array

Where to look first. The README shows the shape; spatie.be/docs/piper is the function inventory. Don't guess names — these are direct ports of Collection and Stringable methods, so collect($x)->kebab() becomes kebab(). If you're Laravel-fluent you already know most of the API; the docs are for confirming the method was actually ported and checking arity (e.g. join(', ', ', and ') still takes glue plus final glue). PORT.md explains the porting strategy, which is what you read when a helper you want is missing.

First use case: convert one transformation you already have, not a whole codebase. The ideal pilot is a 3+ step array/string chain currently written as a collect() pipeline or a run of temp variables, living in a support class, a form request, or a report builder. Ship it as a single PR and let review decide if the diff reads better.

// before
$titles = collect($posts)
    ->filter(fn (Post $p) => $p->views > 1000)
    ->map(fn (Post $p) => Str::of($p->title)->lower()->headline()->toString())
    ->values()
    ->all();

// after
use function Spatie\Piper\Arr\{filter, map, values};
use function Spatie\Piper\Str\{headline, lower};

$titles = $posts
    |> filter(fn (Post $post) => $post->views > 1000)
    |> map(fn (Post $post) => $post->title |> lower() |> headline())
    |> values();

Good pilots: export/report row builders, notification and mail body assembly, CSV import normalisation in a queued job, seeders and fixtures, small artisan utilities, prepareForValidation() in form requests. Bad pilots: anything in a per-request hot loop, anything a LazyCollection currently protects you from, and Blade templates.

Implementation Patterns

Keep your own curried helpers in one file. This is the real extension point — the library is not a closed set, and you compose it with your own domain vocabulary.

// app/Support/pipes.php
use Closure;

function round_to(int $precision): Closure
{
    return fn (float $value): float => round($value, $precision);
}

function excerpt(int $words): Closure
{
    return fn (string $value): string => str_word_count($value) > $words
        ? implode(' ', array_slice(str_word_count($value, 1), 0, $words)) . '…'
        : $value;
}

$teaser = $post->body |> trim(...) |> excerpt(30);

Interop with native PHP using first-class callable syntax. Any function whose subject is its first parameter composes directly — no wrapper needed. Mind the (...):

$total = $values |> array_sum(...);
$slug  = $title  |> trim(...) |> strtolower(...);

Name and export domain pipelines instead of inlining them. A pipeline over primitives is a pure function, so it exports cleanly and tests with plain assertions — no container, no facades, no database.

// app/Support/pipes.php
function winning_numbers(array $numbers): string
{
    return $numbers
        |> filter(fn (int $i) => $i % 2 === 0)
        |> map(fn (int $i) => $i ** 2)
        |> values()
        |> join(', ', ', and ')
        |> prefix('The winning numbers are ')
        |> suffix('.');
}

Treat the Collection boundary as an explicit seam. Pipelines take primitives; Eloquent and most Laravel code hands you Collection/Stringable. Convert once at the top and once at the bottom — never mid-pipe.

$rows = $query->get()                       // Collection
    ->map->only(['title', 'views'])
    ->all();                                // ← leave the Collection world

$csv = $rows
    |> filter(fn (array $r) => $r['views'] > 0)
    |> map(fn (array $r) => $r['title'] . "\t" . $r['views'])
    |> join("\n");

return Str::of($csv);                       // ← re-enter if the caller expects it

Form requests: build your normalised payload with a pipeline. prepareForValidation() is pure input shaping — exactly the shape pipes are good at.

protected function prepareForValidation(): void
{
    $this->merge([
        'email' => $this->input('email') |> trim(...) |> lower(),
        'tags'  => $this->input('tags', [])
            |> filter(fn ($t) => filled($t))
            |> map(fn ($t) => trim($t) |> lower())
            |> values()
            |> join(','),
    ]);
}

In queued jobs, shape payloads at the top of handle(). Import rows, webhook bodies, API responses — normalise into a primitive array, pipe it, hand the clean scalar to the domain layer.

Mix Arr and Str freely, but pick one idiom per method. Array-in, format, string-out is the genuine selling point. The hazard is a method that half-uses Str::of($x)->replace(...) and half-uses piper. Be consistent per file so a reviewer can hold one model in their head.

Use values() after every filter. filter preserves keys, exactly like Laravel's. If the next stage is map to arrays, json_encode, or anything key-sensitive, reindex first or you'll emit JSON objects where you meant arrays.

Test pipelines as plain PHPUnit value assertions. No RefreshDatabase, no $this->app, no fixtures — data providers over primitives.

public function test_winning_numbers(): void
{
    $this->assertSame(
        'The winning numbers are 4, 16, and 36.',
        winning_numbers([1, 2, 3, 4, 5, 6]),
    );
}

Set a house rule now. Pipes for three or more transformation steps over primitives; plain loops or collect() for hot paths, two-step transforms, and anything that needs laziness. Adoption as an unwritten habit is how you end up with three idioms in one class.

Gotchas and Tips

|> fn() and |> fn(...) are not the same thing. The right-hand expression is evaluated first; if the result is callable, it's invoked with the subject. So $x |> count() calls count() with zero arguments and fatals — use $x |> count(...) to hand over the Closure. This bites everyone in week one, and it's most dangerous with dump(): |> dump() eagerly dumps nothing useful and pipes null downstream. Write a labelled pass-through instead of relying on any debug helper's return value:

use Closure;

function tap_pipe(string $label): Closure
{
    return function ($value) use ($label) {
        dump([$label => $value]);
        return $value;                    // never depend on dump()'s return
    };
}

$result = $input
    |> filter($fn)
    |> tap_pipe('after filter')
    |> map($fn);

A missing use function import can fail silently. Without the import, filter(...) resolves to the global namespace and fatals — unless a global of that name exists (your own helper, a polyfill, another package), in which case you get wrong behaviour with no error. Always group-import explicitly, and keep imports adjacent to the pipeline:

use function Spatie\Piper\Arr\{filter, join, map, values};
use function Spatie\Piper\Str\{lower, prefix, replace, suffix};

Validate use function group imports against your static analysis before standardising. Function imports are weaker than method calls for IDE find-usages, rename refactors, and autocompletion, and some rule sets need use function-aware configuration. Run one real pipeline through PHPStan/Psalm — on a PHP 8.5 runtime — and through your CI image before this goes in a style guide.

Debuggability drops inside a single expression. There's no place to drop a dd() between stages (see above). When a pipeline misbehaves, temporarily explode it back into intermediate variables, isolate the bad stage, then re-collapse it. Don't debug by adding side effects into map.

Everything is eager. Each stage allocates a full intermediate array, and map adds a closure call per element on top of your callback. Fine for reports, exports, notifications, and form requests; wrong for a 500k-row import or a hot request path. Benchmark against a plain foreach before using it in high-throughput code, and reach for generators or LazyCollection there regardless — piper has no lazy story by design.

Type-guard the entry point. These helpers are primitive-only. There's no runtime type validation; a pipeline trusts the primitives you feed it. Because it isn't a Laravel package, there's no facade, no config, no Collection::macro-style registration — your extension point is a pipes.php of curried functions, so decide where that file lives and how it's autoloaded (Composer files autoload is the simplest route) before the second developer adds one.

Ports drift; pin and test. Version 1.x is young and the changelog is short (1.1.0 added callable support to sortByMany). Pin the minor, and when a helper's behaviour matters, assert it in a test rather than assuming parity with Collection/Stringable — edge cases around keys, empty arrays, and glue arguments are exactly where a port can diverge.

Keep it out of Blade, models, config, and middleware. Blade is where the pipeline syntax costs the most readability and gains the least; models are where you'd rather keep Eloquent's method surface intact; middleware is a hot path. The natural homes are app/Support/pipes.php, console commands, form requests, job handle() methods, mail/notification body builders, and API resource toArray() helpers.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console