Skip to content

Commit b44bd58

Browse files
committed
Revert "Revert "feature: Add Result\ify()""
This reverts commit b2c07d2.
1 parent 8a7d3c3 commit b44bd58

File tree

5 files changed

+162
-16
lines changed

5 files changed

+162
-16
lines changed

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"psr-4": {
88
"TH\\Maybe\\": "src/"
99
},
10-
"files": ["src/functions/option.php", "src/functions/result.php"]
10+
"files": ["src/functions/option.php", "src/functions/result.php", "src/functions/internal.php"]
1111
},
1212
"autoload-dev": {
1313
"psr-4": {

phpcs.xml.dist

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<exclude name="SlevomatCodingStandard.Functions.DisallowNamedArguments" />
3535
<exclude name="SlevomatCodingStandard.Functions.DisallowTrailingCommaInCall" />
3636
<exclude name="SlevomatCodingStandard.Functions.DisallowTrailingCommaInDeclaration" />
37+
<exclude name="SlevomatCodingStandard.Functions.DisallowTrailingCommaInClosureUse" />
3738
<exclude name="SlevomatCodingStandard.Functions.UnusedParameter" />
3839
<exclude name="SlevomatCodingStandard.Namespaces.FullyQualifiedClassNameInAnnotation" />
3940
<exclude name="SlevomatCodingStandard.Namespaces.FullyQualifiedExceptions" />

src/functions/option.php

+17-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace TH\Maybe\Option;
44

55
use TH\DocTest\Attributes\ExamplesSetup;
6+
use TH\Maybe\Internal;
67
use TH\Maybe\Option;
78
use TH\Maybe\Result;
89
use TH\Maybe\Tests\Helpers\IgnoreUnusedResults;
@@ -114,7 +115,9 @@ function of(callable $callback, mixed $noneValue = null, bool $strict = true): O
114115
* ```
115116
*
116117
* @template U
118+
* @template E of \Throwable
117119
* @param callable():U $callback
120+
* @param class-string<E> $exceptionClass
118121
* @return Option<U>
119122
* @throws \Throwable
120123
*/
@@ -154,7 +157,7 @@ function tryOf(
154157
* ```
155158
*
156159
* @template U
157-
* @param callable():U $callback
160+
* @param callable(mixed...):U $callback
158161
* @return \Closure(mixed...):Option<U>
159162
*/
160163
function ify(callable $callback, mixed $noneValue = null, bool $strict = true): \Closure
@@ -194,25 +197,31 @@ function ify(callable $callback, mixed $noneValue = null, bool $strict = true):
194197
* ```
195198
*
196199
* @template U
197-
* @param callable():U $callback
200+
* @template E of \Throwable
201+
* @param callable(mixed...):U $callback
202+
* @param class-string<E> $exceptionClass
203+
* @param class-string<E> $additionalExceptionClasses
198204
* @return \Closure(mixed...):Option<U>
199205
*/
200206
function tryIfy(
201207
callable $callback,
202208
mixed $noneValue = null,
203209
bool $strict = true,
204210
string $exceptionClass = \Exception::class,
211+
string ...$additionalExceptionClasses,
205212
): \Closure
206213
{
207-
return static function (...$args) use ($callback, $noneValue, $strict, $exceptionClass): mixed {
214+
return static function (...$args) use (
215+
$callback,
216+
$noneValue,
217+
$strict,
218+
$exceptionClass,
219+
$additionalExceptionClasses,
220+
): mixed {
208221
try {
209222
return Option\fromValue($callback(...$args), $noneValue, $strict);
210223
} catch (\Throwable $th) {
211-
if (\is_a($th, $exceptionClass)) {
212-
return Option\none();
213-
}
214-
215-
throw $th;
224+
return Internal\trap($th, Option\none(...), $exceptionClass, ...$additionalExceptionClasses);
216225
}
217226
};
218227
}

src/functions/result.php

+77-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace TH\Maybe\Result;
44

55
use TH\DocTest\Attributes\ExamplesSetup;
6+
use TH\Maybe\Internal;
67
use TH\Maybe\Option;
78
use TH\Maybe\Result;
89
use TH\Maybe\Tests\Helpers\IgnoreUnusedResults;
@@ -79,24 +80,93 @@ function err(mixed $value): Result\Err
7980
* @template E of \Throwable
8081
* @param callable(mixed...):U $callback
8182
* @param class-string<E> $exceptionClass
83+
* @param class-string<E> $additionalExceptionClasses
8284
* @return Result<U,E>
8385
* @throws \Throwable
8486
*/
8587
#[ExamplesSetup(IgnoreUnusedResults::class)]
86-
function trap(callable $callback, string $exceptionClass = \Exception::class): Result
87-
{
88+
function trap(
89+
callable $callback,
90+
string $exceptionClass = \Exception::class,
91+
string ...$additionalExceptionClasses,
92+
): Result {
8893
try {
8994
/** @var Result<U,E> */
9095
return Result\ok($callback());
9196
} catch (\Throwable $th) {
92-
if (\is_a($th, $exceptionClass)) {
93-
return Result\err($th);
94-
}
95-
96-
throw $th;
97+
/** @var Result\Err<E> */
98+
return Internal\trap($th, Result\err(...), $exceptionClass, ...$additionalExceptionClasses);
9799
}
98100
}
99101

102+
/**
103+
* Wrap a callable into one that transforms its returned value or thrown exception
104+
* into a `Result` like `Result\trap()` does, but without executing it.
105+
*
106+
* # Examples
107+
*
108+
* Successful execution:
109+
*
110+
* ```
111+
* self::assertEq(Result\ok(3), Result\ify(fn () => 3)());
112+
* ```
113+
*
114+
* Checked exception:
115+
*
116+
* ```
117+
* $x = Result\ify(fn () => new \DateTimeImmutable("2020-30-30 UTC"))();
118+
* self::assertTrue($x->isErr());
119+
* $x->unwrap();
120+
* // @throws Exception Failed to parse time string (2020-30-30 UTC) at position 6 (0): Unexpected character
121+
* ```
122+
*
123+
* Unchecked exception:
124+
*
125+
* ```
126+
* Result\ify(fn () => 1/0)();
127+
* // @throws DivisionByZeroError Division by zero
128+
* ```
129+
*
130+
* Result-ify `strtotime()`:
131+
*
132+
* ```
133+
* $strtotime = Result\ify(
134+
* static fn (...$args)
135+
* => \strtotime(...$args)
136+
* ?: throw new \RuntimeException("Could not convert string to time"),
137+
* );
138+
*
139+
* self::assertEq($strtotime("2015-09-21 UTC midnight")->unwrap(), 1442793600);
140+
*
141+
* $r = $strtotime("nope");
142+
* self::assertTrue($r->isErr());
143+
* $r->unwrap(); // @throws RuntimeException Could not convert string to time
144+
* ```
145+
*
146+
* @template U
147+
* @template E of \Throwable
148+
* @param callable(mixed...):U $callback
149+
* @param class-string<E> $exceptionClass
150+
* @param class-string<E> $additionalExceptionClasses
151+
* @return \Closure(mixed...):Result<U,E>
152+
*/
153+
#[ExamplesSetup(IgnoreUnusedResults::class)]
154+
function ify(
155+
callable $callback,
156+
string $exceptionClass = \Exception::class,
157+
string ...$additionalExceptionClasses,
158+
): \Closure {
159+
return static function (...$args) use ($callback, $exceptionClass, $additionalExceptionClasses): Result
160+
{
161+
try {
162+
return Result\ok($callback(...$args));
163+
} catch (\Throwable $th) {
164+
/** @var Result\Err<E> */
165+
return Internal\trap($th, Result\err(...), $exceptionClass, ...$additionalExceptionClasses);
166+
}
167+
};
168+
}
169+
100170
/**
101171
* Converts from `Result<Result<T, E>, E>` to `Result<T, E>`.
102172
*

tests/Unit/Result/IfyTest.php

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace TH\Maybe\Tests\Unit\Result;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use TH\Maybe\Result;
7+
use TH\Maybe\Tests\Assert;
8+
use TH\Maybe\Tests\Provider;
9+
10+
final class IfyTest extends TestCase
11+
{
12+
use Provider\Values;
13+
14+
/**
15+
* @dataProvider values
16+
*/
17+
public function testIfyOk(mixed $value): void
18+
{
19+
$callback = static fn () => $value;
20+
21+
Assert::assertEquals($value, Result\ify($callback)()->unwrap());
22+
}
23+
24+
public function testIfyCheckedException(): void
25+
{
26+
$this->expectExceptionObject(
27+
new \Exception(
28+
"Failed to parse time string (nope) at position 0 (n): The timezone could not be found in the database",
29+
),
30+
);
31+
32+
Result\ify(
33+
// @phpstan-ignore-next-line
34+
static fn () => new \DateTimeImmutable("nope"),
35+
)()->unwrap();
36+
}
37+
38+
public function testIfyUncheckedException(): void
39+
{
40+
try {
41+
// @phpstan-ignore-next-line
42+
Result\ify(static fn () => 1 / 0)();
43+
Assert::fail("An exception should have been thrown");
44+
} catch (\DivisionByZeroError $ex) {
45+
Assert::assertEquals(
46+
"Division by zero",
47+
$ex->getMessage(),
48+
);
49+
}
50+
}
51+
52+
/**
53+
* @dataProvider values
54+
*/
55+
public function testIfyWithArguments(mixed $value): void
56+
{
57+
$fileGetContents = Result\ify(
58+
callback: static fn (string $filename): string => match ($content = \file_get_contents($filename)) {
59+
false => throw new \RuntimeException("Can't get content from $filename"),
60+
default => $content,
61+
},
62+
);
63+
64+
Assert::assertIsCallable($fileGetContents);
65+
}
66+
}

0 commit comments

Comments
 (0)