Basic Usage
The ConditionallyThrowable trait provides four core methods for conditional exception throwing. All methods support both boolean values and callbacks for lazy evaluation.
throwIf()
Section titled “throwIf()”Throws the exception when the condition evaluates to true:
use App\Exceptions\ValidationException;
// Basic guard clauseValidationException::invalidEmail()->throwIf(!filter_var($email, FILTER_VALIDATE_EMAIL));
// Null checkingMissingResourceException::notFound()->throwIf($user === null);
// Type validationInvalidTypeException::expectedArray()->throwIf(!is_array($data));Real-World Example
Section titled “Real-World Example”class UserService{ public function findOrFail(int $id): User { $user = User::find($id);
UserNotFoundException::withId($id)->throwIf($user === null);
return $user; }}throwUnless()
Section titled “throwUnless()”Throws the exception when the condition evaluates to false:
// Interface validationInvalidTokenableException::mustImplementInterface() ->throwUnless($tokenable instanceof HasApiTokens);
// Permission checkingUnauthorizedException::missingPermission() ->throwUnless($user->can('admin'));
// State validationInvalidStateException::notPublished() ->throwUnless($post->isPublished());Real-World Example
Section titled “Real-World Example”class PaymentProcessor{ public function process(Order $order): void { PaymentException::orderNotPayable() ->throwUnless($order->canAcceptPayment());
// Process payment... }}Comparison with Laravel Helpers
Section titled “Comparison with Laravel Helpers”Before (using throw_if helper)
Section titled “Before (using throw_if helper)”throw_if($user === null, UserNotFoundException::withId($id));throw_unless($user->can('admin'), UnauthorizedException::class);After (using Throw trait)
Section titled “After (using Throw trait)”UserNotFoundException::withId($id)->throwIf($user === null);UnauthorizedException::missingPermission()->throwUnless($user->can('admin'));Readability Benefits
Section titled “Readability Benefits”The fluent pattern offers several advantages:
- Left-to-right reading: Exception details come first, then the condition
- Natural chaining: Works seamlessly with static factory methods
- Better autocomplete: IDEs suggest all available factory methods
- More explicit: The exception type and message are immediately visible
Chaining with Static Factories
Section titled “Chaining with Static Factories”Throw works beautifully with named constructors:
final class AuthenticationException extends RuntimeException{ use ConditionallyThrowable;
public static function invalidCredentials(): self { return new self('Invalid username or password'); }
public static function accountLocked(): self { return new self('Account has been locked due to too many failed attempts'); }
public static function sessionExpired(): self { return new self('Your session has expired. Please log in again'); }}
// UsageAuthenticationException::invalidCredentials()->throwIf(!Hash::check($password, $user->password));AuthenticationException::accountLocked()->throwIf($user->isLocked());AuthenticationException::sessionExpired()->throwIf($session->isExpired());Complex Conditions
Section titled “Complex Conditions”You can use any boolean expression:
// Multiple conditions with &&InvalidConfigurationException::missingRequiredFields() ->throwIf(empty($config['api_key']) && empty($config['secret']));
// NegationInvalidStateException::unexpectedStatus() ->throwIf(!in_array($order->status, ['pending', 'processing']));
// Method callsRateLimitException::tooManyAttempts() ->throwIf($this->rateLimiter->tooManyAttempts($key));Lazy Evaluation with Callbacks
Section titled “Lazy Evaluation with Callbacks”All conditional methods support callbacks for lazy evaluation, which only execute when needed:
// Expensive database check - only runs if neededUserNotFoundException::notFound() ->throwIf(fn() => User::where('email', $email)->doesntExist());
// Complex permission checkUnauthorizedException::forbidden() ->throwUnless(fn() => $user->can('edit', $post) && !$post->isLocked());
// Rate limiting checkRateLimitException::exceeded() ->throwIf(fn() => !$this->rateLimiter->allow($key, 60));
// Multiple queries deferredDataException::invalid() ->throwUnless(fn() => $this->validator->passes() && $this->exists($id));When to Use Callbacks
Section titled “When to Use Callbacks”Use callbacks when:
- The condition involves expensive operations (database queries, API calls)
- The check should only run if previous conditions pass
- You want to defer evaluation for performance
- The condition has side effects you want to control
Supporting Both Patterns
Section titled “Supporting Both Patterns”You can support both the traditional Laravel helper and the fluent API:
// Traditionalthrow_if($tokenable === null, MissingTokenableException::forParentToken());
// FluentMissingTokenableException::forParentToken()->throwIf($tokenable === null);Both work identically - choose based on your team’s preference or existing codebase conventions.