haspadar / primus
Primitive wrappers for PHP: strong typing for strings, ints, arrays, and more
Requires
- php: ~8.3.16 || ~8.4.3 || ~8.5.0
Requires (Dev)
- haspadar/sheriff: ^0.29
- dev-main
- v0.11.0
- v0.10.0
- v0.9.0
- v0.8.0
- v0.7.0
- v0.6.0
- v0.5.0
- v0.4.0
- v0.3.0
- v0.2.0
- v0.1.0
- dev-text-prefix-suffix-of
- dev-text-formatted
- dev-text-concatenated
- dev-text-reversed
- dev-text-contains-isblank
- dev-text-starts-ends-with
- dev-text-decorators-first-class-callables
- dev-text-mapped-envelope
- dev-text-of-named-constructors
- dev-bump-sheriff-0.30
- dev-decimal-docs
- dev-decimal-sticky
- dev-decimal-abs
- dev-decimal-div-mod-of
- dev-decimal-envelope
- dev-decimal-max-min-of
- dev-decimal-mult-of
- dev-decimal-sum-of
- dev-decimal-asstring
- dev-decimal-interface-and-decimalof
- dev-drop-side-effect-facades
- dev-docs-integer-and-tooling
- dev-drop-number-concrete-classes
- dev-bump-sheriff-0.29.3
- dev-int-sticky
- dev-bump-sheriff-0.29.2
- dev-int-abs
- dev-int-aggregates-div-mod
- dev-rename-intnumber-to-integer
- dev-int-aggregates-max-min
- dev-int-aggregates-sum-mult
- dev-int-number
- dev-number-as-text
- dev-drop-comparison-scalars
- dev-readme-design-rules-spacing
- dev-add-agents-md
- dev-number-sticky
- dev-drop-run-once
- dev-readme-refresh
- dev-cleanup-stale-configs
- dev-root-cause
- dev-proc-module
- dev-uuid-v7
- dev-uuid-v4
- dev-bytes-hashes
- dev-bytes-primitive
- dev-time-primitive
- dev-drop-since-tags
- dev-number-max
- dev-number-avg
- dev-number-mult-and-div
- dev-number-negated
- dev-number-of-text
- dev-number-primitive
- dev-drop-redundant-constructors
- dev-map-intersect-assoc
- dev-map-diff-assoc
- dev-list-intersection
- dev-list-difference
- dev-list-index-of
- dev-list-contains
- dev-list-chunks
- dev-map-unique
- dev-list-unique
- dev-list-range
- dev-map-sliced
- dev-list-sliced
- dev-lazy-joined-text
- dev-lazy-random-text
- dev-parametric-decorators-via-mapped
- dev-simple-decorators-via-mapped
- dev-mapped-text
- dev-lazy-text-decorators
- dev-readme-cactoos-style
- dev-list-sorted-by
- dev-list-sorted
- dev-primus-runtime-exception
- dev-map-plucked-by
- dev-raise-pr-size-limit
- dev-list-plucked
- dev-map-combined
- dev-map-intersect
- dev-map-diff
- dev-map-values
- dev-map-keys
- dev-joined-lists
- dev-sheriff-tooling
- dev-untrack-claude-md
- dev-public-assets-outside-docs
- dev-key-aware-map-filtering
- dev-key-aware-map-values
- dev-current-docs
- dev-map-decorators
- dev-add-map
- dev-drop-iterable
- dev-list-decorators
- dev-add-list
- dev-drop-logic
- dev-require-pr-label
- dev-rename-iterableof
- dev-rename-xorof
- dev-rename-orof
- dev-auto-release-notes
- dev-rename-andof
- dev-drop-afferent-coupling-suppress
- dev-tidy-exception-annotations
- dev-drop-constructor-init-suppresses
- dev-iterators-via-generators
- dev-drop-missing-throws-suppresses
- dev-piqule-extend
- dev-piqule-follow-up
- dev-enable-piqule-ci
- dev-phpstan-suppress
- dev-phpstan-cleanup
- dev-capitalize-phpdoc-summaries
- dev-bump-piqule
- dev-document-constructors
- dev-drop-phpdoc-override
- dev-piqule-migration
- dev-docs/readme-050
- dev-feature/iterator-iterable
- dev-feature/func-constraints-and-messages
- dev-docs/add-hoc-badge-
- dev-refactor/remove-custom-exception
- dev-feature/func
- dev-feature/func-scalar
- dev-ci/infection-badge
- dev-feature/text-components
- dev-chore/rename-to-primus
- dev-mono-rename
- dev-kill-mutants
- dev-15-validations
- dev-12-scalar
- dev-9-disable-deptrac
- dev-8-text-envelope
- dev-feature/6-text-component
- dev-chore/4-ci-pipeline
- dev-chore/2-test-scripts-and-lock
This package is auto-updated.
Last update: 2026-05-17 00:06:27 UTC
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.Pass any
Bytessource into a transformer — in production it can be a realBytesOf(random_bytes(16)), in tests a fixedBytesOf("\x00\x01…")— no framework, no mocking library, just a different constructor argument.$bytes = new BytesOf(random_bytes(16)); $hex = new HexEncoded($bytes);
-
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 IntegerOf(42); // explicit type — 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:
$result = (new And_( new Constant(true), new Not(new Constant(false)), ))->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();
Integers
To wrap a native int and read its projections:
$n = new IntegerOf(42); $n->asInt(); // 42 $n->asFloat(); // 42.0 $n->asText()->value(); // "42"
To aggregate a list of integers:
$total = (new SumOf( new IntegerOf(10), new IntegerOf(20), new IntegerOf(12), ))->asInt(); // 42
Decimals
To wrap an arbitrary-precision decimal value:
$d = new DecimalOf('100000000000000.000001'); $d->asString(); // "100000000000000.000001" — exact, beyond float53 $d->asText()->value(); // "100000000000000.000001"
Float and int sources stay exact at their own precision:
(new DecimalOfFloat(0.3))->asString(); // "0.3" (new DecimalOfInt(42))->asString(); // "42"
To do bcmath arithmetic at a chosen scale (digits past the decimal point):
$sum = new SumOf( new DecimalOf('0.1'), new DecimalOf('0.2'), 1, ); $sum->asString(); // "0.3" $ratio = new DivOf(new DecimalOf('1'), new DecimalOf('3'), 4); $ratio->asString(); // "0.3333"
Time
To wrap an existing timestamp and format it:
$ts = new TimeOf('2026-05-12T12:00:00Z'); (new Iso($ts))->value(); // "2026-05-12T12:00:00+00:00"
Side-effect sources like the current moment stay on the caller — pass
new TimeOf(new DateTimeImmutable()) when you need now, and a fixed
new TimeOf('2026-05-12T12:00:00Z') in tests.
Bytes
To hash and hex-encode raw bytes:
$digest = (new HexEncoded(new Sha256(new BytesOf('hello'))))->value(); // "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
To base64-encode bytes for transport:
$encoded = (new Base64Encoded(new BytesOf('hello')))->value(); // "aGVsbG8="
Random byte sources stay on the caller — feed any Bytes you want, in
production new BytesOf(random_bytes(16)), in tests a fixed
new BytesOf("\x01\x02…").
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+.
Code style tooling notes
Primus exports class names that collide with PHP pseudo-types in phpdoc:
Primus\Integer\Integer, Primus\Scalar\Scalar. Default rules in
php-cs-fixer and slevomat/coding-standard compare type names case
insensitively, so @param Scalar<T> $x is flagged as if it were the
pseudo-type scalar. If your project enforces these tools, exclude the
names explicitly.
php-cs-fixer — pass the names to the exclude option of
phpdoc_types:
'phpdoc_types' => ['exclude' => ['scalar', 'integer']],
phpcs (slevomat) — disable
SlevomatCodingStandard.TypeHints.LongTypeHints for the affected files:
<rule ref="SlevomatCodingStandard.TypeHints.LongTypeHints"> <exclude-pattern>*/src/Integer/*</exclude-pattern> <exclude-pattern>*/src/Scalar/*</exclude-pattern> </rule>
Sheriff — add to
.sheriff.yaml:
override: php_cs_fixer.extend: " 'phpdoc_types' => ['groups' => ['meta', 'simple', 'alias'], 'exclude' => ['scalar', 'integer']],"
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.