1: | <?php declare(strict_types=1); |
2: | |
3: | namespace TH\Maybe\Option; |
4: | |
5: | use TH\DocTest\Attributes\ExamplesSetup; |
6: | use TH\Maybe\Option; |
7: | use TH\Maybe\Result; |
8: | use TH\Maybe\Tests\Helpers\IgnoreUnusedResults; |
9: | use function TH\Maybe\Internal\isOfAnyClass; |
10: | |
11: | /** |
12: | * Return a `Option\Some` option containing `$value`. |
13: | * |
14: | * @template T |
15: | * @param T $value |
16: | * @return Option\Some<T> |
17: | */ |
18: | function some(mixed $value): Option\Some |
19: | { |
20: | return new Option\Some($value); |
21: | } |
22: | |
23: | /** |
24: | * Return a `Option\None` option containing no values. |
25: | */ |
26: | function none(): Option\None |
27: | { |
28: | return Option\None::instance; |
29: | } |
30: | |
31: | /** |
32: | * Transform a value into an `Option`. |
33: | * It will be a `Some` option containing `$value` if `$value` is different from `$noneValue` (default `null`) |
34: | * |
35: | * # Examples |
36: | * |
37: | * ``` |
38: | * self::assertEq(Option\fromValue("fruits"), Option\some("fruits")); |
39: | * self::assertEq(Option\fromValue(null), Option\none()); |
40: | * ``` |
41: | * |
42: | * @template U |
43: | * @param U $value |
44: | * @return Option<U> |
45: | */ |
46: | function fromValue(mixed $value, mixed $noneValue = null, bool $strict = true): Option |
47: | { |
48: | $same = $strict |
49: | ? ($value === $noneValue) |
50: | // @phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators |
51: | : ($value == $noneValue); // @phpstan-ignore equal.notAllowed |
52: | |
53: | /** @var Option<U> */ |
54: | return $same |
55: | ? Option\none() |
56: | : Option\some($value); |
57: | } |
58: | |
59: | /** |
60: | * Execute a callable and transform the result into an `Option`. |
61: | * It will be a `Some` option containing the result if it is different from `$noneValue` (default `null`). |
62: | * |
63: | * # Examples |
64: | * |
65: | * Successful execution: |
66: | * |
67: | * ``` |
68: | * self::assertEq(Option\of(fn() => "fruits"), Option\some("fruits")); |
69: | * ``` |
70: | * |
71: | * Convertion of `null` to `Option\None`: |
72: | * |
73: | * ``` |
74: | * self::assertEq(Option\of(fn() => null), Option\none()); |
75: | * ``` |
76: | * |
77: | * @template U |
78: | * @param callable():U $callback |
79: | * @return Option<U> |
80: | */ |
81: | function of(callable $callback, mixed $noneValue = null, bool $strict = true): Option |
82: | { |
83: | return Option\fromValue($callback(), $noneValue, $strict); |
84: | } |
85: | |
86: | /** |
87: | * Execute a callable and transform the result into an `Option` as `Option\of()` does |
88: | * but also return `Option\None` if it an exception matching $exceptionClass was thrown. |
89: | * |
90: | * # Examples |
91: | * |
92: | * Successful execution: |
93: | * |
94: | * ``` |
95: | * self::assertEq(Option\tryOf(fn () => strtolower("FRUITS")), Option\some("fruits")); |
96: | * ``` |
97: | * |
98: | * Convertion of `null` to `Option\None`: |
99: | * |
100: | * ``` |
101: | * self::assertEq(Option\tryOf(fn() => null), Option\none()); |
102: | * ``` |
103: | * |
104: | * Checked Exception: |
105: | * |
106: | * ``` |
107: | * self::assertEq(Option\tryOf(fn () => new \DateTimeImmutable("nope")), Option\none()); |
108: | * ``` |
109: | * |
110: | * Unchecked Exception: |
111: | * |
112: | * ``` |
113: | * self::assertEq(Option\tryOf(fn () => 1 / 0), Option\none()); |
114: | * // @throws DivisionByZeroError Division by zero |
115: | * ``` |
116: | * |
117: | * @template U |
118: | * @template E of \Throwable |
119: | * @param callable():U $callback |
120: | * @param list<class-string<E>>|class-string<E> $exceptionClass |
121: | * @return Option<U> |
122: | * @throws \Throwable |
123: | */ |
124: | function tryOf( |
125: | callable $callback, |
126: | mixed $noneValue = null, |
127: | bool $strict = true, |
128: | array|string $exceptionClass = \Exception::class, |
129: | ): Option { |
130: | try { |
131: | return Option\of($callback, $noneValue, $strict); |
132: | } catch (\Throwable $th) { |
133: | if (isOfAnyClass($th, (array) $exceptionClass)) { |
134: | return Option\none(); |
135: | } |
136: | |
137: | throw $th; |
138: | } |
139: | } |
140: | |
141: | /** |
142: | * Converts from `Option<Option<T>>` to `Option<T>`. |
143: | * |
144: | * # Examples |
145: | * |
146: | * ``` |
147: | * $x = Option\Some("vegetables"); |
148: | * self::assertSame(Option\flatten(Option\some($x)), $x); |
149: | * self::assertSame(Option\flatten(Option\some(Option\none())), Option\none()); |
150: | * self::assertSame(Option\flatten(Option\none()), Option\none()); |
151: | * ``` |
152: | * |
153: | * @template U |
154: | * @param Option<Option<U>> $option |
155: | * @return Option<U> |
156: | */ |
157: | function flatten(Option $option): Option |
158: | { |
159: | return $option instanceof Option\None |
160: | ? $option |
161: | /** @phpstan-ignore missingType.checkedException */ |
162: | : $option->unwrap(); |
163: | } |
164: | |
165: | /** |
166: | * Unzips an option containing a tuple of two options. |
167: | * |
168: | * If `self` is `Some([a, b])` this method returns `[Some(a), Some(b)]`. Otherwise, `[None, None]` is returned. |
169: | * |
170: | * ``` |
171: | * $x = Option\Some("vegetables"); |
172: | * self::assertEq(Option\unzip(Option\some(["a", 2])), [Option\some("a"), Option\some(2)]); |
173: | * self::assertSame(Option\unzip(Option\none()), [Option\none(), Option\none()]); |
174: | * ``` |
175: | * |
176: | * @template U |
177: | * @template V |
178: | * @param Option<array{U, V}> $option |
179: | * @return array{Option<U>, Option<V>} |
180: | */ |
181: | function unzip(Option $option): array |
182: | { |
183: | /** @var array{Option<U>, Option<V>} */ |
184: | return $option->mapOrElse( |
185: | static fn (array $a): array => [Option\some($a[0]), Option\some($a[1])], |
186: | static fn (): array => [Option\none(), Option\none()], |
187: | ); |
188: | } |
189: | |
190: | /** |
191: | * Transposes an `Option` of a `Result` into a `Result` of an `Option`. |
192: | * |
193: | * `None` will be mapped to `Ok(None)`. |
194: | * `Some(Ok(_))` and `Some(Err(_))` will be mapped to `Ok(Some(_))` and `Err(_)`. |
195: | * |
196: | * ``` |
197: | * use TH\Maybe\Result; |
198: | * |
199: | * self::assertEq(Result\ok(Option\some(4)), Option\transpose(Option\some(Result\ok(4)))); |
200: | * self::assertEq(Result\err("meat"), Option\transpose(Option\some(Result\err("meat")))); |
201: | * self::assertEq(Option\transpose(Option\none()), Result\ok(Option\none())); |
202: | * ``` |
203: | * |
204: | * @template U |
205: | * @template E |
206: | * @param Option<Result<U, E>> $option |
207: | * @return Result<Option<U>, E> |
208: | */ |
209: | #[ExamplesSetup(IgnoreUnusedResults::class)] |
210: | function transpose(Option $option): Result |
211: | { |
212: | /** @var Result<Option<U>, E> */ |
213: | return $option->mapOrElse( |
214: | static fn (Result $result) => $result->map(Option\some(...)), |
215: | static fn () => Result\ok(Option\none()), |
216: | ); |
217: | } |
218: |