PHPackages                             haspadar/primus - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. haspadar/primus

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

haspadar/primus
===============

Primitive wrappers for PHP: strong typing for strings, ints, arrays, and more

v0.12.0(1mo ago)4443[1 issues](https://github.com/haspadar/primus/issues)MITPHPPHP ~8.3.16 || ~8.4.3 || ~8.5.0CI failing

Since Jun 4Pushed 1mo agoCompare

[ Source](https://github.com/haspadar/primus)[ Packagist](https://packagist.org/packages/haspadar/primus)[ RSS](/packages/haspadar-primus/feed)WikiDiscussions main Synced today

READMEChangelog (8)Dependencies (10)Versions (167)Used By (0)

   ![Primus logo](assets/primus-banner-light.svg)

[![CI](https://github.com/haspadar/primus/actions/workflows/sheriff.yml/badge.svg)](https://github.com/haspadar/primus/actions/workflows/sheriff.yml)[![Coverage](https://camo.githubusercontent.com/b98593b89787fd315fa9c31a996447d7d46e3fd23cc054c5fb433f82166daa25/68747470733a2f2f636f6465636f762e696f2f67682f68617370616461722f7072696d75732f6272616e63682f6d61696e2f67726170682f62616467652e737667)](https://codecov.io/gh/haspadar/primus)[![Mutation testing badge](https://camo.githubusercontent.com/90bf1b87f989d9285305f9506f3ec3807098adf8a09340dd422c9488ce8a4db9/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f7374796c653d666c61742675726c3d687474707325334125324625324662616467652d6170692e737472796b65722d6d757461746f722e696f2532466769746875622e636f6d25324668617370616461722532467072696d75732532466d61696e)](https://dashboard.stryker-mutator.io/reports/github.com/haspadar/primus/main)[![PHPStan Level](https://camo.githubusercontent.com/942bdbddc7b2adea1d63ed80793492d06d72ef41911edcba33310d0745581548/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d4c6576656c253230392d627269676874677265656e)](https://phpstan.org/)[![Psalm Level](https://camo.githubusercontent.com/7728b68c0a4248d07625d08c821b633855ee3516a2c4b215ab9fc608bc128358/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5073616c6d2d4c6576656c253230312d627269676874677265656e)](https://psalm.dev/)

---

Object‑Oriented PHP Primitives
==============================

[](#objectoriented-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 FuncOf(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
------------

[](#installation)

```
composer require haspadar/primus
```

Why?
----

[](#why)

- **The pipeline is a value.**
    Build it, pass it, store it, decorate it further. Nothing runs until `value()`.

    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(TextOf::str($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 `Bytes` source into a transformer — in production it can be a real `BytesOf(random_bytes(16))`, in tests a fixed `BytesOf("\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 anonymous `array`/`string`/`callable`.

    You can extend `Mapped` by wrapping it, not by passing more flags. A `Logged(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 is `readonly`; behaviour belongs to instances.

    You can pass a `Number` deep into your code without `?Number` types or null-guards at every boundary.

    ```
    $n = new IntegerOf(42);  // explicit type — never returns null
    $n->asInt();             // 42
    ```

Text
----

[](#text)

To trim and lowercase:

```
$text = (new Lowered(new Trimmed(TextOf::str('  Hello  '))))->value();
// "hello"
```

To take a substring:

```
$text = (new Sub(TextOf::str('Hello, world!'), 0, 5))->value();
// "Hello"
```

Lists
-----

[](#lists)

To filter and sort:

```
$big = (new Sorted(
    new Filtered(
        new ListOf(3, 1, 4, 1, 5, 9, 2, 6),
        new FuncOf(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
----

[](#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
-------

[](#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
---------

[](#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
--------

[](#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
--------

[](#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
----

[](#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
-----

[](#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
------------

[](#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 readonly` classes.**
    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 is `Sticky`. Fallback on failure is `FuncWithFallback`. Iteration with side-effect is `ForEach_`. No class carries a flag that toggles its behaviour.
- **Composition over inheritance.**
    Every class is `final`. You change behaviour by **wrapping** an object, not by subclassing it.
- **No `null` ever.**
    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`, no `isset`, no `empty`.**
    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 no `setName()` 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 for `value()`.

Enforced by [`haspadar/sheriff`](https://github.com/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](https://www.elegantobjects.org) (Yegor Bugayenko) and [cactoos](https://github.com/yegor256/cactoos).

Requirements
------------

[](#requirements)

PHP **8.3+**.

Code style tooling notes
------------------------

[](#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 $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:

```

    */src/Integer/*
    */src/Scalar/*

```

**[Sheriff](https://github.com/haspadar/sheriff)** — add to `.sheriff.yaml`:

```
override:
    php_cs_fixer.extend: "        'phpdoc_types' => ['groups' => ['meta', 'simple', 'alias'], 'exclude' => ['scalar', 'integer']],"
```

Working with AI agents
----------------------

[](#working-with-ai-agents)

Using an AI coding assistant (Claude Code, Codex, Cursor, Aider, …) with this library? See [`AGENTS.md`](AGENTS.md) for the namespace map, composition contract, antipatterns, and a step-by-step guide for adding new primitives.

License
-------

[](#license)

[MIT](LICENSE)

###  Health Score

48

—

FairBetter than 93% of packages

Maintenance87

Actively maintained with recent releases

Popularity22

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity63

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~29 days

Recently: every ~2 days

Total

12

Last Release

45d ago

PHP version history (3 changes)v0.1.0PHP 8.4.\*

v0.2.0PHP 8.2.\*

v0.6.0PHP ~8.3.16 || ~8.4.3 || ~8.5.0

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1282194?v=4)[Konstantinas Mesnikas](/maintainers/haspadar)[@haspadar](https://github.com/haspadar)

---

Top Contributors

[![haspadar](https://avatars.githubusercontent.com/u/1282194?v=4)](https://github.com/haspadar "haspadar (482 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Psalm, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/haspadar-primus/health.svg)

```
[![Health](https://phpackages.com/badges/haspadar-primus/health.svg)](https://phpackages.com/packages/haspadar-primus)
```

###  Alternatives

[ericmann/sessionz

PHP Session Manager Interface

585.4k2](/packages/ericmann-sessionz)[sevaske/zatca

Zatca laravel library

161.1k](/packages/sevaske-zatca)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
