hypothesisphp / result
Type-safe error handling for PHP — chain operations that can fail without exceptions.
v1.0.0
2026-07-17 10:25 UTC
Requires
- php: >=8.2
Requires (Dev)
- infection/infection: ^0.27
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.0 || ^11.0
- vimeo/psalm: ^5.0 || ^6.0
This package is not auto-updated.
Last update: 2026-07-18 08:45:48 UTC
README
Type-safe error handling for PHP — chain operations that can fail without exceptions.
Installation
composer require hypothesisphp/result
Requirements: PHP 8.2+, zero dependencies.
Quick Start
use Result\Result; use Result\Success; use Result\Failure; // Create results $success = Result::success('hello'); $failure = Result::failure('something went wrong'); // Chain operations $result = Result::success(5) ->map(fn (int $v) => $v * 2) ->filter(fn (int $v) => $v > 5, 'too small') ->map(fn (int $v) => $v + 1); // Unwrap safely $value = $result->unwrap(); // 11 $value = $result->unwrapOr(0); // 11 $value = $failure->unwrapOr('default'); // 'default' // Pattern match $message = $result->match( onSuccess: fn (int $v) => "Result: {$v}", onFailure: fn (string $e) => "Error: {$e}", );
Why Result?
Exceptions are unpredictable — they break control flow, hide in call stacks, and make error handling implicit. Result<T, E> makes errors explicit, composable, and type-safe.
Before (exceptions):
try { $user = $userService->find($id); $profile = $profileService->load($user); echo $profile->name; } catch (UserNotFoundException $e) { // handle } catch (ProfileException $e) { // handle }
After (Result):
$userService->find($id) ->flatMap(fn (User $u) => $profileService->load($u)) ->match( onSuccess: fn (Profile $p) => echo $p->name, onFailure: fn (string $e) => handle($e), );
Features
| Feature | Description |
|---|---|
| Immutable | All operations return new instances — original never changes |
| Composable | Chain map, flatMap, filter, tap, or, and |
| Pattern Match | match() with exhaustive success/failure handling |
| Collect | Aggregate multiple results with strategies |
| Try/Catch | Wrap throwing code into safe Results |
| Fiber Support | tryAsync() for Fiber-based async operations |
| PSR-3 Logger | Auto-log failures while preserving the chain |
| Serialization | Convert Results to/from arrays and JSON |
| Retry | Built-in retry with configurable attempts and delay |
| Timeout | Execute with timeout protection |
| PHPDoc Generics | Full @template<T, E> annotations for static analysis |
Core Methods
Creating Results
Result::success($value); // Success<T> Result::failure($error); // Failure<E> Result::fromNullable($value, $error); // null → Failure Result::fromCondition($bool, $value, $error); // bool → Success/Failure Result::try(fn () => risky()); // Throwable → Failure Result::tryCatch(fn () => risky(), \InvalidArgumentException::class); Result::tryAsync(fn () => fiberWork()); // Fiber-aware
Transforming
$result->map(fn ($v) => transform($v)); // Transform success value $result->mapError(fn ($e) => wrap($e)); // Transform error value $result->flatMap(fn ($v) => validate($v)); // Chain Result-returning operations $result->filter(fn ($v) => $v > 0, 'err'); // Conditional success
Extracting
$result->unwrap(); // Value or throw $result->unwrapOr('default'); // Value or default $result->unwrapOrElse(fn ($e) => calc()); // Value or compute from error $result->unwrapOrNull(); // Value or null $result->match(onSuccess: $fn1, onFailure: $fn2); // Pattern match
Side Effects
$result->tap(fn ($v) => log($v)); // Inspect without changing $result->tapError(fn ($e) => log($e)); // Inspect errors $result->ifSuccess(fn ($v) => notify()); // Execute on success $result->ifFailure(fn ($e) => alert()); // Execute on failure
Combining
$result->orElse(fn () => fallback()); // Recovery from failure $result->or(fn () => alternative()); // Try alternative $result->and($otherResult); // Chain to next result // Batch operations Result::collect([$r1, $r2, $r3]); // All succeed or first failure Result::collectWith($results, CollectStrategy::ALL_FAILURES); Result::collectAssoc(['name' => $r1, 'age' => $r2]); Result::all([$r1, $r2]); // Collect ALL errors on failure Result::race([fn () => try1(), fn () => try2()]); // First success wins Result::sequence([fn () => step1(), fn () => step2()]); Result::partition($results); // {successes: [...], failures: [...]}
Helper Functions
use function Result\success; use function Result\failure; use function Result\try_; use function Result\from_nullable; use function Result\from_condition; $result = success(42); $result = try_(fn () => json_decode($json, true)); $result = from_nullable($user, 'User not found');
Collect Strategies
use Result\CollectStrategy; // Stop at first failure (default) Result::collectWith($results, CollectStrategy::FIRST_FAILURE); // Collect ALL failures Result::collectWith($results, CollectStrategy::ALL_FAILURES); // Return only successes, ignore failures Result::collectWith($results, CollectStrategy::BEST_EFFORT);
ResultCollector
Fluent batch aggregation with statistics:
use Result\Collectors\ResultCollector; $collector = (new ResultCollector()) ->add(Result::success(1)) ->add(Result::failure('err')) ->add(Result::success(3)); $collector->count(); // 3 $collector->successCount(); // 2 $collector->failureCount(); // 1 $collector->successRatio(); // 0.67 $collector->allSucceeded(); // false $collector->anyFailed(); // true $collector->collect(); // Failure (first failure) $collector->collectAll(); // Failure (all errors) $collector->collectBestEffort(); // Success([1, 3]) $collector->partition(); // {successes: [1, 3], failures: ['err']}
PSR-3 Logger Integration
use Result\Interop\PsrResultLoggerBridge; $bridge = new PsrResultLoggerBridge($logger, 'error'); $result = $userService->find($id); $bridge->logFailure($result, 'User lookup failed: {error}'); // Or unwrap with automatic logging $user = $bridge->unwrapOrLog($result, 'Critical: user not found');
Serialization
use Result\Interop\ArrayResultSerializer; $serializer = new ArrayResultSerializer(); // To array $array = $serializer->serialize($result); // ['status' => 'success', 'value' => 42] // From array $result = $serializer->deserialize($array); // JSON support $json = $serializer->toJson($result); $result = $serializer->fromJson($json);
Async / Fiber Support
use Result\Interop\AsyncResultBridge; // Execute in Fiber $result = AsyncResultBridge::fromFiber(fn () => heavyWork()); // Concurrent execution $result = AsyncResultBridge::concurrent([ fn () => Result::success(fetchA()), fn () => Result::success(fetchB()), ]); // With timeout $result = AsyncResultBridge::withTimeout(fn () => slow(), 5000); // With retry $result = AsyncResultBridge::withRetry( fn () => unreliable(), maxRetries: 3, delayMs: 100, );
API Reference
Full API documentation: docs/api-reference.md
Requirements
- PHP 8.2 or higher
- No external dependencies
Testing
composer test # Run tests composer test:coverage # Run with HTML coverage composer analyse # PHPStan level 9 composer psalm # Psalm level 1 composer infection # Mutation testing composer ci # Full CI pipeline
Contributing
Please see CONTRIBUTING.md for details.
License
The MIT License (MIT). Please see LICENSE for more information.