ekiwok / option
PHP Option implementations for all scalars. Possible to register custom Option.
Installs: 1 005
Dependents: 0
Suggesters: 0
Security: 0
Stars: 2
Watchers: 1
Forks: 0
Open Issues: 0
Requires (Dev)
- phpunit/phpunit: ^6.5
README
Option is a value that might or might not be present. In other words it's elegant alternative to throwing an exception or allowing method to return null. It allows for fluent chaining method calls.
Instead of either:
/** * @return string|null */ public function get(string $parameter) /** * @throws ParameterNotFoundException */ public function get(string $parameter): string
Just have:
public function get(string $parameter): OptionString
Allow null approach:
$uuid = $request->get('id'); if ($uuid === null) { return new NotFoundResponse(); } $product = $this->products->findOneById($uuid); if ($product === null) { return new NotFoundResponse(); } return new JsonResponse($product);
Option approach:
return $request->get('id') ->map([$this->products, 'findOneById') ->map([JsonResponse::class, 'create']) ->orElse(new NotFoundResponse());
In contrary to other libraries this one implements separate Option for each scalar type and allows registering custom Options for objects. It's the closest to Java templates we can get and enforces strict type checking.
So if you prefer strict type checking over having a few opcodes less, you can enforce:
return Optional::Some("I love strict types")->orElse(new \stdClass()); // TypeError: Argument 1 passed to class@anonymous::orElse() must be of the type string, object given, called in ...
Optional::Some
wraps each value to correct Option class:
$maybeString = Optional::Some("test"); // OptionString $maybeInt = Optional::Some(43); // OptionInteger $maybeDouble = Optional::Some(0.0); // OptionDobule $maybeBool = Optional::Some(false); // OptionBoolean $maybeArray = Optional::Some([]); // OpionArray $maybeBlogPost = Optional::Some($blogPosts->findOneById($uuid)); // Optional OptionArray::of(null) instanceof None; // true OptionArray::of([]) instanceof Some; // true
Installation
With composer: composer require ekiwok/option
Interface
interface Option<T> { public function equals(Option $another): bool; public function isPresent(): bool; public function map(callable $mapper, string $typeToWrap = null): Option; public function get(): T; public function orElse(T $value): T; public function orElseGet(callable $supplier): T; public function orElseThrow(callable $supplier): T; static public function of(T $value): Option<T> static public function Some($value): Some static public function None(): None }
All scalar options enforce that methods get
, orElse
, orElseGet
, orElseThrow
returns the same scalar.
So it's not possible to orElse
float from OptionString:
return OptionString::of("test") ->orElse(34.5); // Fatal error: Uncaught TypeError: Argument 1 passed to class@anonymous::orElse() must be of the type string, float given
The only exception is Optional
which does not enforce types.
return Optional::of("test") ->orElse(34.5);
Important thing to notice is that when you map Option which is None it will return Optional.
$maybeIsPalindrome = OptionString::of(null) ->map('isPalindrome'); $maybeIsPalindrome instanceof OptionBoolean; // false $maybeIsPalindrome instanceof Optional; // true
This is because there is no reasonable way to guess what should be the type of the value returned by a $supplier. In Some we are able to wrap accordingly to the type of the returned value.
On top of that you probably do not care about mapping when you're dealing with None because all further mappings will also return None.
But if you really want to ensure that, for example, orElseGet
$supplier returns value of correct type you might provide expected type as another map parameter:
$maybeIsPalindrome = OptionString::of(null) ->map('isPalindrome', 'boolean'); $maybeIsPalindrome instanceof OptionBoolean; // true $maybeIsPalindrome instanceof Optional; // false $maybeIsPalindrome->orElseGet(function () { return null; }); // Fatal error: Uncaught TypeError: Return value of class@anonymous::orElseGet() must be of the type boolean
Custom mappings
Simply register custom mappings:
Optional::registerMappings([ Foo::class => OptionFoo::class, Bar::class => OptionBar::clsss, ]); Optional::Some(new Foo()) instanceof OptionFoo; // true Optional::Some(new Bar()) instanceof OptionBar; // true
Any
If you don't want to get specific type like, for example, OptionString you can wrap $value into Any
return $products->findOneById($id) ->map(function (Product $product) { return new Any($product->getPrice()); }) ->orElse(3); // Not throwing exception because after map result is Optional instead of OptionProduct
Autogenerating options
There is experimental bin/generator.php
for autogenerating custom Option classes.
It accepts full class name (including namespace) as first argument and optionally desired namespace as second argument (if not provided default namespace is class namespace prefixed with autoload).
So to generate OptionDateTime one could do:
vendor/ekiwok/opion/bin/generator.php \DateTime > ./autogenerated/OptionDateTime.php