php-standard-library/result
A lightweight Result type for PHP that represents success or failure as a value, enabling controlled error handling without exceptions. Helps you return, compose, and inspect outcomes explicitly for safer, predictable application flow.
Installation:
composer require php-standard-library/result
Ensure your project uses PHP 8.1+ (named arguments and union types required).
First Use Case:
Replace a simple try-catch block for validation with Result:
use PHPStandardLibrary\Result\Result;
function validateEmail(string $email): Result {
return filter_var($email, FILTER_VALIDATE_EMAIL)
? Result::ok($email)
: Result::err("Invalid email format");
}
$result = validateEmail("invalid@example");
if ($result->isSuccess()) {
$email = $result->unwrap(); // "invalid@example"
} else {
$error = $result->unwrapErr(); // "Invalid email format"
}
Key Classes to Know:
Result::ok($value): Success case.Result::err($error): Failure case.Result::fromCallable($callable): Wrap existing functions/methods.$result->match($success, $failure): Functional-style handling.Where to Look First:
Result type).Result methods: isSuccess(), isFailure(), unwrap(), unwrapErr(), map(), flatMap().Replace Laravel’s Validator exception-based approach with Result:
use Illuminate\Support\Facades\Validator;
function validateUserInput(array $data): Result {
$validator = Validator::make($data, [
'email' => 'required|email',
'password' => 'required|min:8',
]);
return $validator->fails()
? Result::err($validator->errors())
: Result::ok($data);
}
// Usage in Controller:
$validationResult = validateUserInput($request->all());
$validationResult->match(
fn($validData) => User::create($validData),
fn($errors) => response($errors, 422)
);
Map Result directly to Laravel’s Response facade:
use Illuminate\Http\Response;
function getUserOrFail(int $id): Result {
$user = User::find($id);
return $user ? Result::ok($user) : Result::err("User not found");
}
// Controller
$response = getUserOrFail($id)->match(
fn($user) => response($user),
fn($error) => response(['error' => $error], 404)
);
Model business logic as immutable functions returning Result:
function processOrder(Order $order): Result {
return Result::fromCallable(fn() => $order->charge())
->flatMap(fn($payment) => Result::fromCallable(fn() => $order->fulfill()));
}
// Usage
$orderResult = processOrder($order);
$orderResult->match(
fn($order) => notifyCustomer($order),
fn($error) => logError($error)
);
Propagate errors explicitly in Laravel Jobs:
use Illuminate\Bus\Queueable;
use Illuminate\Queue\InteractsWithQueue;
class ProcessPaymentJob implements ShouldQueue {
use Queueable, InteractsWithQueue;
public function handle() {
$result = Result::fromCallable(fn() => $this->chargeCustomer());
$result->match(
fn($success) => $this->dispatch([$success]),
fn($error) => $this->fail($error)
);
}
}
Short-circuit requests with Result:
public function handle(Request $request, Closure $next) {
$authResult = Result::fromCallable(fn() => Auth::user());
return $authResult->match(
fn($user) => $next($request),
fn($error) => response($error, 401)
);
}
flatMap)Compose multiple Result-returning operations:
function createUserWithProfile(array $data): Result {
return validateUserInput($data)
->flatMap(fn($validData) => Result::fromCallable(fn() => User::create($validData)))
->flatMap(fn($user) => Result::fromCallable(fn() => $user->profile()->create($data['profile'])));
}
Recover from failures gracefully:
function safeOperation(): Result {
return Result::fromCallable(fn() => riskyOperation())
->match(
fn($result) => Result::ok($result),
fn($error) => Result::ok(fallbackOperation($error))
);
}
Assert Result outcomes in tests:
public function test_validation_fails() {
$result = validateUserInput(['email' => 'invalid']);
$this->assertTrue($result->isFailure());
$this->assertEquals(['email' => ['The email must be a valid email address.']], $result->unwrapErr());
}
Bind Result adapters for reusable wrappers:
// app/Providers/AppServiceProvider.php
public function register() {
$this->app->bind(ResultWrapper::class, function () {
return new ResultWrapper();
});
}
class ResultWrapper {
public function safeCall(callable $callable): Result {
return Result::fromCallable($callable);
}
}
Extend Result with domain-specific errors:
class PaymentFailed extends \RuntimeException {}
class InsufficientFunds extends PaymentFailed {}
function processPayment(): Result {
return Result::fromCallable(fn() => chargePayment())
->mapErr(fn($e) => $e instanceof PaymentFailed ? $e : new PaymentFailed($e->getMessage()));
}
Log Err variants in middleware:
public function handle($request, Closure $next) {
$result = Result::fromCallable(fn() => $next($request));
$result->match(
fn($response) => $response,
fn($error) => (new LogError($error))->next($request)
);
}
class LogError {
public function __construct(private $error) {}
public function __invoke($request) {
\Log::error("Request failed: " . $this->error);
return response($this->error, 500);
}
}
Convert Result::err() to Laravel exceptions where needed:
$validationResult = validateUserInput($data);
if ($validationResult->isFailure()) {
throw new \Illuminate\Validation\ValidationException($validationResult->unwrapErr());
}
Overusing Result for Exceptions:
Result for unrecoverable errors (e.g., DB connection failures).Result for expected failures (e.g., validation, business logic).Result::fromCallable() only if you handle the error explicitly.Ignoring Result Values:
unwrap() on a Result::err() without checks.
// ❌ Dangerous
$user = $result->unwrap(); // Throws if $result is Err
isSuccess() or use match():
// ✅ Safe
$user = $result->isSuccess() ? $result->unwrap() : null;
Performance in Hot Paths:
Result objects in tight loops (e.g., bulk operations).Result instances or use early returns:
if (!$user) return Result::err("User not found");
Type Safety Gaps:
Err types when domain-specific errors are needed.
// ❌ Less clear
Result<string, string> $result;
// ✅ More explicit
Result<User, ValidationError> $result;
Result<User, ValidationError|PaymentFailed> $result;
Middleware/Service Provider Order:
Result will propagate through all layers.Result in each layer (e.g., middleware, controllers, services).Unwrapping Failures:
unwrapErr() to inspect errors:
$error = $result->unwrapErr();
\Log::error($error);
Stack Traces:
How can I help you explore Laravel packages today?