joelbutcher / php-optional
An opinionated implementation of Java's Optional class for PHP.
Requires
- php: ^8.0.2
- azjezz/psl: ^1.9|^2.1
- vimeo/psalm: ^4.30
Requires (Dev)
- laravel/pint: ^1.2
- pestphp/pest: ^1.22
- phpstan/phpstan: ^1.9
README
Introduction
Inspired by Java's Optional class, this package aims to provide a comprehensive API for optional field values.
Examples
You can use the Optional
class in a variety of ways, see below for a few examples:
Updating a user's profile
The below class indicates how this package may be used to update a users profile information. All fields are optional, because our user may not want to update all fields.
<?php namespace App\DataObjects; use JoelButcher\PhpOptional\Optional; use Psl\Type; use Webmozart\Assert\Assert; class UpdateProfile { public function __construct( private readonly int $id private readonly Optional $name private readonly Optional $email private readonly Options $bio ) { // } public static function fromFormRequest(int $id UpdateProfileRequest $request): self { return new self( id: $id, name: Optional::forNullable($request->get('name')), email: Optional::forNullable($request->get('email')), bio: Optional::forNullable($request->get('bio')), ); } public static function fromArray(array $data): self { Assert::keyExists($post, 'id'); Assert::positiveInteger($post['id']); return new self( id: $data['id'], name: Optional::forOptionalArrayKey($data, 'name', Type\non_empty_string()), email: Optional::forOptionalArrayKey($data, 'email', Type\non_empty_string()), bio: Optional::forOptionalArrayKey($data, 'bio', Type\non_empty_string()), ); } public function handle(UserRepository $users): void { $user = $users->findOneById($this->id); // These callbacks are only called if the value for each optional field is present. $this->name->apply(fn (string $name) => $user->updateName($name)); $this->email->apply(fn (string $email) => $user->updateEmail($email)); $this->bio->apply(fn (string $bio) => $user->updateBio($bio)); $users->save($user); } }
Noticed the \Psl\Type\non_empty_string()
call? That's an abstraction coming from azjezz/psl
, which allows for having a type declared both at runtime and at static analysis level. We use it to parse inputs into valid values, or to produce crashes, if something is malformed.
The azjezz/psl
, Psl\Type
tooling gives us both type safety and runtime validation by implicitly validating our values:
OptionalField::forPossiblyMissingArrayKey( ['foo' => 'bar'], 'foo', \Psl\Type\positive_int() ); // crashes: `foo` does not contain a `positive-int`!