diff --git a/README.md b/README.md index 24c6a7c..902fc81 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,9 @@ $optionalString->ifPresent(function (string $value): void { echo $value; }); if ($optionalString->equals('value')) { echo 'It is `value`.'; } + +echo $optionalString->map(fn ($s) => "`{$s}`")->orElse('empty'); +echo $optionalString->flatMap(fn ($s) => OptionalString::of("`{$s}`"))->orElse('empty'); ``` ### Create and use your own typed optional diff --git a/src/AbstractOptional.php b/src/AbstractOptional.php index 4252c3e..59ca093 100644 --- a/src/AbstractOptional.php +++ b/src/AbstractOptional.php @@ -63,6 +63,25 @@ public function equals(mixed $obj): bool return ($obj === null || static::isSupported($obj)) && $this->value == $obj; } + /** + * @template U of mixed + * + * @param callable(T): self $mapper + * + * @return self + */ + public function flatMap(callable $mapper): self + { + if ($this->value !== null) { + $mapped = $mapper($this->value); + if (!$mapped instanceof self) { + throw new InvalidArgumentException('Mapper must return instance of ' . self::class . '.'); + } + return $mapped; + } + return $this; + } + /** * @return T * @@ -94,6 +113,27 @@ public function isPresent(): bool return $this->wasPresent = $this->value !== null; } + /** + * @template U of mixed + * + * @param callable(T): U $mapper + * + * @return self + */ + public function map(callable $mapper): self + { + /** @var callable(T): self $flatMapper */ + $flatMapper = static function (mixed $value) use ($mapper): self { + $mapped = $mapper($value); + try { + return TypedOptional::of($mapped); + } catch (Exception\CouldNotFindTypedOptionalForValue) { + return Optional::of($mapped); + } + }; + return $this->flatMap($flatMapper); + } + /** * @param T $other * @@ -140,7 +180,7 @@ public function orElseThrow(callable $exceptionSupplier): mixed } /** - * @param T|mixed $value not null + * @param mixed $value not null */ abstract protected static function isSupported(mixed $value): bool; } diff --git a/tests/OptionalTest.php b/tests/OptionalTest.php index 1ad9a96..516eccc 100644 --- a/tests/OptionalTest.php +++ b/tests/OptionalTest.php @@ -76,6 +76,20 @@ public static function dataMethodEqualsWorks(): array ]; } + #[DataProvider('dataMethodFlatMapWorks')] + public function testMethodFlatMapWorks(Optional $optional, Optional $expectedResult): void + { + self::assertTrue($expectedResult->equals($optional->flatMap(fn (string $v): Optional => Optional::of($v . 'x')))); + } + + public static function dataMethodFlatMapWorks(): array + { + return self::makeDataSet([ + [Optional::of(self::VALUE . 'x')], + [Optional::empty()], + ]); + } + #[DataProvider('dataMethodGetWorks')] public function testMethodGetWorks(Optional $optional, ?string $expectedValue, ?string $expectedException): void { @@ -127,6 +141,20 @@ public static function dataMethodIsPresentWorks(): array ]); } + #[DataProvider('dataMethodMapWorks')] + public function testMethodMapWorks(Optional $optional, mixed $expectedResult): void + { + self::assertTrue($expectedResult->equals($optional->map(fn (string $v): string => $v . 'x'))); + } + + public static function dataMethodMapWorks(): array + { + return self::makeDataSet([ + [OptionalString::of(self::VALUE . 'x')], + [Optional::empty()], + ]); + } + #[DataProvider('dataMethodOrElseWorks')] public function testMethodOrElseWorks(Optional $optional, string $expectedValue): void { diff --git a/tests/ReadmeTest.php b/tests/ReadmeTest.php index ce0e79d..2c00024 100644 --- a/tests/ReadmeTest.php +++ b/tests/ReadmeTest.php @@ -27,6 +27,8 @@ public static function getExpectedOutputsOfPhpExamples(): iterable . 'value' . 'value' . 'It is `value`.' + . '`value`' + . '`value`' , 'create--and-use-your-own-typed-optional' => '', ];