Rust-style Option and Result Classes for PHP
1<?php
2
3namespace Ciarancoza\OptionResult;
4
5use Ciarancoza\OptionResult\Exceptions\UnwrapNoneException;
6
7/**
8 * Option<T> represents an optional value.
9 * An option may be `some` or `none`, where `some` contains a value and `none` does not.
10 *
11 * @template T
12 */
13class Option
14{
15 /**
16 * @nodoc
17 *
18 * @param T $value
19 */
20 private function __construct(
21 private mixed $value,
22 private bool $isSome
23 ) {}
24
25 /**
26 * Returns `None` if the option is `None`, otherwise returns `$optb`
27 *
28 * @template U
29 *
30 * @param Option<U> $optb
31 * @return Option<U>
32 */
33 public function and(self $optb): Option
34 {
35 if ($this->isSome()) {
36 return $optb;
37 }
38
39 return static::None();
40 }
41
42 /**
43 * Returns `None` if the option is `None`, otherwise calls `$f` with the wrapped value and returns the result.
44 *
45 * @template U
46 *
47 * @param callable(T): Option<U> $f
48 * @return Option<U>
49 */
50 public function andThen(callable $f): Option
51 {
52 if ($this->isSome()) {
53 return $f($this->unwrap());
54 }
55
56 return static::None();
57 }
58
59 /**
60 * Returns the contained `Some` value, or throws UnwrapNoneException if the value is `None` with a custom panic message provided by `$msg`.
61 *
62 * @return T
63 *
64 * @throws UnwrapNoneException
65 */
66 public function expect(string $msg): mixed
67 {
68 if ($this->isNone()) {
69 throw new UnwrapNoneException($msg);
70 }
71
72 return $this->unwrap();
73 }
74
75 /**
76 * Returns `None` if the option is `None`, otherwise calls `$predicate` with the wrapped value and returns:
77 * - `Some(T)` if `predicate` returns `true` (where `t` is the wrapped value and
78 * - `None` if `predicate` returns `false`
79 *
80 * @param callable(T): bool $predicate
81 * @return Option<T>
82 */
83 public function filter(callable $predicate): self
84 {
85 if ($this->isSome() && $predicate($this->unwrap())) {
86 return static::Some($this->unwrap());
87 }
88
89 return static::None();
90 }
91
92 /**
93 * Calls a function with a reference to the contained value if `Some`
94 *
95 * @param callable(T): void $f
96 * @return Option<T>
97 */
98 public function inspect(callable $f): self
99 {
100 if ($this->isSome()) {
101 $f($this->unwrap());
102
103 return static::Some($this->unwrap());
104 }
105
106 return static::None();
107 }
108
109 /**
110 * Returns `true` of the option is a `None` value
111 */
112 public function isNone(): bool
113 {
114 return ! $this->isSome();
115 }
116
117 /**
118 * Returns `true` of the option is a `Some` value
119 */
120 public function isSome(): bool
121 {
122 return $this->isSome;
123 }
124
125 /**
126 * Maps an `Option<T>` to `Option<U>` by applying a function to a contained value (if `Some`) or returns `None` (if `None`)
127 *
128 * @template U
129 *
130 * @param callable(T): U $f
131 * @return Option<U>
132 */
133 public function map(callable $f): self
134 {
135 if ($this->isSome()) {
136 return static::Some($f($this->value));
137 }
138
139 return static::None();
140
141 }
142
143 /**
144 * Returns the provided default (if none), or applies a function to the contained value (if any).
145 * If `or` is callable, it will be invoked to get the default value.
146 *
147 * @template U
148 * @template V
149 *
150 * @param V|callable():V $or
151 * @param callable(T): U $f
152 * @return U|V
153 */
154 public function mapOr(mixed $or, callable $f): mixed
155 {
156 if ($this->isSome()) {
157 return $f($this->unwrap());
158 }
159
160 return is_callable($or) ? $or() : $or;
161 }
162
163 /**
164 * Creates a `none` Option
165 *
166 * @return Option<never>
167 */
168 public static function None(): static
169 {
170 return new static(null, false);
171 }
172
173 /**
174 * Returns the option if it contains a value, otherwise returns `optb`.
175 * If `optb` is callable, it will be invoked to get the alternative option.
176 *
177 * @param Option<T>|callable():Option<T> $optb
178 * @return Option<T>
179 */
180 public function or(mixed $optb): self
181 {
182 if ($this->isNone()) {
183 return is_callable($optb) ? $optb() : $optb;
184 }
185
186 return static::Some($this->unwrap());
187 }
188
189 /**
190 * Reduces two options into one, using the provided function if both are `Some`.
191 *
192 * If `$this` is `Some(s)` and `$other` is `Some(o)`, this method returns `Some($f(s,o))`.
193 * Otherwise, if only one of `$this` and `$other` is `Some`, that one is returned.
194 * If both `$this` and `$other` are `None`, `None` is returned.
195 *
196 * @template V
197 * @template U
198 *
199 * @param Option<V> $other
200 * @param callable(T, V): U $f
201 * @return Option<U|T|V>
202 */
203 public function reduce(self $other, callable $f): self
204 {
205 return match (true) {
206 $this->isSome() && $other->isSome() => static::Some($f($this->unwrap(), $other->unwrap())),
207 $this->isSome() => $this,
208 $other->isSome() => $other,
209 default => static::None(),
210 };
211 }
212
213 /**
214 * Creates a `some` Option
215 *
216 * @template T
217 *
218 * @param T $value
219 * @return Option<T>
220 */
221 public static function Some(mixed $value = true): static
222 {
223 if ($value === null) {
224 return static::None();
225 }
226
227 return new static($value, true);
228 }
229
230 /**
231 * Returns the contained `Some` value or throws UnwrapNoneException
232 *
233 * @return T
234 *
235 * @throws UnwrapNoneException
236 */
237 public function unwrap(): mixed
238 {
239 if ($this->isNone()) {
240 throw new UnwrapNoneException;
241 }
242
243 return $this->value;
244 }
245
246 /**
247 * Returns the contained `some` value or a provided default.
248 * If `or` is callable, it will be invoked to get the default value.
249 *
250 * @param V|callable():V $or
251 * @return T|V
252 */
253 public function unwrapOr(mixed $or): mixed
254 {
255 if ($this->isSome()) {
256 return $this->unwrap();
257 }
258
259 return is_callable($or) ? $or() : $or;
260 }
261}