php-standard-library/option
Option type for PHP with Some/None to replace nullable values with explicit presence semantics. Helps avoid null checks, clarifies intent, and models optional data safely. Part of PHP Standard Library; see docs and contributing links.
Installation
composer require php-standard-library/option
Add to composer.json if needed:
"require": {
"php-standard-library/option": "^6.2"
}
First Use Case
Replace a nullable return type with Option in a Laravel service:
use PhpStandardLibrary\Option;
class UserService {
public function findByEmail(string $email): Option {
$user = User::where('email', $email)->first();
return Option::fromNullable($user);
}
}
Where to Look First
vendor/php-standard-library/option/src/Option.phpOption::fromNullable() and Option::some()/Option::none() methods.Replacing Nullable Types
Convert ?Type to Option<Type> in method signatures:
// Before
public function getUser(): ?User { ... }
// After
public function getUser(): Option { ... }
Functional Chaining
Use map, flatMap, and filter for immutable transformations:
$userOption = $this->userService->findByEmail('test@example.com');
$emailOption = $userOption->map(fn(User $user) => $user->email);
Laravel-Specific Patterns
$request->validate(['email' => 'required']);
$user = $this->userService->findByEmail($request->email);
$user->map(fn(User $user) => $this->sendWelcomeEmail($user));
$userOption = $this->userService->findById(1);
$addressOption = $userOption->flatMap(fn(User $user) => Option::fromNullable($user->address));
Error Handling
Use unwrapOr, unwrapOrElse, or expect for fallback logic:
$user = $userOption->unwrapOr(User::guest());
$user = $userOption->unwrapOrElse(fn() => User::create(['email' => 'default@example.com']));
Configuration Management
Wrap Laravel config values in Option:
$dbHost = Option::fromNullable(config('database.connections.mysql.host'));
$dbHost->map(fn($host) => Log::info("DB Host: $host"));
Domain-Driven Design (DDD)
Use Option for entities with optional attributes:
class Order {
private Option $shippingAddress;
public function __construct(Option $shippingAddress) {
$this->shippingAddress = $shippingAddress;
}
public function getShippingAddress(): Option {
return $this->shippingAddress;
}
}
API Layer Explicitly handle missing data in responses:
$userOption = $this->userService->findById($request->id);
return response()->json($userOption->map(fn(User $user) => $user->toArray()));
Legacy Code Migration
Gradually replace null checks with Option:
// Legacy
if ($user && $user->address) { ... }
// Refactored
$userOption = Option::fromNullable($user);
$addressOption = $userOption->flatMap(fn(User $user) => Option::fromNullable($user->address));
$addressOption->map(...);
Service Container
Bind Option to resolve dependencies:
$this->app->bind(Option::class, function ($app) {
return Option::none(); // Default fallback
});
Form Requests
Use Option in custom validation rules:
public function rules(): array {
return [
'email' => ['required', new OptionalEmailRule()],
];
}
class OptionalEmailRule implements Rule {
public function passes($attribute, $value) {
return Option::fromNullable($value)
->filter(fn($email) => filter_var($email, FILTER_VALIDATE_EMAIL))
->isSome();
}
}
Blade Templates
Create helpers for Option rendering:
@php
function option($option, $callback)
{
if ($option->isSome()) {
echo $callback($option->unwrap());
}
}
@endphp
<!-- Usage -->
@option($userOption, function ($user) { ... })
Testing
Assert Option states in PHPUnit:
$this->assertTrue($userOption->isSome());
$this->assertEquals('test@example.com', $userOption->unwrap()->email);
$this->assertTrue($emptyOption->isNone());
Forgetting to Handle None
Always check isSome() or isNone() before unwrapping:
// Anti-pattern: May throw if $option is None
$user = $option->unwrap(); // Dangerous!
// Correct
if ($option->isSome()) {
$user = $option->unwrap();
}
Overusing Option
Avoid Option for trivial cases where null is sufficient:
// Overkill
function getSimpleValue(): Option { return Option::some($value); }
// Better
function getSimpleValue() { return $value; }
Performance Overhead
Option adds method calls; avoid deep nesting in performance-critical paths:
// Inefficient
$result = $option1
->flatMap(...)
->flatMap(...)
->flatMap(...);
// Better: Break into steps
$step1 = $option1->flatMap(...);
$step2 = $step1->flatMap(...);
Laravel Magic Methods
Option may conflict with Laravel’s magic methods (e.g., User::address vs. Option::map):
// Problematic
$user->address->map(...); // Calls magic __get(), not Option::map
// Solution
$addressOption = Option::fromNullable($user->address);
$addressOption->map(...);
Static Analysis Conflicts
PHPStan/Psalm may flag Option as unused if not configured:
# phpstan.neon
parameters:
level: 5
checkMissingIterableMethod: false
checkMissingProperties: false
Inspect Option State
Use ->isSome()/->isNone() or ->unwrap() in dd():
dd($option->isSome() ? 'Some' : 'None');
Common Errors
Cannot call method on None: Use ->map() instead of direct property access.unwrap() on None: Always check isSome() first.Option generic type matches the wrapped value.Logging
Log Option states for debugging:
Log::debug('User Option:', ['state' => $userOption->isSome() ? 'Some' : 'None']);
Autoloading
Ensure composer dump-autoload is run after installation:
composer dump-autoload
PHP Version
Requires PHP 8.0+ for full match expression support:
return match ($option->isSome()) {
true => $option->unwrap(),
false => null,
};
IDE Support
Configure your IDE to recognize Option methods (e.g., in PHPStorm):
vendor/php-standard-library/option/src to "Sources" in PHPStorm.@method annotations for custom classes extending Option.Custom Option Classes
Extend Option for domain-specific behavior:
class UserOption extends Option {
public function getEmail(): Option {
return $this->map(fn(User $user) => $user->email);
}
}
Integration with Laravel Collections
Add Option-aware methods to Laravel’s Collection:
Collection::macro('toOption', function () {
return Option::some($this->all());
});
Custom Match Expressions
How can I help you explore Laravel packages today?