majesko / fp-kit
Small functional programming utilities for PHP (pipe/compose, Result, Option, Validation).
Requires
- php: ^8.2
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.60
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-02-19 21:26:12 UTC
README
A lightweight functional programming toolkit for PHP 8.2+. This library provides immutable data types, function composition utilities, and array helpers that enable a clean functional programming style in PHP.
Features
- Monadic Types:
Result,Option, andValidationfor safe, composable error handling - Function Composition:
pipe,compose,tap, andpartialfor building complex operations - Array Utilities: Functional helpers like
map,filter,reduce,groupBy, andindexBy - Zero Dependencies: Pure PHP with no external dependencies
- Fully Typed: Strict types throughout with PHPStan level 7 compliance
- Comprehensive Tests: 79 tests with extensive edge case coverage
Table of Contents
- Installation
- Quick Start
- Function Composition
- Array Functions
- Result Type
- Option Type
- Validation Type
- API Reference
- Testing
- Contributing
- License
Installation
composer require majesko/fp-kit
Requires PHP 8.2 or higher.
Quick Start
<?php declare(strict_types=1); use function Majesko\FpKit\Functions\pipe; use function Majesko\FpKit\Result\{ok, err, map as rmap, bind as rbind}; use function Majesko\FpKit\Option\{some, none, map as omap}; // Function composition with pipe $result = pipe( fn($x) => $x * 2, fn($x) => $x + 10, fn($x) => $x / 2 )(5); // Result: 10 // Safe error handling with Result function divide(float $a, float $b): array { return $b === 0.0 ? err('Division by zero') : ok($a / $b); } $result = divide(10, 2); $doubled = rmap($result, fn($x) => $x * 2); // ['ok' => true, 'value' => 10] // Working with Option for nullable values $user = ['name' => 'John', 'email' => 'john@example.com']; $email = omap(some($user['email'] ?? null), 'strtoupper'); // ['some' => true, 'value' => 'JOHN@EXAMPLE.COM']
Function Composition
pipe
Chains functions left-to-right, passing the result of each function to the next.
use function Majesko\FpKit\Functions\pipe; $transform = pipe( fn($x) => $x * 2, fn($x) => $x + 5, fn($x) => "Result: $x" ); echo $transform(10); // "Result: 25"
compose
Chains functions right-to-left (mathematical composition).
use function Majesko\FpKit\Functions\compose; $f = fn($x) => $x + 1; $g = fn($x) => $x * 2; $composed = compose($f, $g); // Equivalent to: f(g(x)) echo $composed(5); // 11 (5 * 2 + 1)
tap
Executes a side effect without changing the value (useful for debugging).
use function Majesko\FpKit\Functions\{pipe, tap}; $result = pipe( fn($x) => $x * 2, tap(fn($x) => error_log("Debug: $x")), // Logs but doesn't modify fn($x) => $x + 10 )(5); // Result: 20 (and logs "Debug: 10")
partial
Creates a new function with some arguments pre-filled.
use function Majesko\FpKit\Functions\partial; $multiply = fn($a, $b) => $a * $b; $double = partial($multiply, 2); echo $double(5); // 10 echo $double(8); // 16
Array Functions
Functional array manipulation utilities.
use function Majesko\FpKit\Functions\{map, filter, reduce, groupBy, indexBy}; $numbers = [1, 2, 3, 4, 5]; // map: Transform each element $doubled = map($numbers, fn($x) => $x * 2); // [2, 4, 6, 8, 10] // filter: Keep elements that match predicate $evens = filter($numbers, fn($x) => $x % 2 === 0); // [2, 4] // reduce: Accumulate to a single value $sum = reduce($numbers, fn($acc, $x) => $acc + $x, 0); // 15 // groupBy: Group by key $users = [ ['name' => 'Alice', 'role' => 'admin'], ['name' => 'Bob', 'role' => 'user'], ['name' => 'Carol', 'role' => 'admin'] ]; $byRole = groupBy($users, fn($u) => $u['role']); // ['admin' => [['name' => 'Alice', ...], ['name' => 'Carol', ...]], 'user' => [...]] // indexBy: Create associative array by key $byName = indexBy($users, fn($u) => $u['name']); // ['Alice' => ['name' => 'Alice', ...], 'Bob' => [...], ...]
Result Type
Result represents an operation that can succeed (ok) or fail (err). It's perfect for error handling without exceptions.
Creating Results
use function Majesko\FpKit\Result\{ok, err}; $success = ok(42); // ['ok' => true, 'value' => 42] $failure = err('Not found'); // ['ok' => false, 'error' => 'Not found']
Checking Results
use function Majesko\FpKit\Result\{isOk, isErr}; if (isOk($result)) { // Handle success } if (isErr($result)) { // Handle error }
Transforming Results
use function Majesko\FpKit\Result\{map, bind, mapError}; // map: Transform the success value $result = ok(10); $doubled = map($result, fn($x) => $x * 2); // ['ok' => true, 'value' => 20] // bind (flatMap): Chain operations that return Results function parseNumber(string $s): array { return is_numeric($s) ? ok((float) $s) : err('Not a number'); } function squareRoot(float $n): array { return $n >= 0 ? ok(sqrt($n)) : err('Negative number'); } $result = bind(parseNumber('16'), fn($n) => squareRoot($n)); // ['ok' => true, 'value' => 4.0] // mapError: Transform the error value $result = err('user_not_found'); $mapped = mapError($result, fn($e) => "Error: $e"); // ['ok' => false, 'error' => 'Error: user_not_found']
Pattern Matching
use function Majesko\FpKit\Result\{matchResult, fold}; // matchResult: Pattern match on success/error $message = matchResult( $result, ok: fn($value) => "Success: $value", err: fn($error) => "Error: $error" ); // fold: Extract value with fallback $value = fold($result, ok: fn($v) => $v, err: fn($e) => 0 ); // unwrapOr: Get value or default use function Majesko\FpKit\Result\unwrapOr; $value = unwrapOr($result, 'default');
Real-World Example
use function Majesko\FpKit\Result\{ok, err, bind}; use function Majesko\FpKit\Functions\pipe; function findUser(int $id): array { $user = getUserFromDb($id); return $user ? ok($user) : err('User not found'); } function validateUser(array $user): array { return $user['active'] ?? false ? ok($user) : err('User is inactive'); } function getPermissions(array $user): array { return ok($user['permissions'] ?? []); } $result = pipe( fn() => findUser(123), fn($r) => bind($r, fn($u) => validateUser($u)), fn($r) => bind($r, fn($u) => getPermissions($u)) )(); // Result is either ok(['read', 'write']) or err('User not found')
Option Type
Option represents a value that may or may not exist, similar to null but composable.
Creating Options
use function Majesko\FpKit\Option\{some, none, fromNullable}; $present = some(42); // ['some' => true, 'value' => 42] $absent = none(); // ['some' => false, 'value' => null] // Create from potentially null value $option = fromNullable($maybeNull);
Transforming Options
use function Majesko\FpKit\Option\{map, bind}; // map: Transform the value if present $option = some(10); $doubled = map($option, fn($x) => $x * 2); // ['some' => true, 'value' => 20] $empty = none(); $result = map($empty, fn($x) => $x * 2); // ['some' => false, 'value' => null] (no transformation) // bind: Chain operations that return Options $result = bind(some('john@example.com'), fn($email) => str_contains($email, '@') ? some(strtoupper($email)) : none() );
Pattern Matching
use function Majesko\FpKit\Option\{matchOption, unwrapOr}; // matchOption: Handle both cases $message = matchOption( $option, some: fn($value) => "Found: $value", none: fn() => "Not found" ); // unwrapOr: Get value or default $value = unwrapOr($option, 'default');
Interop with Result
use function Majesko\FpKit\Option\toResult; $option = some(42); $result = toResult($option, 'Value was None'); // ['ok' => true, 'value' => 42] $option = none(); $result = toResult($option, 'Value was None'); // ['ok' => false, 'error' => 'Value was None']
Validation Type
Validation is similar to Result but accumulates multiple errors instead of short-circuiting on the first error. Perfect for form validation.
Creating Validations
use function Majesko\FpKit\Validation\{valid, invalid}; $success = valid(42); // ['valid' => true, 'value' => 42] $failure = invalid(['Required']); // ['valid' => false, 'errors' => ['Required']]
Combining Validations
use function Majesko\FpKit\Validation\{valid, invalid, combine}; function validateName(string $name): array { $errors = []; if (strlen($name) < 2) $errors[] = 'Name too short'; if (strlen($name) > 50) $errors[] = 'Name too long'; return empty($errors) ? valid($name) : invalid($errors); } function validateEmail(string $email): array { return str_contains($email, '@') ? valid($email) : invalid(['Invalid email']); } function validateAge(int $age): array { return $age >= 18 ? valid($age) : invalid(['Must be 18 or older']); } // combine: Collect all errors $result = combine([ validateName('A'), // Error: Name too short validateEmail('invalid'), // Error: Invalid email validateAge(15) // Error: Must be 18 or older ]); // ['valid' => false, 'errors' => ['Name too short', 'Invalid email', 'Must be 18 or older']] // All valid $result = combine([ validateName('John Doe'), validateEmail('john@example.com'), validateAge(25) ]); // ['valid' => true, 'value' => ['John Doe', 'john@example.com', 25]]
Lifting Functions
use function Majesko\FpKit\Validation\{lift, valid}; // lift: Apply a function to multiple Validations $createUser = fn($name, $email, $age) => [ 'name' => $name, 'email' => $email, 'age' => $age ]; $result = lift( $createUser, validateName('John'), validateEmail('john@example.com'), validateAge(25) ); // ['valid' => true, 'value' => ['name' => 'John', 'email' => 'john@example.com', 'age' => 25]]
Pattern Matching
use function Majesko\FpKit\Validation\{matchValidation, errors}; $message = matchValidation( $validation, valid: fn($value) => "Valid: " . json_encode($value), invalid: fn($errors) => "Errors: " . implode(', ', $errors) ); // Get errors array $errorList = errors($validation); // [] if valid, ['error1', 'error2'] if invalid
API Reference
Function Composition
| Function | Signature | Description |
|---|---|---|
pipe |
(callable ...$fns): callable |
Left-to-right function composition |
compose |
(callable ...$fns): callable |
Right-to-left function composition |
tap |
(callable $fn): callable |
Execute side effect without changing value |
partial |
(callable $fn, mixed ...$args): callable |
Partial application |
Array Functions
| Function | Signature | Description |
|---|---|---|
map |
(array $xs, callable $fn): array |
Transform each element |
filter |
(array $xs, callable $fn): array |
Keep elements matching predicate |
reduce |
(array $xs, callable $fn, mixed $init): mixed |
Reduce to single value |
groupBy |
(array $xs, callable $fn): array |
Group by key function |
indexBy |
(array $xs, callable $fn): array |
Create associative array by key |
Result Type
| Function | Signature | Description |
|---|---|---|
ok |
(mixed $value): array |
Create success Result |
err |
(mixed $error): array |
Create error Result |
isOk |
(array $r): bool |
Check if Result is success |
isErr |
(array $r): bool |
Check if Result is error |
map |
(array $r, callable $fn): array |
Transform success value |
bind |
(array $r, callable $fn): array |
Chain Result-returning operations |
mapError |
(array $r, callable $fn): array |
Transform error value |
unwrapOr |
(array $r, mixed $default): mixed |
Get value or default |
fold |
(array $r, callable $ok, callable $err): mixed |
Extract value with handlers |
matchResult |
(array $r, callable $ok, callable $err): mixed |
Pattern match |
Option Type
| Function | Signature | Description |
|---|---|---|
some |
(mixed $value): array |
Create present Option |
none |
(): array |
Create absent Option |
isSome |
(array $o): bool |
Check if Option has value |
isNone |
(array $o): bool |
Check if Option is empty |
map |
(array $o, callable $fn): array |
Transform value if present |
bind |
(array $o, callable $fn): array |
Chain Option-returning operations |
unwrapOr |
(array $o, mixed $default): mixed |
Get value or default |
fromNullable |
(?mixed $value): array |
Create from nullable value |
fromArray |
(array $arr, int|string $key): array |
Create from array key |
toResult |
(array $o, mixed $error): array |
Convert to Result |
matchOption |
(array $o, callable $some, callable $none): mixed |
Pattern match |
Validation Type
| Function | Signature | Description |
|---|---|---|
valid |
(mixed $value): array |
Create valid Validation |
invalid |
(array $errors): array |
Create invalid Validation |
isValid |
(array $v): bool |
Check if valid |
isInvalid |
(array $v): bool |
Check if invalid |
errors |
(array $v): array |
Get error list |
map |
(array $v, callable $fn): array |
Transform value if valid |
combine |
(array $validations): array |
Combine, accumulating errors |
lift |
(callable $fn, array ...$validations): array |
Apply function to validations |
toResult |
(array $v): array |
Convert to Result |
matchValidation |
(array $v, callable $valid, callable $invalid): mixed |
Pattern match |
Testing
Run the test suite:
# Run all tests composer test # Run PHPStan static analysis composer stan # Run code style checks composer cs:check # Fix code style issues composer cs:fix # Run all quality checks composer qa
Contributing
Contributions are welcome! This project follows:
- PSR-12 coding standard
- PHPStan level 7 static analysis
- Strict types throughout
- Comprehensive test coverage for all features
Please ensure all tests pass and code style checks succeed before submitting a PR.
License
MIT License. See LICENSE file for details.
Why fp-kit?
Modern PHP has powerful features like arrow functions, named arguments, and union types that make functional programming more ergonomic. This library provides:
- Type Safety: Strict types and PHPStan ensure correctness
- Composability: Functions designed to work together seamlessly
- No Magic: Simple array-based types, no complex OOP hierarchies
- Zero Overhead: No dependencies, minimal abstraction cost
- Practical: Focused on real-world use cases, not academic purity
Inspired by functional programming patterns from Rust, Haskell, and Scala, adapted for PHP's strengths.