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
No additional configuration is required—just import the Option class.
First Use Case: Replace Nullable Types
Convert a nullable return type to an explicit Option:
use PhpStandardLibrary\Option;
function findUserByEmail(string $email): Option {
$user = User::query()->where('email', $email)->first();
return Option::fromNullable($user);
}
// Usage in a Laravel controller
$userOption = findUserByEmail('user@example.com');
if ($userOption->isSome()) {
$user = $userOption->unwrap();
return response()->json(['user' => $user]);
}
return response()->json(['error' => 'User not found'], 404);
Where to Look First
Option.php (methods like some(), none(), map(), unwrap()).?Type in service methods, repositories, and controllers.Option-aware assertions (e.g., assertTrue($option->isSome())).Pattern: Return Option from service methods to enforce explicit handling of missing data.
class UserService {
public function getUserById(int $id): Option {
return Option::fromNullable(User::find($id));
}
public function getUserWithAddress(int $id): Option {
return $this->getUserById($id)
->map(fn($user) => $user->address)
->filter(fn($address) => $address !== null);
}
}
Pattern: Wrap Eloquent queries in Option to handle missing records.
class UserRepository {
public function findByEmail(string $email): Option {
return Option::fromNullable(User::where('email', $email)->first());
}
}
Pattern: Use Option to validate optional request data.
use Illuminate\Http\Request;
use PhpStandardLibrary\Option;
public function update(Request $request, int $id) {
$user = User::findOrFail($id);
$newEmail = Option::fromNullable($request->input('email'));
if ($newEmail->isSome()) {
$user->email = $newEmail->unwrap();
}
$user->save();
}
Pattern: Replace nullable properties with Option in DTOs or entities.
class Order {
private Option $shippingAddress;
public function __construct(Option $shippingAddress) {
$this->shippingAddress = $shippingAddress;
}
public function getShippingAddress(): Option {
return $this->shippingAddress;
}
}
Pattern: Use Option to propagate missing data through middleware.
public function handle($request, Closure $next) {
$authUser = Option::fromNullable(auth()->user());
if ($authUser->isNone()) {
return redirect()->route('login');
}
return $next($request);
}
Pattern: Return Option-wrapped data in API responses.
public function show(User $user) {
$address = Option::fromNullable($user->address);
return response()->json([
'user' => $user,
'address' => $address->isSome() ? $address->unwrap() : null,
]);
}
Pattern: Use Option in unit tests to simulate missing data.
public function testUserNotFound() {
$repository = new UserRepository();
$result = $repository->findByEmail('nonexistent@example.com');
$this->assertTrue($result->isNone());
}
Forgetting to Handle None
unwrap() or unwrapOr() on a None value throws an exception.isSome() or use match() (PHP 8.0+):
$value = $option->match(
fn() => 'default', // None case
fn($val) => $val // Some case
);
Overusing Option
Option to trivial cases (e.g., optional query params) adds unnecessary complexity.Option for semantically meaningful absence (e.g., database lookups, API responses).Breaking Existing null Logic
Option and null in the same codebase can lead to confusion.null returns in Option::fromNullable() during transition.Performance in Hot Paths
map/filter on Option can be less performant than direct null checks in tight loops.Option for readability in business logic; optimize critical paths with null checks if needed.IDE Autocompletion
Option methods (e.g., map, flatMap).Check for Unhandled None
Use Option::expect() in development to fail fast:
$user = $userOption->expect('User not found!');
Log Option States
Add a helper method to log Option values:
Option::debug(function($value) {
logger()->debug('Option value:', ['value' => $value]);
});
Static Analysis
Configure PHPStan to enforce Option usage:
# phpstan.neon
parameters:
level: 7
rules:
PhpStandardLibrary\Option\Rules\OptionTypeRule: true
Laravel Service Container
Bind Option to a facade for consistency:
$this->app->bind('option', function() {
return new Option();
});
Eloquent Relationships
Wrap relationships to avoid null leaks:
class User extends Model {
public function address(): Option {
return Option::fromNullable($this->address);
}
}
Blade Templates
Create a helper for Option rendering:
@if($option->isSome())
{{ $option->unwrap() }}
@else
<p>No data available</p>
@endif
Custom Match Logic
Extend Option with domain-specific matchers:
class UserOption extends Option {
public function isActive(): bool {
return $this->match(
fn() => false,
fn($user) => $user->is_active
);
}
}
Integration with Collections
Add Option-aware methods to Laravel Collections:
Collection::macro('toOption', function() {
return Option::fromNullable($this->first());
});
Custom Exceptions
Override Option::expect() to throw domain-specific exceptions:
Option::expect('User not found', new UserNotFoundException());
Serialization
Implement JsonSerializable for Option to support API responses:
class Option implements JsonSerializable {
public function jsonSerialize() {
return $this->match(
fn() => null,
fn($value) => $value
);
}
}
Form Request Validation
Use Option to handle optional fields:
public function rules() {
return [
'email' => 'nullable|email',
'shipping_address' => 'nullable|string',
];
}
public function withValidator($validator) {
$validator->after(function($validator) {
$shippingAddress = Option::fromNullable($this->shipping_address);
if ($shippingAddress->isSome()) {
// Validate address format
}
});
}
Event Handling
Use Option in event listeners to handle missing data:
public function handle(OrderCreated $event) {
$userOption = Option::fromNullable($event->order->user);
if ($userOption->isSome()) {
// Send notification
}
}
How can I help you explore Laravel packages today?