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.
- 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
- Installation
- Quick Start
- Function Composition
- Array Functions
- Result Type
- Option Type
- Validation Type
- API Reference
- Testing
- Contributing
- License
composer require majesko/fp-kitRequires PHP 8.2 or higher.
<?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']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"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)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")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); // 16Functional 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 represents an operation that can succeed (ok) or fail (err). It's perfect for error handling without exceptions.
use function Majesko\FpKit\Result\{ok, err};
$success = ok(42); // ['ok' => true, 'value' => 42]
$failure = err('Not found'); // ['ok' => false, 'error' => 'Not found']use function Majesko\FpKit\Result\{isOk, isErr};
if (isOk($result)) {
// Handle success
}
if (isErr($result)) {
// Handle error
}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']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');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 represents a value that may or may not exist, similar to null but composable.
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);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()
);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');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 is similar to Result but accumulates multiple errors instead of short-circuiting on the first error. Perfect for form validation.
use function Majesko\FpKit\Validation\{valid, invalid};
$success = valid(42); // ['valid' => true, 'value' => 42]
$failure = invalid(['Required']); // ['valid' => false, 'errors' => ['Required']]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]]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]]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| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
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 qaContributions 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.
MIT License. See LICENSE file for details.
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.