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

Vec Laravel Package

php-standard-library/vec

Functions for creating and transforming sequential, 0-indexed arrays (lists) in PHP. Part of the PHP Standard Library, offering a consistent, functional approach to building and manipulating vec-like list structures.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require php-standard-library/vec

The package is a namespace of functions, not a class with methods — everything lives under Psl\Vec\* (e.g. Psl\Vec\map, Psl\Vec\chunk). There is no service provider, no facade, and no config to publish. Composer's files autoload picks the functions up on composer dump-autoload.

Where to look first

  1. The function reference at php-standard-library.dev — browse the Vec namespace listing before writing anything by hand. Nine times out of ten the helper you want already exists (zip, enumerate, partition, group_by, reductions, unique_by, sort_by, chunk).
  2. The sibling Psl\Type package (Type\vec(), Type\shape()) — this is how you actually enforce the "sequential, 0-indexed" contract at runtime.
  3. release/6.x notes — the changelog itself lives in the main repository, so watch that, not this repo's releases.

First use case: normalising validated request input

<?php

declare(strict_types=1);

use function Psl\Vec\filter_nulls;
use function Psl\Vec\map;
use function Psl\Vec\unique;

final class StorePostRequest extends FormRequest
{
    /** @return list<string> */
    public function tags(): array
    {
        return unique(filter_nulls(map(
            (array) $this->input('tags', []),
            static fn (mixed $tag): ?string => is_string($tag) ? trim($tag) : null,
        )));
    }
}

Two things to notice immediately: declare(strict_types=1) at the top of every file (PSL is strict-typed and will not coerce for you), and use function imports — you cannot chain, so readability comes from named intermediate steps or a small transformer class.


Implementation Patterns

Normalise at the boundary, Vec in the middle

The highest-value pattern in a Laravel app is: validate raw input with Psl\Type, then never touch a raw array again until you hand data back to Eloquent or the response layer.

use Psl\Type;
use function Psl\Vec\map;

$payload = Type\shape([
    'items' => Type\vec(Type\shape([
        'sku' => Type\string(),
        'qty' => Type\positive_int(),
    ])),
])->coerce($request->validated());

$lines = map($payload['items'], fn (array $i) => new OrderLine($i['sku'], $i['qty']));

Type\vec() rejects associative arrays and gaps in the index with a Psl\Type\Exception\CoercionException, which is exactly the failure you want surfaced early rather than as a silent array_map oddity downstream.

DTO / API resource mapping

map returns a fresh list<T>, so it composes well with readonly DTO constructors and with the JsonResource layer:

use function Psl\Vec\map;
use function Psl\Vec\filter;
use function Psl\Vec\sort_by;

$active = filter($this->orders->all(), fn (Order $o) => $o->isActive());
$sorted = sort_by($active, fn (Order $o) => $o->createdAt);   // stable sort

return OrderResource::collection(collect(
    map($sorted, fn (Order $o) => OrderData::fromModel($o)),
));

Convert at the edges: ->all() going in, collect() coming out. Keep the transform in between pure.

Chunked writes for batch jobs

chunk is the day-to-day workhorse for queue jobs and importers:

use function Psl\Vec\chunk;
use function Psl\Vec\map;

foreach (chunk($rows, 500) as $batch) {
    Order::upsert(
        map($batch, fn (array $r) => [...$r, 'updated_at' => now()]),
        ['external_id'],
        ['status', 'total', 'updated_at'],
    );
}

Because chunk returns list<list<T>>, every batch is a proper list — no array_values() guard needed before upsert.

Reporting and aggregation without loops

use function Psl\Vec\group_by;
use function Psl\Vec\partition;
use function Psl\Vec\reduce;
use function Psl\Vec\reductions;
use function Psl\Vec\associate;

$byStatus   = group_by($orders, fn (Order $o) => $o->status->value);
[$failed, $ok] = partition($orders, fn (Order $o) => $o->paymentCaptured());
$total      = reduce($orders, fn (int $carry, Order $o) => $carry + $o->total, 0);
$running    = reductions($ledger, fn (int $c, Entry $e) => $c + $e->amount, 0); // running balance
$indexed    = associate($users, fn (User $u) => [$u->id, $u]);

partition returns [non-matching, matching] — destructure with named variables so nobody has to remember which side is which.

Pipelines

There is no fluent builder here. Two idioms scale well:

// 1. Named locals — easiest to debug, breakpoint, and read in PRs.
$tags   = map($input, 'trim');
$tags   = filter($tags, fn (string $t) => $t !== '');
$tags   = unique($tags);

// 2. Psl\Pipeline for longer chains.
use function Psl\Pipeline\compose;
$normalise = compose(
    fn (array $a) => map($a, 'trim'),
    fn (array $a) => filter($a, fn (string $t) => $t !== ''),
    unique(...),
);

Wrap recurring chains in a dedicated App\Support\Transform (or per-domain OrderList class) rather than repeating five nested calls across controllers. That wrapper is also your escape hatch if you ever swap the library out.

Testing

Vec values are plain PHP arrays, so assertions stay trivial; the value is in the generic annotations:

/** @var list<int> $result */
$result = map([1, 2, 3], fn (int $n) => $n * 2);

$this->assertSame([2, 4, 6], $result);
$this->assertSame([], filter([], fn () => true));

Enable Larastan/PHPStan at a decent level — PSL's templates will then propagate list<User> through your DTO mappers and catch the moment someone feeds an associative array into a Vec function.


Gotchas and Tips

Functions, not a class. You will not write Vec::map(). Every call site needs use function Psl\Vec\map; (or a fully-qualified call). If your team's mental model is Collection, budget a short ramp-up — the biggest adjustment is that composition is inside-out (map(filter(...))) rather than left-to-right.

The list contract is enforced, not suggested. Psl\Vec\* expects list<T>. Passing ['a' => 1, 'b' => 2] is a type error under static analysis and can throw at runtime via Type\vec(). For keyed data use Psl\Dict\*, or Laravel's Collection. A common bug is feeding $request->validated() (a map) straight into map(); wrap with array_values() or, better, Type\vec(...) first.

Key semantics differ from the native functions. array_filter preserves keys and produces gaps; Psl\Vec\filter re-indexes into a clean list. That is the whole point of the package — but it also means array_keys/array_values noise disappears from your code, and any code that relied on preserved keys will silently change behaviour. Audit before swapping existing array_filter calls.

first() / last() throw on empty. PSL raises Psl\Exception\InvariantViolationException. Use first_or_null() / last_or_null() when the collection may legitimately be empty (very common in request-scoped code), and reserve the throwing variants for genuinely invariant cases.

Strict types bites on numerics. String integers ("42") will not be coerced to int inside Type\positive_int() unless you use coerce() rather than assert(). Pick deliberately: coerce at HTTP boundaries, assert internally.

Performance is fine, not free. Each call is a function invocation plus an internal loop, so it's marginally slower than an inline foreach or array_map. That's irrelevant in controllers and jobs, and measurable inside loops over 100k+ rows. Rule of thumb: use Vec at the composition level, drop to a plain foreach for the innermost hot loop. Benchmark with phpbench before optimising anything.

Don't leak Vec semantics into the framework layer. Eloquent, JsonResource, and Blade all speak Collection/array. Return list<T> from your domain classes, then collect() or ->toArray() at the boundary. Mixing map() (PSL) and ->map() (Collection) in one expression is the fastest way to confuse a reviewer — pick one per layer.

No Laravel integration means no magic, which is a feature. There's no facade to mock and no container binding needed, so unit tests need zero bootstrapping. If you want DI-friendly ergonomics, wrap the functions in a small injectable class (final class ListTransformer) so consumers can swap or fake it.

Version pinning matters more than usual. This package is a thin slice of a monorepo; the real changelog and any breaking changes are tracked in the main repo. Pin to a caret constraint on the current major, keep an eye on the main-repo releases before composer update, and check the PHP platform requirement against your Laravel version before bumping majors.

Extension point: your own namespace. If you find yourself repeatedly needing project-specific helpers (toOptions(), pluckIds()), define them in App\Support\Vec composing the PSL primitives. That keeps the third-party surface small and gives you a single place to change if the package's low adoption ever makes you reconsider it.

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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style