Skip to content

Instantly share code, notes, and snippets.

@bfg
Created April 25, 2024 17:34
Show Gist options
  • Save bfg/ca0f5318a4f7665d543f388c70867a69 to your computer and use it in GitHub Desktop.
Save bfg/ca0f5318a4f7665d543f388c70867a69 to your computer and use it in GitHub Desktop.
PHP implementation of Optional monad
<?php
use ArrayIterator;
use Iterator;
use IteratorAggregate;
use RuntimeException;
use Throwable;
/**
* Optional value container that can hold a single result and can be used in foreach loop
* @template T
*/
class Optional implements IteratorAggregate {
private $item;
/**
* Creates new instance
* @param $item mixed item to store
*/
private function __construct($item) {
$this->item = $item;
}
/**
* Returns empty instance
* @return Optional<T> empty value container
*/
public static function empty(): Optional {
return new Optional(null);
}
/**
* Creates new instance.
* @param T $item value item
* @return Optional<T> value container
*/
public static function of($item): Optional {
return new Optional($item);
}
/**
* Tells if value is present
* @return bool true/false
*/
public function isPresent(): bool {
return !$this->isEmpty();
}
/**
* Tells if value is absent
* @return bool true/false
*/
public function isEmpty(): bool {
return is_null($this->item);
}
/**
* Returns stored value if it exists, otherwise null
* @return T stored value if exists, otherwise null
*/
public function orNull() {
return $this->item;
}
/**
* Returns stored value if it exists, otherwise null
* @return T stored value if exists, otherwise null
* @throws RuntimeException if value is not present
*/
public function get() {
return $this->orElseThrow();
}
/**
* Returns value if it is present, otherwise returns given default value
* @param $defaultValue mixed default value to return if value is not present
* @return T stored value or default value
*/
public function orElse($defaultValue) {
return $this->isPresent() ? $this->orNull() : $defaultValue;
}
/**
* Returns value if it is present, otherwise returns value supplied by supplier
* @param callable $supplier value supplier in case value is absent
* @return T stored value or value supplied by supplier
*/
public function orElseGet(callable $supplier) {
return $this->isPresent() ? $this->orNull() : $supplier();
}
/**
* Returns value if it is present, otherwise throws an exception
* @param mixed $exSupplier optional exception supplier or exception message string
* @return T stored value
* @throws RuntimeException or supplier's created exception if value is not present
*/
public function orElseThrow($exSupplier = null) {
if ($this->isEmpty()) {
if ($exSupplier === null) {
throw new RuntimeException("No value is present.");
} elseif (is_string($exSupplier)) {
throw new RuntimeException($exSupplier);
} elseif (is_callable($exSupplier)) {
$ex = $exSupplier();
if ($ex instanceof \Throwable) {
throw $ex;
} else {
throw new RuntimeException("$ex");
}
} else {
throw new RuntimeException("No value is present and exception supplier was given.");
}
}
return $this->orNull();
}
/**
* If a value is present, returns an Optional describing the value,
* otherwise returns an Optional produced by the supplying function.
* @param callable $supplier value supplier that is invoked if value is absent
* @return Optional|$this reference to itself if value is present, otherwise new Optional with value
* supplied by supplier
*/
public function or(callable $supplier): Optional {
if ($this->isPresent()) {
return $this;
}
// fetch value from supplier
$newValue = $supplier();
// we tolerate both optional or raw return types
if ($newValue instanceof Optional) {
return $newValue;
} else {
return Optional::of($newValue);
}
}
/**
* If a value is present, and the value matches the given predicate, return an Optional
* describing the value, otherwise return an empty Optional.
* @param callable $predicate predicate to apply
* @return Optional<T>|$this filtered optional
*/
public function filter(callable $predicate): Optional {
if ($this->isEmpty()) {
return $this;
}
$satisfies = $predicate($this->get());
return $satisfies ? $this : Optional::empty();
}
/**
* If a value is present, apply the provided mapping function to it,
* and if the result is non-null, return an Optional describing the result
* @param callable $mapper mapper that transforms the value into new value
* @return Optional<T>|$this mapped optional
*/
public function map(callable $mapper): Optional {
if ($this->isEmpty()) {
return $this;
}
$newValue = $mapper($this->get());
return is_null($newValue) ? Optional::empty() : Optional::of($newValue);
}
/**
* If a value is present, apply the provided Optional-bearing mapping function to it,
* return that result, otherwise return an empty Optional.
* @param callable $mapper
* @return Optional|$this mapped optional
*/
public function flatMap(callable $mapper): Optional {
if ($this->isEmpty()) {
return $this;
}
$newOptional = $mapper($this->get());
if (is_null($newOptional)) {
throw new RuntimeException("flatMapper returned null.");
} elseif (!($newOptional instanceof Optional)) {
throw new RuntimeException("flatMapper didn't return Optional value.");
}
return $newOptional;
}
/**
* Invokes given consumer if value is present
* @param callable $consumer consumer to invoke with contained value
* @return Optional|$this reference to itself
*/
public function peek(callable $consumer): Optional {
if ($this->isPresent()) {
$consumer($this->get());
}
return $this;
}
/**
* Invokes given action if value is absent
* @param callable $action action to invoke if value is absent
* @return Optional|$this reference to itself
*/
public function ifEmpty(callable $action): Optional {
if ($this->isEmpty()) {
$action();
}
return $this;
}
/**
* Returns iterator over potential value so that this instance can be used in foreach loop
* @return Iterator iterator over value
*/
public function getIterator(): Iterator {
$arr = ($this->isPresent()) ? [$this->get()] : [];
return new ArrayIterator($arr);
}
public function __toString(): string {
$present = "n";
$desc = "";
if ($this->isPresent()) {
$present = "y";
// try to stringify the item, but this might throw exception if __toString() is not implemented
try {
$desc = " item=" . ($this->item . "");
} catch (Throwable $e) {
}
}
return "Optional[present=${present}${desc}]";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment