Attempt Monad (Scala-Style Try)
The attempt() helper provides a Scala-inspired Try monad for handling exceptions fluently. It wraps code execution and lets you handle success/failure with various strategies.
Core Concepts
Section titled “Core Concepts”Success vs Failure
Section titled “Success vs Failure”The Attempt class represents a computation that may either succeed (Success) or fail (Failure). Unlike traditional try-catch, you handle both cases fluently:
use function Cline\Throw\attempt;
// Execute and get result$user = attempt(fn() => User::findOrFail($id))->get();
// Execute with fallback$user = attempt(fn() => User::find($id))->getOrElse(null);
// Convert to Option-like (null on failure)$user = attempt(fn() => loadUser())->toOption();
// Recover from failure$data = attempt(fn() => fetchFromApi())->recover(fn($e) => getCached());Scala-Style Methods
Section titled “Scala-Style Methods”Unwrap the result or throw the original exception:
// Throws original exception if it fails$user = attempt(fn() => User::findOrFail($id))->get();
// Equivalent to traditional:try { $user = User::findOrFail($id);} catch (Throwable $e) { throw $e;}getOrElse()
Section titled “getOrElse()”Get result or return a default value:
// Return null if not found$user = attempt(fn() => User::find($id))->getOrElse(null);
// Return empty array$items = attempt(fn() => fetchItems())->getOrElse([]);
// Return default object$config = attempt(fn() => loadConfig())->getOrElse(new Config());toOption()
Section titled “toOption()”Convert to Option monad (Some<T> or None):
use Cline\Monad\Option\Option;
// Returns Some<User> or None$user = attempt(fn() => loadUser())->toOption();
// Chain with Option methods$name = attempt(fn() => $user->profile->name) ->toOption() ->map(fn($n) => strtoupper($n)) ->unwrapOr('GUEST');
// Use with Option's unwrapOr$result = attempt(fn() => dangerousOp()) ->toOption() ->unwrapOr('default');
// Transform with map/filter$email = attempt(fn() => $user->email) ->toOption() ->filter(fn($e) => str_contains($e, '@')) ->map(fn($e) => strtolower($e)) ->unwrapOr('no-email@example.com');recover()
Section titled “recover()”Execute a callback to recover from failure:
// Recover with cached data$data = attempt(fn() => fetchFromApi()) ->recover(fn($e) => Cache::get('data', []));
// Log and return fallback$result = attempt(fn() => processPayment($order)) ->recover(function (Throwable $e) { Log::error('Payment failed', ['error' => $e->getMessage()]); return ['status' => 'pending']; });
// Transform exception into value$status = attempt(fn() => checkService()) ->recover(fn($e) => 'offline');Custom Exception Handling
Section titled “Custom Exception Handling”orThrow()
Section titled “orThrow()”Throw a custom exception instead of the original:
// Throw custom exception classattempt(fn() => parseJson($data)) ->orThrow(InvalidJsonException::class, 'Invalid JSON provided');
// Throw exception instanceattempt(fn() => loadResource($id)) ->orThrow(ResourceNotFoundException::notFound($id));
// Original exception is wrapped as previoustry { attempt(fn() => dangerousOp()) ->orThrow(ServiceException::class, 'Service failed');} catch (ServiceException $e) { $original = $e->getPrevious(); // Original exception}HTTP Abort Helpers
Section titled “HTTP Abort Helpers”abort()
Section titled “abort()”Abort HTTP request with status code:
use Cline\Throw\Support\HttpStatusCode;
attempt(fn() => authorize($user)) ->abort(HttpStatusCode::Forbidden, 'Access denied');Convenience Helpers
Section titled “Convenience Helpers”// 400 Bad Requestattempt(fn() => validateInput($data)) ->orBadRequest('Invalid input');
// 401 Unauthorizedattempt(fn() => authenticate($token)) ->orUnauthorized('Authentication required');
// 403 Forbiddenattempt(fn() => checkPermission($user, 'admin')) ->orForbidden();
// 404 Not Foundattempt(fn() => Post::findOrFail($id)) ->orNotFound('Post not found');
// 409 Conflictattempt(fn() => createUser($email)) ->orConflict('Email already exists');
// 422 Unprocessable Entityattempt(fn() => validator($data)->validate()) ->orUnprocessable('Validation failed');
// 429 Too Many Requestsattempt(fn() => rateLimiter()->attempt($key)) ->orTooManyRequests();
// 500 Internal Server Errorattempt(fn() => criticalOperation()) ->orServerError();Executing Different Callables
Section titled “Executing Different Callables”Closures
Section titled “Closures”$result = attempt(fn() => expensiveComputation())->get();Invokable Classes
Section titled “Invokable Classes”class ProcessPayment{ public function __invoke(Order $order): PaymentResult { // Process payment }}
$result = attempt(ProcessPayment::class)->getOrElse(null);$result = attempt(new ProcessPayment())->get();Classes with handle() Method
Section titled “Classes with handle() Method”class ProcessOrder{ public function handle(): OrderResult { // Process order }}
$result = attempt(new ProcessOrder())->get();Callable Arrays
Section titled “Callable Arrays”// Static methods$result = attempt([User::class, 'findByEmail']) ->getOrElse(null);
// Instance methods$service = new ApiService();$data = attempt([$service, 'fetchData']) ->recover(fn($e) => []);Real-World Examples
Section titled “Real-World Examples”API Calls with Fallbacks
Section titled “API Calls with Fallbacks”// Try API, fall back to cache, then default$data = attempt(fn() => Http::get($url)->json()) ->recover(fn($e) => Cache::get("api:{$url}")) ?? [];Safe Property Access
Section titled “Safe Property Access”// Deeply nested property access with Option$city = attempt(fn() => $user->address->location->city) ->toOption() ->unwrapOr('Unknown');
// Chain transformations$displayName = attempt(fn() => $user->profile->fullName) ->toOption() ->map(fn($name) => trim($name)) ->filter(fn($name) => $name !== '') ->unwrapOr('Anonymous');Database Operations
Section titled “Database Operations”// Find or create pattern$user = attempt(fn() => User::where('email', $email)->firstOrFail()) ->recover(fn($e) => User::create(['email' => $email]));File Operations
Section titled “File Operations”// Read file with fallback$content = attempt(fn() => file_get_contents($path)) ->getOrElse('');
// Parse JSON safely$data = attempt(fn() => json_decode($json, true, flags: JSON_THROW_ON_ERROR)) ->recover(fn($e) => []);Service Layer
Section titled “Service Layer”class PaymentService{ public function charge(Order $order): PaymentResult { return attempt(fn() => $this->gateway->charge($order)) ->recover(function (Throwable $e) use ($order) { Log::error('Payment failed', [ 'order_id' => $order->id, 'error' => $e->getMessage(), ]);
return PaymentResult::failed($e->getMessage()); }); }}Controller Actions
Section titled “Controller Actions”class PostController{ public function show(int $id) { $post = attempt(fn() => Post::findOrFail($id)) ->orNotFound('Post not found');
return view('posts.show', compact('post')); }
public function update(Request $request, int $id) { attempt(fn() => $this->authorize('update', Post::findOrFail($id))) ->orForbidden();
$post = attempt(fn() => $this->postService->update($id, $request->validated())) ->orServerError('Failed to update post');
return redirect()->route('posts.show', $post); }}Multi-Step Operations
Section titled “Multi-Step Operations”// Chain multiple risky operations$result = attempt(fn() => $this->validateData($input)) ->recover(fn($e) => throw ValidationException::invalid($e->getMessage()));
$processed = attempt(fn() => $this->processData($result)) ->recover(fn($e) => $this->fallbackProcessor($result));
$saved = attempt(fn() => $this->saveData($processed)) ->orThrow(PersistenceException::class, 'Failed to save');Pattern Comparison
Section titled “Pattern Comparison”Traditional Try-Catch
Section titled “Traditional Try-Catch”try { $user = User::findOrFail($id);} catch (ModelNotFoundException $e) { $user = null;}With Attempt + Option
Section titled “With Attempt + Option”$user = attempt(fn() => User::findOrFail($id)) ->toOption() ->unwrapOr(null);
// Or use the Option directly$user = attempt(fn() => User::findOrFail($id))->toOption();// Returns Some<User> or NoneTraditional Try-Catch with Custom Exception
Section titled “Traditional Try-Catch with Custom Exception”try { $data = fetchFromApi();} catch (Throwable $e) { Log::error('API failed', ['error' => $e->getMessage()]); throw new ServiceException('Service unavailable', previous: $e);}With Attempt
Section titled “With Attempt”$data = attempt(fn() => fetchFromApi()) ->recover(function ($e) { Log::error('API failed', ['error' => $e->getMessage()]); throw new ServiceException('Service unavailable', previous: $e); });Best Practices
Section titled “Best Practices”1. Use get() for Pure Unwrapping
Section titled “1. Use get() for Pure Unwrapping”When you want to propagate the original exception:
$user = attempt(fn() => User::findOrFail($id))->get();2. Use getOrElse() for Safe Defaults
Section titled “2. Use getOrElse() for Safe Defaults”When you have a reasonable default value:
$config = attempt(fn() => loadConfig())->getOrElse([]);3. Use toOption() for Option Monad Chaining
Section titled “3. Use toOption() for Option Monad Chaining”When you want to leverage Option’s map/filter/flatMap:
$name = attempt(fn() => $user->name) ->toOption() ->map(fn($n) => strtoupper($n)) ->unwrapOr('GUEST');
// Complex transformations$validEmail = attempt(fn() => $user->email) ->toOption() ->filter(fn($e) => filter_var($e, FILTER_VALIDATE_EMAIL)) ->map(fn($e) => strtolower($e)) ->unwrapOr(null);4. Use recover() for Side Effects
Section titled “4. Use recover() for Side Effects”When you need to log, notify, or transform errors:
attempt(fn() => sendEmail($user)) ->recover(function ($e) use ($user) { Log::warning('Email failed', ['user' => $user->id]); Queue::push(new SendEmailJob($user)); });5. Use orThrow() for Custom Exceptions
Section titled “5. Use orThrow() for Custom Exceptions”When you want to transform exceptions into your domain:
attempt(fn() => $this->externalApi->call()) ->orThrow(ServiceUnavailableException::class);6. Use HTTP Helpers in Controllers
Section titled “6. Use HTTP Helpers in Controllers”Keep controller code clean and expressive:
public function destroy(int $id){ $post = attempt(fn() => Post::findOrFail($id)) ->orNotFound();
attempt(fn() => $this->authorize('delete', $post)) ->orForbidden();
attempt(fn() => $post->delete()) ->orServerError();
return redirect()->route('posts.index');}Type Safety
Section titled “Type Safety”The Attempt class is fully typed with generics:
/** @var Attempt<User> */$userAttempt = attempt(fn() => User::find($id));
/** @var User|null */$user = $userAttempt->toOption();
/** @var User */$user = $userAttempt->get(); // Throws if failed
/** @var User|Guest */$user = $userAttempt->getOrElse(new Guest());