From 382b9ca46d7f88129d867da98990dace7e33cb98 Mon Sep 17 00:00:00 2001 From: Petr Knap <8299754+petrknap@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:58:32 +0200 Subject: [PATCH] fix: correct behavior of `flatMap` method --- phpdoc_check.php | 32 ++++++++++++-------------------- src/JavaSe8/Optional.php | 23 ++++++++++++----------- src/Optional.php | 24 ++++++------------------ src/OptionalOverrides.php | 13 +++++++++++++ tests/JavaSe8/AnotherOption.php | 4 ++-- tests/JavaSe8/OptionalTest.java | 10 +++------- 6 files changed, 48 insertions(+), 58 deletions(-) diff --git a/phpdoc_check.php b/phpdoc_check.php index c97926b..aacd8f9 100644 --- a/phpdoc_check.php +++ b/phpdoc_check.php @@ -104,20 +104,20 @@ public function rawInputs(int $int = 0, string $string = ''): void {} $concrete->filter(static fn (int $value): bool => true); $concrete->filter(static fn (string $value): bool => true); // @phpstan-ignore argument.type // - flatMap -$abstract->flatMap(static fn (int $value): Optional => $abstract); -$abstract->flatMap(static fn (string $value): Optional => $abstract); // @phpstan-ignore argument.type -$concrete->flatMap(static fn (int $value): Some\Optional => $concrete); -$concrete->flatMap(static fn (string $value): Some\Optional => $concrete); // @phpstan-ignore argument.type +$abstractFlatMapped = $abstract->flatMap(static fn (int $value): Some\Optional => Some\Optional::of('')); +$abstract->flatMap(static fn (string $value): Some\Optional => Some\Optional::of(1)); // @phpstan-ignore argument.type +$concreteFlatMapped = $concrete->flatMap(static fn (int $value): Optional => Optional::of('')); +$concrete->flatMap(static fn (string $value): Optional => Optional::of(1)); // @phpstan-ignore argument.type // - ifPresent $abstract->ifPresent(static fn (int $value): int => $value); $abstract->ifPresent(static fn (string $value): string => $value); // @phpstan-ignore argument.type $concrete->ifPresent(static fn (int $value): int => $value); $concrete->ifPresent(static fn (string $value): string => $value); // @phpstan-ignore argument.type // - map -$abstract->map(static fn (int $value): int => $value); -$abstract->map(static fn (string $value): string => $value); // @phpstan-ignore argument.type -$concrete->map(static fn (int $value): int => $value); -$concrete->map(static fn (string $value): string => $value); // @phpstan-ignore argument.type +$abstractMapped = $abstract->map(static fn (int $value): string => ''); +$abstract->map(static fn (string $value): int => 1); // @phpstan-ignore argument.type +$concreteMapped = $concrete->map(static fn (int $value): string => ''); +$concrete->map(static fn (string $value): int => 1); // @phpstan-ignore argument.type // - orElse $abstractOrElse = $abstract->orElse(0); $abstractOrElseNull = $abstract->orElse(null); @@ -131,33 +131,25 @@ public function rawInputs(int $int = 0, string $string = ''): void {} $concreteOrElseGet = $concrete->orElseGet(static fn (): int => 0); $concrete->orElseGet(static fn (): string => ''); // @phpstan-ignore argument.type -// Re-map & filter it -$abstractMapped = $abstract->map(static fn (int $value): string => ''); -$abstractMappedFiltered = $abstractMapped->filter(static fn (string $value): bool => true); -$abstractMapped->filter(static fn (int $value): bool => true); // @phpstan-ignore argument.type -$concreteMapped = $concrete->map(static fn (int $value): string => ''); -$concreteMappedFiltered = $concreteMapped->filter(static fn (string $value): bool => true); -$concreteMapped->filter(static fn (int $value): bool => true); // @phpstan-ignore argument.type - // Check it // - abstract $check->genericInputOptions(abstract: $abstract); // @phpstan-ignore argument.type -$check->genericInputOptions(abstract: $abstractMappedFiltered); $check->rawInputs(int: $abstract->get()); $check->rawInputs(int: $abstractOrElse); $check->rawInputs(int: $abstractOrElseGet); $check->rawInputs(int: $abstractOrElseNull); // @phpstan-ignore argument.type $check->rawInputs(int: $abstractOrElseNull ?? 0); -$check->rawInputs(string: $abstractMappedFiltered->orElseThrow()); +$check->rawInputs(string: $abstractMapped->orElseThrow()); +$check->rawInputs(string: $abstractFlatMapped->orElseThrow()); // - concrete $check->genericInputOptions(concrete: $concrete); // @phpstan-ignore argument.type -$check->genericInputOptions(concrete: $concreteMappedFiltered); $check->rawInputs(int: $concrete->get()); $check->rawInputs(int: $concreteOrElse); $check->rawInputs(int: $concreteOrElseGet); $check->rawInputs(int: $concreteOrElseNull); // @phpstan-ignore argument.type $check->rawInputs(int: $concreteOrElseNull ?? 0); -$check->rawInputs(string: $concreteMappedFiltered->orElseThrow()); +$check->rawInputs(string: $concreteMapped->orElseThrow()); +$check->rawInputs(string: $concreteFlatMapped->orElseThrow()); // --------------------------------------------------------------------------------------------------------------------- diff --git a/src/JavaSe8/Optional.php b/src/JavaSe8/Optional.php index 833fc28..d6ce153 100644 --- a/src/JavaSe8/Optional.php +++ b/src/JavaSe8/Optional.php @@ -16,14 +16,14 @@ interface Optional { /** - * Returns an empty {@see self::class} instance. + * Returns an empty {@see Optional} instance. * * @return self */ public static function empty(): self; /** - * Returns an {@see self::class} with the specified present non-null value. + * Returns an {@see Optional} with the specified present non-null value. * * @template U of T * @@ -34,7 +34,7 @@ public static function empty(): self; public static function of(mixed $value): self; /** - * Returns an {@see self::class} describing the specified value, if non-null, otherwise returns an empty {@see self::class}. + * Returns an {@see Optional} describing the specified value, if non-null, otherwise returns an empty {@see Optional}. * * @template U of T * @@ -45,12 +45,12 @@ public static function of(mixed $value): self; public static function ofNullable(mixed $value): self; /** - * Indicates whether some other object is "equal to" this {@see self::class}. + * Indicates whether some other object is "equal to" this {@see Optional}. */ public function equals(mixed $obj): bool; /** - * If a value is present, and the value matches the given predicate, return an {@see self::class} describing the value, otherwise return an empty {@see self::class}. + * If a value is present, and the value matches the given predicate, return an {@see Optional} describing the value, otherwise return an empty {@see Optional}. * * @param callable(T): bool $predicate * @@ -59,18 +59,19 @@ public function equals(mixed $obj): bool; public function filter(callable $predicate): self; /** - * If a value is present, apply the provided {@see self::class}-bearing mapping function to it, return that result, otherwise return an empty {@see self::class}. + * If a value is present, apply the provided {@see Optional}-bearing mapping function to it, return that result, otherwise return an empty {@see Optional}. * * @template U of mixed type of non-null value + * @template V of Optional * - * @param callable(T): self $mapper + * @param callable(T): V $mapper * - * @return self + * @return V|Optional */ - public function flatMap(callable $mapper): self; + public function flatMap(callable $mapper): Optional; /** - * If a value is present in this {@see self::class}, returns the value, otherwise throws {@see NoSuchElementException}. + * If a value is present in this {@see Optional}, returns the value, otherwise throws {@see NoSuchElementException}. * * @return T * @@ -91,7 +92,7 @@ public function ifPresent(callable $consumer): void; public function isPresent(): bool; /** - * If a value is present, apply the provided mapping function to it, and if the result is non-null, return an {@see self::class} describing the result. + * If a value is present, apply the provided mapping function to it, and if the result is non-null, return an {@see Optional} describing the result. * * @template U of mixed type of non-null value * diff --git a/src/Optional.php b/src/Optional.php index 9e37bac..b76c701 100644 --- a/src/Optional.php +++ b/src/Optional.php @@ -155,31 +155,19 @@ public function filter(callable $predicate): self /** * @template U of mixed type of non-null value - * @template V of Optional + * @template V of JavaSe8\Optional * - * @param ($empty is null ? callable(T): JavaSe8\Optional : callable(T): V) $mapper - * @param V|null $empty + * @param callable(T): V $mapper * - * @return ($empty is null ? Optional : V) - * - * @note do not use {@see self}/{@see static}, it maps itself to another {@see Optional} - * - * @todo return JavaSe8\Optional + * @return V|self */ - public function flatMap(callable $mapper, Optional|null $empty = null): Optional + public function flatMap(callable $mapper): JavaSe8\Optional { if ($this->value === null) { - /** @var V */ - return $empty ?? Optional::empty(); + return $this; } - /** @var JavaSe8\Optional|"" $mapped */ - $mapped = $mapper($this->value); - return match (true) { - $mapped instanceof Optional => $mapped, - $mapped instanceof JavaSe8\Optional => $mapped->isPresent() ? Optional::of($mapped->get()) : Optional::empty(), - default => throw new InvalidArgumentException('Mapper must return instance of ' . JavaSe8\Optional::class . '.'), - }; + return $mapper($this->value); } public function get(): mixed diff --git a/src/OptionalOverrides.php b/src/OptionalOverrides.php index 7d02609..6326580 100644 --- a/src/OptionalOverrides.php +++ b/src/OptionalOverrides.php @@ -95,6 +95,19 @@ public function filter(callable $predicate): self return parent::filter($predicate); } + /** + * @template U of mixed type of non-null value + * @template V of JavaSe8\Optional + * + * @param callable(T): V $mapper + * + * @return V|self + */ + public function flatMap(callable $mapper): JavaSe8\Optional + { + return parent::flatMap($mapper); + } + /** * @template U of mixed type of non-null value * diff --git a/tests/JavaSe8/AnotherOption.php b/tests/JavaSe8/AnotherOption.php index 415b1ac..a8d539e 100644 --- a/tests/JavaSe8/AnotherOption.php +++ b/tests/JavaSe8/AnotherOption.php @@ -48,9 +48,9 @@ public function ifPresent(callable $consumer): never NotImplemented::throw(__METHOD__); } - public function isPresent(): bool + public function isPresent(): never { - return false; + NotImplemented::throw(__METHOD__); } public function map(callable $mapper): never diff --git a/tests/JavaSe8/OptionalTest.java b/tests/JavaSe8/OptionalTest.java index 78b7f26..92a513b 100644 --- a/tests/JavaSe8/OptionalTest.java +++ b/tests/JavaSe8/OptionalTest.java @@ -37,13 +37,9 @@ private static void testFilter() { } private static void testFlatMap() { - /** - * @todo - * Optional $o1; - * $o1 = Optional.empty(); assert (Optional.of(1).flatMap($v -> $o1) == $o1); - */ - Optional $o2; - $o2 = Optional.empty(); assert (Optional.empty().flatMap($v -> $o2) != $o2); + Optional $anotherOption, $thisOption; + $anotherOption = Optional.empty(); assert (Optional.of(1).flatMap($v -> $anotherOption) == $anotherOption); + $thisOption = Optional.empty(); assert ($thisOption.flatMap($v -> Optional.of(new Another())) == $thisOption); } private static void testGetters() {