haspadar/primus
最新稳定版本:v0.11.0
Composer 安装命令:
composer require haspadar/primus
包简介
Primitive wrappers for PHP: strong typing for strings, ints, arrays, and more
README 文档
README
Object‑Oriented PHP Primitives
Primus is a library of object‑oriented PHP primitives. It provides common operations as small composable objects instead of functions.
Procedural PHP buries the steps inside out — you read from the innermost call:
$result = array_values(array_filter([3, 1, 4, 1, 5], fn ($x) => $x > 2)); sort($result);
Primus reads top to bottom — each step is a named object:
(new Sorted( new Filtered( new ListOf(3, 1, 4, 1, 5), new PredicateOf(fn (int $x) => $x > 2), ), ))->value();
The pipeline is a value itself: store it, pass it around, decorate it further.
Reading the result is always explicit — call value().
Installation
composer require haspadar/primus
Why?
-
The pipeline is a value.
Build it, pass it, store it, decorate it further. Nothing runs untilvalue().You can return a pipeline from a function, cache it, or wrap it in retry/logging — things you can't do with a procedural chain.
$headline = new Lowered(new Trimmed(new TextOf($raw))); $cached = new Sticky(new ScalarOf(fn () => $headline->value())); // No work done yet.
-
Constructors only remember.
No I/O, no branches, no work in__construct— just dependency capture.You can substitute
RandomByteswith a fixedBytesOfin tests — no framework, no mocking library, just a different constructor argument.$uuid = new UuidV4(new RandomBytes(16)); $hex = new HexEncoded($uuid);
-
Every operation is a class.
Named types replace anonymousarray/string/callable.You can extend
Mappedby wrapping it, not by passing more flags. ALogged(new Mapped(...))decorator works the same way.$doubled = new Mapped( new ListOf(1, 2, 3), new FuncOf(fn (int $x): int => $x * 2), );
-
No
null, no mutation, no statics.
Missing input fails fast; state isreadonly; behaviour belongs to instances.You can pass a
Numberdeep into your code without?Numbertypes or null-guards at every boundary.$n = new NumberOfText(new TextOf('42')); // throws on bad input — never returns null $n->asInt(); // 42
Text
To trim and lowercase:
$text = (new Lowered(new Trimmed(new TextOf(' Hello '))))->value(); // "hello"
To take a substring:
$text = (new Sub(new TextOf('Hello, world!'), 0, 5))->value(); // "Hello"
Lists
To filter and sort:
$big = (new Sorted( new Filtered( new ListOf(3, 1, 4, 1, 5, 9, 2, 6), new PredicateOf(static fn (int $x): bool => $x > 2), ), ))->value(); // [3, 4, 5, 6, 9]
To pluck a column from a list of rows:
$names = (new Plucked( new ListOf( ['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Bob'], ), 'name', ))->value(); // ['Alice', 'Bob']
Maps
To merge two maps with last‑wins precedence:
$merged = (new Merged( new MapOf(['a' => 1, 'b' => 2]), new MapOf(['b' => 99, 'c' => 3]), ))->value(); // ['a' => 1, 'b' => 99, 'c' => 3]
To index a list of rows by one column, with values from another:
$byId = (new PluckedBy( new ListOf( ['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Bob'], ), 'id', 'name', ))->value(); // [1 => 'Alice', 2 => 'Bob']
Scalars
To compose lazy boolean logic:
$between = (new And_( new GreaterThan(new Constant(5), new Constant(0)), new LessThan(new Constant(5), new Constant(10)), ))->value(); // true
To memoize an expensive computation:
$cached = new Sticky( new ScalarOf(static fn () => expensive_computation()), ); $cached->value(); // expensive_computation() runs once $cached->value(); // cached
To unwrap an exception chain to its underlying cause:
try { $repo->save($entity); } catch (\Throwable $e) { $root = (new RootCause($e))->value(); logger()->error($root->getMessage()); }
Functions
To wrap a callable as a reusable, swappable object:
$double = new FuncOf(static fn (int $x): int => $x * 2); $double->apply(21); // 42
To memoize a function by its input:
$cached = new StickyFunc( new FuncOf(static fn (int $id): User => $repo->find($id)), ); $cached->apply(1); // hits the repo $cached->apply(1); // cached
To fall back to another function on failure:
$safe = new FuncWithFallback( new FuncOf(static fn (string $url): string => http_get($url)), new FuncOf(static fn (string $url): string => ''), );
To run a side-effect over every list element:
(new ForEach_( new ListOf('a', 'b', 'c'), new ProcOf(fn (string $s) => error_log($s)), ))->exec();
Numbers
To parse and read a numeric value:
$n = new NumberOfText(new TextOf('42')); $n->asInt(); // 42 $n->asFloat(); // 42.0
To aggregate a list of numbers:
$total = (new SumOf( new NumberOf(10), new NumberOf(20), new NumberOf(12), ))->asFloat(); // 42.0 $avg = (new AvgOf(new NumberOf(1), new NumberOf(2), new NumberOf(3)))->asFloat(); // 2.0
Time
To capture and format the current moment:
$now = new Now(); (new Iso($now))->value(); // e.g. "2026-05-13T07:14:00+00:00"
To parse an existing timestamp:
$ts = new TimeOf('2026-05-12T12:00:00Z'); $ts->value()->format('Y-m-d'); // "2026-05-12"
Bytes
To hash and hex-encode raw bytes:
$digest = (new HexEncoded(new Sha256(new BytesOf('hello'))))->value(); // "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
To generate a UUID v4 from injectable randomness:
$uuid = (new HexEncoded(new UuidV4(new RandomBytes(16))))->value(); // 32 lowercase hex chars
To make a time-ordered UUID v7 — sortable, ideal for database keys:
$uuid = (new HexEncoded(new UuidV7(new Now(), new RandomBytes(10))))->value();
Design rules
Every primitive in this library is built to the same set of rules. They explain what you can expect from any class you pick and how your own extensions should look.
-
final readonlyclasses.
Every instance is a value — safe to share, pass, decorate, without defensive copies. There are no setters and no inheritance points for "convenience" overrides. -
No work in constructors.
Building a graph of objects is always free — no I/O, no parsing, no branching. Failures surface in the computation method (value()/asInt()/exec()…), at the call site that asked for the result. -
One class, one behaviour.
When you need two behaviours, compose two classes. Memoization isSticky. Fallback on failure isFuncWithFallback. Iteration with side-effect isForEach_. No class carries a flag that toggles its behaviour. -
Composition over inheritance.
Every class isfinal. You change behaviour by wrapping an object, not by subclassing it. -
No
nullever.
No method returns it, no method accepts it. There is no?Text, no?Number. Missing data fails fast at the boundary with a real exception. -
No
static, noisset, noempty.
Behaviour belongs to instances, never to classes. Signatures must be honest — no hidden "I might be absent" checks. -
No getters and setters.
A class exposes behaviour, not data.name()returns a value because asking is a behaviour; there is nosetName()because changing state means constructing a new object. -
Computation is lazy.
Nothing runs until you call the computation method. A pipeline built with ten decorators costs no CPU until you ask forvalue().
Enforced by haspadar/sheriff — a
curated bundle of PHPStan level 9, Psalm with custom EO rules,
PHP‑CS‑Fixer, PHPMD, PHPMetrics, Infection, and repository lints.
Inspired by Elegant Objects (Yegor Bugayenko) and cactoos.
Requirements
PHP 8.3+.
Working with AI agents
Using an AI coding assistant (Claude Code, Codex, Cursor, Aider, …) with
this library? See AGENTS.md for the namespace map,
composition contract, antipatterns, and a step-by-step guide for adding
new primitives.
License
统计信息
- 总下载量: 443
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 4
- 点击次数: 7
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2025-10-10