Assertions
Use the ensure() helper for fluent, readable guard clauses that throw exceptions or abort HTTP requests.
Overview
Section titled “Overview”The ensure() helper provides a clean alternative to traditional guard clauses:
// Traditional approachif ($user === null) { throw new UserNotFoundException();}
// With ensure()ensure($user !== null)->orThrow(UserNotFoundException::class);Basic Usage
Section titled “Basic Usage”Throw Exceptions
Section titled “Throw Exceptions”use function Cline\Throw\ensure;
// With exception classensure($user !== null)->orThrow(UserNotFoundException::class);
// With exception class and messageensure($email !== null)->orThrow(ValidationException::class, 'Email is required');
// With exception instanceensure($token->isValid())->orThrow(InvalidTokenException::expired());Abort HTTP Requests
Section titled “Abort HTTP Requests”// Abort with status codeensure($user->isAdmin())->orAbort(403);
// Abort with status code and messageensure($post !== null)->orAbort(404, 'Post not found');Common Patterns
Section titled “Common Patterns”Null Checks
Section titled “Null Checks”// Ensure value is not nullensure($user !== null)->orThrow(UserNotFoundException::class);
// Ensure value existsensure(isset($data['email']))->orThrow(ValidationException::class, 'Email required');Type Validation
Section titled “Type Validation”// Check typesensure(is_array($data))->orThrow(InvalidTypeException::class, 'Expected array');
ensure(is_string($email))->orThrow(InvalidTypeException::class, 'Email must be string');
ensure($user instanceof User)->orThrow(InvalidTypeException::class);Range Validation
Section titled “Range Validation”// Numeric rangesensure($age >= 18)->orThrow(ValidationException::class, 'Must be 18 or older');
ensure($quantity > 0 && $quantity <= 100) ->orThrow(ValidationException::class, 'Quantity must be between 1 and 100');String Validation
Section titled “String Validation”// String lengthensure(strlen($password) >= 8) ->orThrow(ValidationException::class, 'Password must be at least 8 characters');
// String contentensure(str_contains($email, '@')) ->orThrow(ValidationException::class, 'Invalid email format');Permission Checks
Section titled “Permission Checks”// Authorizationensure($user->can('edit', $post)) ->orAbort(403, 'You cannot edit this post');
// Role checksensure($user->hasRole('admin')) ->orAbort(403, 'Admin access required');Real-World Examples
Section titled “Real-World Examples”Controller Usage
Section titled “Controller Usage”class PostController extends Controller{ public function update(Request $request, int $id) { $post = Post::find($id);
// Ensure post exists ensure($post !== null)->orAbort(404, 'Post not found');
// Ensure user can edit ensure($request->user()->can('update', $post)) ->orAbort(403, 'Cannot edit this post');
$post->update($request->validated());
return response()->json($post); }
public function destroy(Request $request, int $id) { $post = Post::find($id);
ensure($post !== null)->orAbort(404); ensure($request->user()->owns($post))->orAbort(403);
$post->delete();
return response()->noContent(); }}Service Layer
Section titled “Service Layer”class PaymentService{ public function processPayment(Order $order, PaymentMethod $method): Payment { // Business rule validation ensure($order->canAcceptPayment()) ->orThrow(OrderException::cannotAcceptPayment());
ensure($order->total->isPositive()) ->orThrow(OrderException::invalidAmount());
ensure($method->isValid()) ->orThrow(PaymentMethodException::invalid());
// Process payment... }}Repository Layer
Section titled “Repository Layer”class UserRepository{ public function findByEmail(string $email): User { ensure(!empty($email)) ->orThrow(ValidationException::class, 'Email cannot be empty');
ensure(filter_var($email, FILTER_VALIDATE_EMAIL)) ->orThrow(ValidationException::class, 'Invalid email format');
$user = User::where('email', $email)->first();
ensure($user !== null) ->orThrow(UserNotFoundException::class);
return $user; }}Domain Models
Section titled “Domain Models”class Order{ public function cancel(): void { ensure($this->canBeCancelled()) ->orThrow(OrderException::cannotCancel());
ensure($this->status !== 'shipped') ->orThrow(OrderException::alreadyShipped());
$this->status = 'cancelled'; $this->save(); }
public function ship(): void { ensure($this->isPaid()) ->orThrow(OrderException::notPaid());
ensure($this->hasAddress()) ->orThrow(OrderException::missingAddress());
$this->status = 'shipped'; $this->shipped_at = now(); $this->save(); }}Middleware
Section titled “Middleware”class RequireApiKey{ public function handle(Request $request, Closure $next) { $apiKey = $request->header('X-API-Key');
// Ensure API key present ensure(!empty($apiKey)) ->orAbort(401, 'API key required');
// Ensure API key valid ensure($this->isValidApiKey($apiKey)) ->orAbort(403, 'Invalid API key');
return $next($request); }}Form Requests
Section titled “Form Requests”class StoreUserRequest extends FormRequest{ public function authorize(): bool { ensure($this->user()->can('create-users')) ->orAbort(403);
return true; }
protected function prepareForValidation(): void { // Ensure required data exists ensure($this->has('email')) ->orThrow(ValidationException::class, 'Email is required'); }}Combining with Exception Features
Section titled “Combining with Exception Features”With Context
Section titled “With Context”ensure($account->balance >= $amount) ->orThrow( InsufficientFundsException::forAmount($amount) ->withContext([ 'account_id' => $account->id, 'balance' => $account->balance, 'required' => $amount, ]) );With Tags
Section titled “With Tags”ensure($subscription->isActive()) ->orThrow( SubscriptionInactiveException::create() ->withTags(['subscription', 'access-control']) );With Wrapping
Section titled “With Wrapping”try { $result = $this->api->call();} catch (ApiException $e) { ensure($result !== null) ->orThrow( ExternalServiceException::failed() ->wrap($e) ->withTags(['external-api', 'critical']) );}Multiple Assertions
Section titled “Multiple Assertions”Chain multiple assertions for comprehensive validation:
public function createUser(array $data): User{ // Validate all inputs ensure(isset($data['email'])) ->orThrow(ValidationException::class, 'Email required');
ensure(isset($data['password'])) ->orThrow(ValidationException::class, 'Password required');
ensure(filter_var($data['email'], FILTER_VALIDATE_EMAIL)) ->orThrow(ValidationException::class, 'Invalid email');
ensure(strlen($data['password']) >= 8) ->orThrow(ValidationException::class, 'Password too short');
return User::create($data);}Comparison with Alternatives
Section titled “Comparison with Alternatives”vs throw_if()
Section titled “vs throw_if()”// Laravel's throw_ifthrow_if($user === null, UserNotFoundException::class);
// ensure() - reads left to rightensure($user !== null)->orThrow(UserNotFoundException::class);vs abort_if()
Section titled “vs abort_if()”// Laravel's abort_ifabort_if(!$user->isAdmin(), 403);
// ensure() - more explicitensure($user->isAdmin())->orAbort(403);vs Traditional Guards
Section titled “vs Traditional Guards”// Traditionalif ($user === null) { throw new UserNotFoundException();}
// ensure() - more conciseensure($user !== null)->orThrow(UserNotFoundException::class);When to Use ensure()
Section titled “When to Use ensure()”Use ensure() when:
- Writing guard clauses
- Validating preconditions
- Checking permissions/authorization
- Asserting business rules
- Validating input data
Don’t use ensure() when:
- The condition is part of normal control flow
- You need complex error handling
- Multiple outcomes are valid
- You’re checking for expected conditions (use if/else)
Best Practices
Section titled “Best Practices”- Keep conditions simple - Complex conditions reduce readability
- Use descriptive messages - Help debugging with clear error messages
- Fail early - Place assertions at the start of methods
- Be explicit - Prefer positive conditions (
!== nullvs=== null) - Combine with exceptions - Use factory methods for rich exception data
Testing
Section titled “Testing”use function Cline\Throw\ensure;
test('throws when condition fails', function () { expect(fn () => ensure(false)->orThrow(RuntimeException::class)) ->toThrow(RuntimeException::class);});
test('aborts when condition fails', function () { expect(fn () => ensure(false)->orAbort(404)) ->toThrow(HttpException::class);});
test('does not throw when condition passes', function () { ensure(true)->orThrow(RuntimeException::class);
expect(true)->toBeTrue();});Next Steps
Section titled “Next Steps”- Learn about Base Exceptions for creating domain-specific exceptions
- Explore Error Context for adding debugging information
- See Error Wrapping for exception chains