prewk/option
Lightweight Option type for PHP providing Some/None to avoid nulls. Adds map/flatMap/filter, unwrap with defaults, and safe chaining inspired by functional programming. Handy for Laravel and general PHP codebases where nullable values cause bugs.
Installation
composer require prewk/option
Ensure your project uses PHP 8.1+ and has prewk/result (≥1.2.0) installed.
First Use Case: Safe Null Handling
Replace a common null check in a Laravel controller:
use Prewk\Option\Option;
public function showUser(Request $request, int $id)
{
$user = Option::fromNullable(User::find($id))
->map(fn($user) => UserResource::make($user))
->getOrElse(abort(404));
return response()->json($user);
}
Where to Look First
Option::some(), Option::none(), map(), getOrElse(), and match().null Checks in ControllersPattern: Use Option to handle missing or failed operations in API responses.
public function updateProfile(Request $request)
{
$profile = Option::fromNullable(Profile::find(auth()->id()))
->map(fn($p) => $p->update($request->validated()))
->match(
fn($success) => ['status' => 'updated'],
fn() => ['error' => 'Profile not found']
);
return response()->json($profile);
}
Pattern: Chain Option with Eloquent queries to avoid nested if statements.
$user = User::where('active', true)
->find($id)
->toOption() // Custom helper (see below)
->map(fn($u) => $u->posts()->where('published', true)->get())
->getOrElse([]);
// Add this helper to your `AppServiceProvider`:
if (!method_exists(Builder::class, 'toOption')) {
Builder::macro('toOption', fn($builder) => Option::fromNullable($builder->first()));
}
OptionPattern: Validate request inputs without isset() or null checks.
$value = Option::fromRequest('optional_field', $request)
->filter(fn($v) => $v !== 'invalid')
->map(fn($v) => (int)$v)
->getOrElse(0);
// Helper macro (add to `AppServiceProvider`):
Option::macro('fromRequest', function ($key, $request = null) {
return Option::fromNullable($request?->input($key));
});
Pattern: Use Option in middleware to handle optional services.
public function handle(Request $request, Closure $next)
{
$cache = Option::fromNullable(app('cache'))
->map(fn($cache) => $cache->get('key'))
->getOrElse(null);
$request->merge(['cache_hit' => $cache !== null]);
return $next($request);
}
prewk/result for ErrorsPattern: Use Option for success cases and Result for failures.
$result = Option::some($user)
->flatMap(fn($u) => UserService::processPayment($u))
->match(
fn($success) => Result::ok($success),
fn() => Result::err('User not found')
);
Option ExtensionsPattern: Extend Option with Laravel-specific methods.
// Add to `AppServiceProvider`:
Option::macro('toResponse', function ($default = null) {
return $this->match(
fn($value) => response()->json($value),
fn() => response()->json($default, 404)
);
});
// Usage:
$user = User::find($id)->toOption()->toResponse();
PHP 8.1+ Requirement
Hard Dependency on prewk/result
prewk/result (≥1.2.0), even if you only use Option.composer require prewk/option prewk/result
Verbosity in Chaining
map()/filter() chains can become hard to read.match() for complex logic:
$result = $option->match(
fn($value) => processValue($value),
fn() => fallbackLogic()
);
Lack of Laravel-Specific Helpers
AppServiceProvider (see Implementation Patterns).Static Analysis Gaps
Option types.@phpstan-ignore-next-line or configure Psalm to treat Option as a custom type:
# psalm.xml
<type name="Prewk\Option\Option" />
Performance Overhead
Option in performance-critical paths (e.g., loops) may add micro-overhead.Option vs. native null checks. For hot paths, consider:
if ($option->isSome()) {
// Native null check for performance
$value = $option->unwrap();
// ...
}
Inspect Option Values
Add a temporary __toString() method for debugging:
Option::macro('debug', function () {
return $this->match(
fn($value) => "Some({$value})",
fn() => "None"
);
});
Usage:
echo $option->debug(); // Output: "Some(123)" or "None"
Common Errors
Cannot call method 'map' on null: You’re calling map() on Option::none().
Fix: Use flatMap() or ensure the Option is Some before chaining.Unwrap panic: Calling unwrap() on Option::none().
Fix: Use unwrapOr() or getOrElse() instead.Integration with Laravel Debugbar
Extend Debugbar to display Option values:
Debugbar::extend('option', function ($option) {
return $option->match(
fn($value) => ['value' => $value],
fn() => ['value' => null, 'type' => 'None']
);
});
Custom Matchers
Extend Option with domain-specific matchers:
Option::macro('matchUser', function ($some, $none) {
return $this->match(
fn($user) => $some($user->name, $user->email),
$none
);
});
Laravel Collection Integration
Add toOption() to Laravel Collections:
Collection::macro('toOption', function () {
return Option::fromNullable($this->first());
});
Usage:
$user = User::where('active', true)->get()->toOption();
Integration with Validation
Use Option in Form Requests:
public function rules()
{
return [
'optional_field' => [
'nullable',
Rule::function('custom_option', function ($attribute, $value, $fail) {
return Option::fromNullable($value)
->filter(fn($v) => $v !== 'invalid')
->isSome()
?: $fail('Invalid value');
}),
],
];
}
**Testing
How can I help you explore Laravel packages today?