Skip to content

Commit 4b82f18

Browse files
committed
Revert "Revert "Feature: add Option::ify() & Option::tryIfy()""
This reverts commit 95dc3bf.
1 parent 95dc3bf commit 4b82f18

File tree

2 files changed

+154
-2
lines changed

2 files changed

+154
-2
lines changed

src/functions/option.php

+82-2
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,7 @@ function of(callable $callback, mixed $noneValue = null, bool $strict = true): O
114114
* ```
115115
*
116116
* @template U
117-
* @template E of \Throwable
118117
* @param callable():U $callback
119-
* @param class-string<E> $exceptionClass
120118
* @return Option<U>
121119
* @throws \Throwable
122120
*/
@@ -137,6 +135,88 @@ function tryOf(
137135
}
138136
}
139137

138+
/**
139+
* Wrap a callable into one that transforms its result into an `Option`.
140+
* It will be a `Some` option containing the result if it is different from `$noneValue` (default `null`).
141+
*
142+
* # Examples
143+
*
144+
* Successful execution:
145+
*
146+
* ```
147+
* self::assertEq(Option\ify(strtolower(...))("FRUITS"), Option\some("fruits"));
148+
* ```
149+
*
150+
* Convertion of `null` to `Option\None`:
151+
*
152+
* ```
153+
* self::assertEq(Option\ify(fn() => null)(), Option\none());
154+
* ```
155+
*
156+
* @template U
157+
* @param callable():U $callback
158+
* @return \Closure(mixed...):Option<U>
159+
*/
160+
function ify(callable $callback, mixed $noneValue = null, bool $strict = true): \Closure
161+
{
162+
return static fn (...$args) => Option\fromValue($callback(...$args), $noneValue, $strict);
163+
}
164+
165+
/**
166+
* Wrap a callable into one that transforms its result into an `Option` like `Option\ify()` does
167+
* but also return `Option\None` if it an exception matching $exceptionClass was thrown.
168+
*
169+
* # Examples
170+
*
171+
* Successful execution:
172+
*
173+
* ```
174+
* self::assertEq(Option\tryIfy(strtolower(...))("FRUITS"), Option\some("fruits"));
175+
* ```
176+
*
177+
* Convertion of `null` to `Option\None`:
178+
*
179+
* ```
180+
* self::assertEq(Option\tryIfy(fn() => null)(), Option\none());
181+
* ```
182+
*
183+
* Checked Exception:
184+
*
185+
* ```
186+
* self::assertEq(Option\tryIfy(fn () => new \DateTimeImmutable("nope"))(), Option\none());
187+
* ```
188+
*
189+
* Unchecked Exception:
190+
*
191+
* ```
192+
* self::assertEq(Option\tryIfy(fn () => 1 / 0)(), Option\none());
193+
* // @throws DivisionByZeroError Division by zero
194+
* ```
195+
*
196+
* @template U
197+
* @param callable():U $callback
198+
* @return \Closure(mixed...):Option<U>
199+
*/
200+
function tryIfy(
201+
callable $callback,
202+
mixed $noneValue = null,
203+
bool $strict = true,
204+
string $exceptionClass = \Exception::class,
205+
): \Closure
206+
{
207+
return static function (...$args) use ($callback, $noneValue, $strict, $exceptionClass): mixed {
208+
try {
209+
return Option\fromValue($callback(...$args), $noneValue, $strict);
210+
} catch (\Throwable $th) {
211+
if (\is_a($th, $exceptionClass)) {
212+
return Option\none();
213+
}
214+
215+
throw $th;
216+
}
217+
};
218+
}
219+
140220
/**
141221
* Converts from `Option<Option<T>>` to `Option<T>`.
142222
*

tests/Unit/Option/IfyTest.php

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace TH\Maybe\Tests\Unit\Option;
4+
5+
use PHPUnit\Framework\Assert;
6+
use PHPUnit\Framework\TestCase;
7+
use TH\Maybe\Option;
8+
use TH\Maybe\Tests\Provider;
9+
10+
final class IfyTest extends TestCase
11+
{
12+
use Provider\Options;
13+
14+
/**
15+
* @dataProvider fromValueMatrix
16+
* @param Option<mixed> $expected
17+
*/
18+
public function testIfy(Option $expected, mixed $value, mixed $noneValue, bool $strict = true): void
19+
{
20+
Assert::assertEquals($expected, Option\ify(static fn () => $value, $noneValue, strict: $strict)());
21+
}
22+
23+
/**
24+
* @dataProvider fromValueMatrix
25+
* @param Option<mixed> $expected
26+
*/
27+
public function testTryOf(Option $expected, mixed $value, mixed $noneValue, bool $strict = true): void
28+
{
29+
Assert::assertEquals($expected, Option\tryIfy(static fn () => $value, $noneValue, strict: $strict)());
30+
}
31+
32+
public function testOfDefaultToNull(): void
33+
{
34+
Assert::assertEquals(Option\none(), Option\ify(static fn () => null)());
35+
Assert::assertEquals(Option\some(1), Option\ify(static fn () => 1)());
36+
}
37+
38+
public function testTryOfDefaultToNull(): void
39+
{
40+
Assert::assertEquals(Option\none(), Option\tryIfy(static fn () => null)());
41+
Assert::assertEquals(Option\some(1), Option\tryIfy(static fn () => 1)());
42+
}
43+
44+
public function testOfDefaultToStrict(): void
45+
{
46+
$o = (object)[];
47+
48+
Assert::assertEquals(Option\none(), Option\ify(static fn () => $o, (object)[], strict: false)());
49+
Assert::assertEquals($o, Option\ify(static fn () => $o, (object)[])()->unwrap());
50+
}
51+
52+
public function testTryOfDefaultToStrict(): void
53+
{
54+
$o = (object)[];
55+
56+
Assert::assertEquals(Option\none(), Option\tryIfy(static fn () => $o, (object)[], strict: false)());
57+
Assert::assertEquals($o, Option\tryIfy(static fn () => $o, (object)[])()->unwrap());
58+
}
59+
60+
public function testTryOfExeptions(): void
61+
{
62+
// @phpstan-ignore-next-line
63+
Assert::assertEquals(Option\none(), Option\tryIfy(static fn () => new \DateTimeImmutable("nope"))());
64+
65+
try {
66+
// @phpstan-ignore-next-line
67+
Option\tryIfy(static fn () => 1 / 0)();
68+
Assert::fail("An exception should have been thrown");
69+
} catch (\DivisionByZeroError) {
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)