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