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 Add the package via Composer:
composer require php-standard-library/result
No configuration or service provider registration is needed—it’s a standalone type.
First Use Case: Validation in a Laravel Controller
Replace a traditional try-catch block with Result for explicit error handling:
use PhpStandardLibrary\Result\Result;
use Illuminate\Http\Request;
public function store(Request $request) {
$result = Result::fromCallable(function() use ($request) {
$validated = $request->validate([
'email' => 'required|email',
'name' => 'required|string',
]);
return User::create($validated);
});
return $result->match(
fn($user) => response()->json($user, 201),
fn($error) => response()->json(['error' => $error], 422)
);
}
Key Methods to Know
Result::ok($value): Wrap a successful operation.Result::fail($error): Wrap a failed operation.Result::fromCallable($callable): Convert a callable that may throw into a Result.$result->match($onSuccess, $onFailure): Handle success/failure cases.$result->unwrap(): Get the value (throws if failure).$result->unwrapErr(): Get the error (throws if success).Use Result to chain operations functionally, avoiding nested if-else or try-catch blocks:
use PhpStandardLibrary\Result\Result;
function processOrder(Order $order) {
return Result::fromCallable(function() use ($order) {
return OrderValidator::validate($order)
->then(fn() => PaymentService::charge($order))
->then(fn() => OrderRepository::save($order));
});
}
$result = processOrder($order);
$result->match(
fn($order) => notifyUser($order),
fn($error) => logError($error)
);
Standardize HTTP responses using Result:
use Illuminate\Http\Response;
public function getUser($id) {
$result = Result::fromCallable(fn() => User::findOrFail($id));
return $result->match(
fn($user) => response()->json($user),
fn($error) => response()->json(['error' => $error], 404)
);
}
Extend Result with custom error types for better type safety:
class ValidationError {}
class PaymentError {}
function createUser(array $data): Result<User, ValidationError|PaymentError> {
$validation = validate($data);
if ($validation->isFailure()) {
return Result::fail(new ValidationError($validation->unwrapErr()));
}
return Result::fromCallable(fn() => User::create($data))
->mapErr(fn($e) => new PaymentError($e->getMessage()));
}
Create middleware to convert exceptions to Result::fail for consistent error handling:
public function handle($request, Closure $next) {
try {
return $next($request);
} catch (Throwable $e) {
return Result::fail($e->getMessage());
}
}
Simplify test assertions by treating failures as values:
public function test_user_creation_fails_with_invalid_email() {
$result = createUser(['email' => 'invalid', 'name' => 'Test']);
$this->assertTrue($result->isFailure());
$this->assertInstanceOf(ValidationError::class, $result->unwrapErr());
}
Handle job failures explicitly using Result:
public function handle() {
$result = Result::fromCallable(fn() => $this->processPayment());
return $result->match(
fn($success) => $this->notifySuccess(),
fn($error) => $this->notifyFailure($error)
);
}
Overusing unwrap()
$result->unwrap() on a failure throws an exception, defeating the purpose of Result.$result->isOk() or use match() before unwrapping.Ignoring Errors
$result->isOk() without handling the error case) can hide bugs.match() or unwrapErr() to ensure errors are handled.Performance with fromCallable
Result::fromCallable() catches exceptions, which can mask unexpected errors.Type Safety Gaps
Result<User, string>) may not work as expected in older PHP versions or IDEs.@phpstan-type annotations for better static analysis:
/** @phpstan-return Result<User, ValidationError> */
function createUser(array $data) { ... }
Middleware and Exceptions
Result handling unless wrapped.Result::fail in middleware (see Implementation Patterns).Log Results
Use tap() to log intermediate results for debugging:
$result = processOrder($order)
->tap(fn($r) => \Log::debug('Result state:', ['isOk' => $r->isOk()]));
Custom Error Messages Provide descriptive error messages for failures:
Result::fail(new ValidationError("Email is invalid: {$email}"))
IDE Support
Result types:
/**
* @return Result<User, ValidationError>
*/
function createUser(array $data) { ... }
Result types for better autocompletion.Testing Failures
Result::fail() in tests to simulate failures:
$this->mock(Result::class)->shouldReceive('fail')->andReturn(Result::fail(new ValidationError()));
Custom Result Classes
Extend Result for domain-specific behavior:
class DomainResult extends Result {
public function toArray(): array {
return $this->match(
fn($value) => ['success' => true, 'data' => $value],
fn($error) => ['success' => false, 'error' => $error->getMessage()]
);
}
}
Laravel Service Provider Bindings
Bind Result to the container for dependency injection:
$this->app->bind(Result::class, fn() => new Result());
Integration with Laravel Validation
Create a Result-aware validator:
use Illuminate\Validation\Validator;
class ResultValidator extends Validator {
public function validateResolved(): Result {
if ($this->fails()) {
return Result::fail($this->errors()->first());
}
return Result::ok(true);
}
}
Async/Await with Results
Use spatie/async or Laravel Queues to handle Result in async workflows:
Bus::dispatch(new ProcessOrderJob($order))
->then(fn($result) => $result->match(
fn($order) => notifyUser($order),
fn($error) => logError($error)
));
Result::ok()/Result::fail() for older versions.Result objects are immutable, so chaining operations (e.g., map()) creates new instances. For hot paths, consider:
$result = Result::ok($value)->map(...)->map(...); // Creates intermediate objects
Result::fromCallable() to defer computation until needed:
$result = Result::fromCallable(fn() => expensiveOperation());
How can I help you explore Laravel packages today?