From 0995dea788a49b0bb0d81a4385e1e341ba0d198d Mon Sep 17 00:00:00 2001 From: Petr Knap <8299754+petrknap@users.noreply.github.com> Date: Wed, 7 Jan 2026 22:56:11 +0100 Subject: [PATCH 1/9] refactor: `static`s replaced by `self`s and `Optional`s --- composer.json | 2 +- phpdoc_check.php | 137 +++++++++++++++++++++++ src/Exception/TypedOptionalException.php | 2 +- src/JavaSe8/Optional.php | 26 +++-- src/NonGenericOptional.php | 49 ++++++++ src/Optional.php | 80 +++++++++---- src/OptionalArray.php | 84 +++++++++++++- src/OptionalBool.php | 2 + src/OptionalFloat.php | 2 + src/OptionalInt.php | 2 + src/OptionalObject.php | 72 +++++++++++- src/OptionalObject/OptionalStdClass.php | 3 + src/OptionalResource.php | 12 +- src/OptionalResource/OptionalStream.php | 3 + src/OptionalString.php | 2 + tests/NonGenericOptionalTest.php | 41 +++++++ tests/Some/OptionalDataObject.php | 3 + 17 files changed, 482 insertions(+), 40 deletions(-) create mode 100644 phpdoc_check.php create mode 100644 src/NonGenericOptional.php create mode 100644 tests/NonGenericOptionalTest.php diff --git a/composer.json b/composer.json index 312726b..6c50df3 100755 --- a/composer.json +++ b/composer.json @@ -46,7 +46,7 @@ ], "check-implementation": [ "phpcs --colors --standard=PSR12 --exclude=Generic.Files.LineLength src tests", - "phpstan analyse --level max src --ansi --no-interaction", + "phpstan analyse --level max src phpdoc_check.php --ansi --no-interaction", "phpstan analyse --level 5 tests --ansi --no-interaction", "phpinsights analyse src tests --ansi --no-interaction --format=github-action | sed -e \"s#::error file=$PWD/#::notice file=#g\"" ], diff --git a/phpdoc_check.php b/phpdoc_check.php new file mode 100644 index 0000000..20ce7c1 --- /dev/null +++ b/phpdoc_check.php @@ -0,0 +1,137 @@ + null; +$functionWithNonGenericInputOptions = fn (OptionalString $string = null, OptionalInt $int = null) => null; +$functionWithNonGenericInputs = fn (string $string = '', int $int = 0) => null; + +// --------------------------------------------------------------------------------------------------------------------- + +// Call all factories +OptionalString::of(''); +OptionalString::of(null); // @phpstan-ignore argument.type +OptionalString::of(false); // @phpstan-ignore argument.type +OptionalString::ofFalsable(''); +OptionalString::ofFalsable(null); // @phpstan-ignore argument.type +OptionalString::ofFalsable(false); +OptionalString::ofNullable(''); +OptionalString::ofNullable(null); +OptionalString::ofNullable(false); // @phpstan-ignore argument.type +OptionalString::ofSingle(['']); +OptionalString::ofSingle([null]); // @phpstan-ignore argument.type +OptionalString::ofSingle([false]); // @phpstan-ignore argument.type +OptionalString::ofSingle([]); + +// Check sub-typed optional factory +OptionalObject::of(new stdClass()); +OptionalObject::of(new class { +}); +OptionalObject::of(''); // @phpstan-ignore argument.type, argument.templateType +OptionalObject\OptionalStdClass::of(new stdClass()); +OptionalObject\OptionalStdClass::of(new class { // @phpstan-ignore argument.type +}); +OptionalObject\OptionalStdClass::of(''); // @phpstan-ignore argument.type + +// --------------------------------------------------------------------------------------------------------------------- + +// Create generic option +$stringOption = Optional::of(''); + +// Call all methods with generic arguments +$stringOption->filter(static fn (string $value): bool => true); +$stringOption->filter(static fn (int $value): bool => true); // @phpstan-ignore argument.type +$stringOption->flatMap(static fn (string $value): Optional => $stringOption); +$stringOption->flatMap(static fn (int $value): Optional => $stringOption); // @phpstan-ignore argument.type +$stringOption->ifPresent(static fn (string $value): string => $value); +$stringOption->ifPresent(static fn (int $value): int => $value); // @phpstan-ignore argument.type +$stringOption->map(static fn (string $value): string => $value); +$stringOption->map(static fn (int $value): int => $value); // @phpstan-ignore argument.type +$stringOptionOrElse = $stringOption->orElse(''); +$stringOptionOrElseNull = $stringOption->orElse(null); +$stringOption->orElse(0); // @phpstan-ignore argument.type +$stringOptionOrElseGet = $stringOption->orElseGet(static fn (): string => ''); +$stringOption->orElseGet(static fn (): int => 0); // @phpstan-ignore argument.type + +// Use generic option as input for functions +$functionWithGenericInputOptions(string: $stringOption); +$functionWithNonGenericInputOptions(string: $stringOption); // @phpstan-ignore argument.type +$functionWithNonGenericInputs(string: $stringOption->get()); +$functionWithNonGenericInputs(string: $stringOption->orElseThrow()); +$functionWithNonGenericInputs(string: $stringOptionOrElse); +$functionWithNonGenericInputs(string: $stringOptionOrElseNull); // @phpstan-ignore argument.type +$functionWithNonGenericInputs(string: $stringOptionOrElseNull ?? ''); +$functionWithNonGenericInputs(string: $stringOptionOrElseGet); + +// Re-map generic option & call filter on it to check new generic +$intOptionMapped = $stringOption->map(static fn (string $value): int => 0); +$intOptionMappedFiltered = $intOptionMapped->filter(static fn (int $value): bool => true); +$intOptionMapped->filter(static fn (string $value): bool => true); // @phpstan-ignore argument.type +$intOptionFlatMapped = $stringOption->flatMap(static fn (string $value): Optional => Optional::of(0)); +$intOptionFlatMappedFiltered = $intOptionFlatMapped->filter(static fn (int $value): bool => true); +$intOptionFlatMapped->filter(static fn (string $value): bool => true); // @phpstan-ignore argument.type + +// Use re-mapped filtered options as input for functions +$functionWithGenericInputOptions(int: $intOptionMappedFiltered); +$functionWithNonGenericInputOptions(int: $intOptionMappedFiltered); // @phpstan-ignore argument.type +$functionWithNonGenericInputs(int: $intOptionMappedFiltered->get()); +$functionWithGenericInputOptions(int: $intOptionFlatMappedFiltered); +$functionWithNonGenericInputOptions(int: $intOptionFlatMappedFiltered); // @phpstan-ignore argument.type +$functionWithNonGenericInputs(int: $intOptionFlatMappedFiltered->get()); + +// --------------------------------------------------------------------------------------------------------------------- + +// Create non-generic option +$stringOption = OptionalString::of(''); + +// Call all methods with generic arguments +$stringOption->filter(static fn (string $value): bool => true); +$stringOption->filter(static fn (int $value): bool => true); // @phpstan-ignore argument.type +$stringOption->flatMap(static fn (string $value): OptionalString => $stringOption); +$stringOption->flatMap(static fn (int $value): OptionalString => $stringOption); // @phpstan-ignore argument.type +$stringOption->ifPresent(static fn (string $value): string => $value); +$stringOption->ifPresent(static fn (int $value): int => $value); // @phpstan-ignore argument.type +$stringOption->map(static fn (string $value): string => $value); +$stringOption->map(static fn (int $value): int => $value); // @phpstan-ignore argument.type +$stringOptionOrElse = $stringOption->orElse(''); +$stringOptionOrElseNull = $stringOption->orElse(null); +$stringOption->orElse(0); // @phpstan-ignore argument.type +$stringOptionOrElseGet = $stringOption->orElseGet(static fn (): string => ''); +$stringOption->orElseGet(static fn (): int => 0); // @phpstan-ignore argument.type + +// Use non-generic option as input for functions +$functionWithGenericInputOptions(string: $stringOption); +$functionWithNonGenericInputOptions(string: $stringOption); +$functionWithNonGenericInputs(string: $stringOption->get()); +$functionWithNonGenericInputs(string: $stringOption->orElseThrow()); +$functionWithNonGenericInputs(string: $stringOptionOrElse); +$functionWithNonGenericInputs(string: $stringOptionOrElseNull); // @phpstan-ignore argument.type +$functionWithNonGenericInputs(string: $stringOptionOrElseNull ?? ''); +$functionWithNonGenericInputs(string: $stringOptionOrElseGet); + +// Re-map typed option & call filter on it to check new generic +$intOptionMapped = $stringOption->map(static fn (string $value): int => 0); +$intOptionMappedFiltered = $intOptionMapped->filter(static fn (int $value): bool => true); +$intOptionMapped->filter(static fn (string $value): bool => true); // @phpstan-ignore argument.type +$intOptionFlatMapped = $stringOption->flatMap(static fn (string $value): OptionalInt => OptionalInt::of(0)); +$intOptionFlatMappedFiltered = $intOptionFlatMapped->filter(static fn (int $value): bool => true); +$intOptionFlatMapped->filter(static fn (string $value): bool => true); // @phpstan-ignore argument.type + +// Use re-mapped filtered options as input for functions +$functionWithGenericInputOptions(int: $intOptionMappedFiltered); +$functionWithNonGenericInputOptions(int: $intOptionMappedFiltered); // @phpstan-ignore argument.type +$functionWithNonGenericInputs(int: $intOptionMappedFiltered->get()); +$functionWithGenericInputOptions(int: $intOptionFlatMappedFiltered); +$functionWithNonGenericInputOptions(int: $intOptionFlatMappedFiltered); // @phpstan-ignore argument.type +$functionWithNonGenericInputs(int: $intOptionFlatMappedFiltered->get()); + +// --------------------------------------------------------------------------------------------------------------------- diff --git a/src/Exception/TypedOptionalException.php b/src/Exception/TypedOptionalException.php index 6338cde..3709716 100644 --- a/src/Exception/TypedOptionalException.php +++ b/src/Exception/TypedOptionalException.php @@ -5,7 +5,7 @@ namespace PetrKnap\Optional\Exception; /** - * @todo extend {@see Exception}, not {@see OptionalException} + * @todo BC extend {@see Exception}, not {@see OptionalException} */ interface TypedOptionalException extends OptionalException { diff --git a/src/JavaSe8/Optional.php b/src/JavaSe8/Optional.php index c2b2e8c..8e50d1f 100644 --- a/src/JavaSe8/Optional.php +++ b/src/JavaSe8/Optional.php @@ -17,22 +17,32 @@ interface Optional { /** * Returns an empty {@see self::class} instance. + * + * @return self */ - public static function empty(): static; + public static function empty(): self; /** * Returns an {@see self::class} with the specified present non-null value. * - * @param T $value + * @template U of T + * + * @param U $value + * + * @return self */ - public static function of(mixed $value): static; + 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}. * - * @param T|null $value + * @template U of T + * + * @param U|null $value + * + * @return self */ - public static function ofNullable(mixed $value): static; + public static function ofNullable(mixed $value): self; /** * Indicates whether some other object is "equal to" this {@see self::class}. @@ -43,8 +53,10 @@ 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}. * * @param callable(T): bool $predicate + * + * @return self */ - public function filter(callable $predicate): static; + 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}. @@ -69,7 +81,7 @@ public function get(): mixed; /** * If a value is present, invoke the specified consumer with the value, otherwise do nothing. * - * @param callable(T): void $consumer + * @param callable(T): mixed $consumer */ public function ifPresent(callable $consumer): void; diff --git a/src/NonGenericOptional.php b/src/NonGenericOptional.php new file mode 100644 index 0000000..ecc9814 --- /dev/null +++ b/src/NonGenericOptional.php @@ -0,0 +1,49 @@ + + */ + public static function empty(): self { - return static::ofNullable(null); + /** @var T|null $typedNull */ + $typedNull = null; + return static::ofNullable($typedNull); } - public static function of(mixed $value): static + /** + * @template U of T + * + * @param U $value + * + * @return self + */ + public static function of(mixed $value): self { if ($value === null) { throw new InvalidArgumentException('Value must not be null.'); @@ -43,27 +55,39 @@ public static function of(mixed $value): static /** * In many cases a failure returns `false`, this method serves as a factory for them. * - * @param T|false $value + * @template U of T + * + * @param U|false $value + * + * @return self */ - public static function ofFalsable(mixed $value): static + public static function ofFalsable(mixed $value): self { if ($value === false) { + /** @var self */ return static::empty(); } return static::of($value); } - public static function ofNullable(mixed $value): static + /** + * @template U of T + * + * @param U|null $value + * + * @return self + */ + public static function ofNullable(mixed $value): self { if (static::class === Optional::class) { if ($value !== null) { try { - /** @var static */ + /** @var self */ return TypedOptional::of($value, Optional::class); } catch (Exception\CouldNotFindTypedOptionalForValue) { } } - /** @var static */ + /** @var self */ return new class ($value) extends Optional { protected static function isInstanceOfStatic(object $obj): bool { @@ -83,9 +107,13 @@ protected static function isSupported(mixed $value): bool /** * In many cases you will receive an `iterable` of single value, this method serves as a factory for them. * - * @param iterable $value + * @template U of T + * + * @param iterable $value + * + * @return self */ - public static function ofSingle(iterable $value): static + public static function ofSingle(iterable $value): self { $option = null; foreach ($value as $v) { @@ -103,6 +131,7 @@ public static function ofSingle(iterable $value): static public function equals(mixed $obj, bool $strict = false): bool { if (!($obj instanceof JavaSe8\Optional)) { + /** @var T|null $obj */ try { $obj = static::ofNullable($obj); } catch (InvalidArgumentException) { @@ -124,7 +153,10 @@ public function equals(mixed $obj, bool $strict = false): bool return false; } - public function filter(callable $predicate): static + /** + * @return self + */ + public function filter(callable $predicate): self { if ($this->value !== null) { $matches = $predicate($this->value); @@ -143,20 +175,22 @@ public function filter(callable $predicate): static * * @param callable(T): JavaSe8\Optional $mapper * - * @return self + * @return Optional + * + * @note do not use {@see self} to indicate that it uses {@see Optional}, not {@see static} */ - public function flatMap(callable $mapper): self + public function flatMap(callable $mapper): Optional { if ($this->value === null) { - /** @var self */ + /** @var Optional */ return Optional::empty(); } - /** @var mixed $mapped */ + /** @var JavaSe8\Optional|"" $mapped */ $mapped = $mapper($this->value); - /** @var self */ + /** @var Optional */ return match (true) { - $mapped instanceof self => $mapped, + $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 . '.'), }; @@ -189,13 +223,15 @@ public function isPresent(): bool * * @param callable(T): U $mapper * - * @return self + * @return Optional + * + * @note do not use {@see self} to indicate that it uses {@see Optional}, not {@see static} */ - public function map(callable $mapper): self + public function map(callable $mapper): Optional { - /** @var callable(T): self $flatMapper */ - $flatMapper = static function (mixed $value) use ($mapper): self { - /** @var mixed $mapped */ + /** @var callable(T): Optional $flatMapper */ + $flatMapper = static function (mixed $value) use ($mapper): Optional { + /** @var T $mapped */ $mapped = $mapper($value); return Optional::ofNullable($mapped); }; diff --git a/src/OptionalArray.php b/src/OptionalArray.php index cd92a90..b436b31 100644 --- a/src/OptionalArray.php +++ b/src/OptionalArray.php @@ -5,13 +5,91 @@ namespace PetrKnap\Optional; /** - * @template K of array-key - * @template V of mixed + * @template TKey of array-key + * @template TValue of mixed * - * @extends Optional> + * @extends Optional> */ final class OptionalArray extends Optional { + /** + * @return self + */ + public static function empty(): self + { + /** @var self */ + return parent::empty(); + } + + /** + * @template UKey of TKey + * @template UValue of TValue + * + * @param array $value + * + * @return self + */ + public static function of(mixed $value): self + { + /** @var array $value */ + /** @var self */ + return parent::of($value); + } + + /** + * @template UKey of TKey + * @template UValue of TValue + * + * @param array|false $value + * + * @return self + */ + public static function ofFalsable(mixed $value): self + { + /** @var array|false $value */ + /** @var self */ + return parent::ofFalsable($value); + } + + /** + * @template UKey of TKey + * @template UValue of TValue + * + * @param array|null $value + * + * @return self + */ + public static function ofNullable(mixed $value): self + { + /** @var array|null $value */ + /** @var self */ + return parent::ofNullable($value); + } + + /** + * @template UKey of TKey + * @template UValue of TValue + * + * @param iterable> $value + * + * @return self + */ + public static function ofSingle(iterable $value): self + { + /** @var iterable> $value */ + /** @var self */ + return parent::ofSingle($value); + } + + /** + * @return self + */ + public function filter(callable $predicate): self + { + /** @var self */ + return parent::filter($predicate); + } + protected static function isSupported(mixed $value): bool { return is_array($value); diff --git a/src/OptionalBool.php b/src/OptionalBool.php index 5eade2a..b6d7f4d 100644 --- a/src/OptionalBool.php +++ b/src/OptionalBool.php @@ -9,6 +9,8 @@ */ final class OptionalBool extends Optional { + use NonGenericOptional; + protected static function isSupported(mixed $value): bool { return is_bool($value); diff --git a/src/OptionalFloat.php b/src/OptionalFloat.php index b2a62e8..9c69542 100644 --- a/src/OptionalFloat.php +++ b/src/OptionalFloat.php @@ -9,6 +9,8 @@ */ final class OptionalFloat extends Optional { + use NonGenericOptional; + protected static function isSupported(mixed $value): bool { return is_float($value); diff --git a/src/OptionalInt.php b/src/OptionalInt.php index 8178e86..411b327 100644 --- a/src/OptionalInt.php +++ b/src/OptionalInt.php @@ -9,6 +9,8 @@ */ final class OptionalInt extends Optional { + use NonGenericOptional; + protected static function isSupported(mixed $value): bool { return is_int($value); diff --git a/src/OptionalObject.php b/src/OptionalObject.php index 1829dd7..1d08171 100644 --- a/src/OptionalObject.php +++ b/src/OptionalObject.php @@ -14,17 +14,60 @@ abstract class OptionalObject extends Optional /** @internal */ protected const ANY_INSTANCE_OF = ''; - public static function ofNullable(mixed $value): static + + /** + * @return self + */ + public static function empty(): self + { + /** @var self */ + return parent::empty(); + } + + /** + * @template U of T + * + * @param U $value + * + * @return self + */ + public static function of(mixed $value): self + { + /** @var self */ + return parent::of($value); + } + + /** + * @template U of T + * + * @param U|false $value + * + * @return self + */ + public static function ofFalsable(mixed $value): self + { + /** @var self */ + return parent::ofFalsable($value); + } + + /** + * @template U of T + * + * @param U|null $value + * + * @return self + */ + public static function ofNullable(mixed $value): self { if (static::class === OptionalObject::class) { if ($value !== null) { try { - /** @var static */ + /** @var self */ return TypedOptional::of($value, OptionalObject::class); } catch (Exception\CouldNotFindTypedOptionalForValue) { } } - /** @var static */ + /** @var self */ return new class ($value) extends OptionalObject { protected static function isInstanceOfStatic(object $obj): bool { @@ -38,9 +81,32 @@ protected static function getInstanceOf(): string } }; } + /** @var self */ return parent::ofNullable($value); } + /** + * @template U of T + * + * @param iterable $value + * + * @return self + */ + public static function ofSingle(iterable $value): self + { + /** @var self */ + return parent::ofSingle($value); + } + + /** + * @return self + */ + public function filter(callable $predicate): self + { + /** @var self */ + return parent::filter($predicate); + } + protected static function isSupported(mixed $value): bool { /** @var string $expectedInstanceOf */ diff --git a/src/OptionalObject/OptionalStdClass.php b/src/OptionalObject/OptionalStdClass.php index d83cbbb..e8707e4 100644 --- a/src/OptionalObject/OptionalStdClass.php +++ b/src/OptionalObject/OptionalStdClass.php @@ -4,6 +4,7 @@ namespace PetrKnap\Optional\OptionalObject; +use PetrKnap\Optional\NonGenericOptional; use PetrKnap\Optional\OptionalObject; use stdClass; @@ -12,6 +13,8 @@ */ final class OptionalStdClass extends OptionalObject { + use NonGenericOptional; + protected static function getInstanceOf(): string { return stdClass::class; diff --git a/src/OptionalResource.php b/src/OptionalResource.php index 74553e4..cc3e6d1 100644 --- a/src/OptionalResource.php +++ b/src/OptionalResource.php @@ -9,20 +9,25 @@ */ abstract class OptionalResource extends Optional { + use NonGenericOptional; + /** @internal */ protected const ANY_RESOURCE_TYPE = ''; - public static function ofNullable(mixed $value): static + /** + * @param resource|null $value + */ + public static function ofNullable(mixed $value): self { if (static::class === OptionalResource::class) { if ($value !== null) { try { - /** @var static */ + /** @var self */ return TypedOptional::of($value, OptionalResource::class); } catch (Exception\CouldNotFindTypedOptionalForValue) { } } - /** @var static */ + /** @var self */ return new class ($value) extends OptionalResource { protected static function isInstanceOfStatic(object $obj): bool { @@ -36,6 +41,7 @@ protected static function getResourceType(): string } }; } + /** @var self */ return parent::ofNullable($value); } diff --git a/src/OptionalResource/OptionalStream.php b/src/OptionalResource/OptionalStream.php index 69bddec..aa2d452 100644 --- a/src/OptionalResource/OptionalStream.php +++ b/src/OptionalResource/OptionalStream.php @@ -4,10 +4,13 @@ namespace PetrKnap\Optional\OptionalResource; +use PetrKnap\Optional\NonGenericOptional; use PetrKnap\Optional\OptionalResource; final class OptionalStream extends OptionalResource { + use NonGenericOptional; + protected static function getResourceType(): string { return 'stream'; diff --git a/src/OptionalString.php b/src/OptionalString.php index 925bed6..c8784a1 100644 --- a/src/OptionalString.php +++ b/src/OptionalString.php @@ -9,6 +9,8 @@ */ final class OptionalString extends Optional { + use NonGenericOptional; + protected static function isSupported(mixed $value): bool { return is_string($value); diff --git a/tests/NonGenericOptionalTest.php b/tests/NonGenericOptionalTest.php new file mode 100644 index 0000000..c6be444 --- /dev/null +++ b/tests/NonGenericOptionalTest.php @@ -0,0 +1,41 @@ +filter(static fn (): bool => true); + $option->filter(static fn (): bool => false); + } + + public static function dataMethodsReturnsSelf(): array + { + return [ + 'empty' => [Some\OptionalDataObject::empty()], + 'some' => [Some\OptionalDataObject::of(new Some\DataObject())], + ]; + } +} diff --git a/tests/Some/OptionalDataObject.php b/tests/Some/OptionalDataObject.php index 78a0949..47c179c 100644 --- a/tests/Some/OptionalDataObject.php +++ b/tests/Some/OptionalDataObject.php @@ -4,6 +4,7 @@ namespace PetrKnap\Optional\Some; +use PetrKnap\Optional\NonGenericOptional; use PetrKnap\Optional\OptionalObject; /** @@ -11,6 +12,8 @@ */ final class OptionalDataObject extends OptionalObject { + use NonGenericOptional; + protected static function getInstanceOf(): string { return DataObject::class; From 19d863b0efdeb2d010813f2e6c85255910c2901d Mon Sep 17 00:00:00 2001 From: Petr Knap <8299754+petrknap@users.noreply.github.com> Date: Sat, 10 Jan 2026 10:07:37 +0100 Subject: [PATCH 2/9] wip --- src/Optional.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Optional.php b/src/Optional.php index 9c10e9a..00c44d3 100644 --- a/src/Optional.php +++ b/src/Optional.php @@ -122,6 +122,7 @@ public static function ofSingle(iterable $value): self } $option = static::of($v); } + /** @var self */ return $option ?? static::empty(); } From b298fac58e6fa4cd42fcc5af7c68c08050297a0a Mon Sep 17 00:00:00 2001 From: Petr Knap <8299754+petrknap@users.noreply.github.com> Date: Sat, 10 Jan 2026 10:07:54 +0100 Subject: [PATCH 3/9] wip --- src/OptionalArray.php | 53 ++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/src/OptionalArray.php b/src/OptionalArray.php index b436b31..e93bd45 100644 --- a/src/OptionalArray.php +++ b/src/OptionalArray.php @@ -5,88 +5,79 @@ namespace PetrKnap\Optional; /** - * @template TKey of array-key - * @template TValue of mixed + * @template T of array * - * @extends Optional> + * @extends Optional */ final class OptionalArray extends Optional { /** - * @return self + * @return self */ public static function empty(): self { - /** @var self */ + /** @var self */ return parent::empty(); } /** - * @template UKey of TKey - * @template UValue of TValue + * @template U of T * - * @param array $value + * @param U $value * - * @return self + * @return self */ public static function of(mixed $value): self { - /** @var array $value */ - /** @var self */ + /** @var self */ return parent::of($value); } /** - * @template UKey of TKey - * @template UValue of TValue + * @template U of T * - * @param array|false $value + * @param U|false $value * - * @return self + * @return self */ public static function ofFalsable(mixed $value): self { - /** @var array|false $value */ - /** @var self */ + /** @var self */ return parent::ofFalsable($value); } /** - * @template UKey of TKey - * @template UValue of TValue + * @template U of T * - * @param array|null $value + * @param U|null $value * - * @return self + * @return self */ public static function ofNullable(mixed $value): self { - /** @var array|null $value */ - /** @var self */ + /** @var self */ return parent::ofNullable($value); } /** - * @template UKey of TKey - * @template UValue of TValue + * @template U of T * - * @param iterable> $value + * @param iterable $value * - * @return self + * @return self */ public static function ofSingle(iterable $value): self { - /** @var iterable> $value */ - /** @var self */ + /** @var self */ return parent::ofSingle($value); } /** - * @return self + * @return self */ public function filter(callable $predicate): self { - /** @var self */ + /** @var self */ return parent::filter($predicate); } From f7f7fc6ddc4c63337f8e92e5df03d8398b852f22 Mon Sep 17 00:00:00 2001 From: Petr Knap <8299754+petrknap@users.noreply.github.com> Date: Sat, 10 Jan 2026 10:31:20 +0100 Subject: [PATCH 4/9] wip --- phpdoc_check.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/phpdoc_check.php b/phpdoc_check.php index 20ce7c1..98110cb 100644 --- a/phpdoc_check.php +++ b/phpdoc_check.php @@ -7,6 +7,7 @@ declare(strict_types=1); use PetrKnap\Optional\Optional; +use PetrKnap\Optional\OptionalArray; use PetrKnap\Optional\OptionalInt; use PetrKnap\Optional\OptionalObject; use PetrKnap\Optional\OptionalString; @@ -135,3 +136,16 @@ $functionWithNonGenericInputs(int: $intOptionFlatMappedFiltered->get()); // --------------------------------------------------------------------------------------------------------------------- + +// Create complexly generic option +/** @var array{string, int} $array */ +$array = ['', 1]; +$arrayOption = OptionalArray::of($array); + +// Use complexly generic option as input for function +$functionWithNonGenericInputs(string: $arrayOption->get()[0]); +$functionWithNonGenericInputs(string: $arrayOption->get()[1]); // @phpstan-ignore argument.type +$functionWithNonGenericInputs(int: $arrayOption->get()[0]); // @phpstan-ignore argument.type +$functionWithNonGenericInputs(int: $arrayOption->get()[1]); + +// --------------------------------------------------------------------------------------------------------------------- From b4f1f0c6be7c28226e26f464ac24397d5056ec34 Mon Sep 17 00:00:00 2001 From: Petr Knap <8299754+petrknap@users.noreply.github.com> Date: Sat, 10 Jan 2026 10:37:11 +0100 Subject: [PATCH 5/9] wip --- phpdoc_check.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/phpdoc_check.php b/phpdoc_check.php index 98110cb..e159a21 100644 --- a/phpdoc_check.php +++ b/phpdoc_check.php @@ -139,9 +139,13 @@ // Create complexly generic option /** @var array{string, int} $array */ -$array = ['', 1]; +$array = ['', 0]; $arrayOption = OptionalArray::of($array); +// Call some methods with generic arguments +$arrayOption->orElse(['1', 1]); +$arrayOption->orElse([1, '1']); // @phpstan-ignore argument.type + // Use complexly generic option as input for function $functionWithNonGenericInputs(string: $arrayOption->get()[0]); $functionWithNonGenericInputs(string: $arrayOption->get()[1]); // @phpstan-ignore argument.type From 907fd6031bae72ba10d1f93a6b037999d570acae Mon Sep 17 00:00:00 2001 From: Petr Knap <8299754+petrknap@users.noreply.github.com> Date: Sat, 10 Jan 2026 10:42:29 +0100 Subject: [PATCH 6/9] wip --- phpdoc_check.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/phpdoc_check.php b/phpdoc_check.php index e159a21..7e86cb8 100644 --- a/phpdoc_check.php +++ b/phpdoc_check.php @@ -143,13 +143,15 @@ $arrayOption = OptionalArray::of($array); // Call some methods with generic arguments +$arrayOptionFiltered = $arrayOption->filter(static fn (array $value): bool => true); +$arrayOption->filter(static fn (string $value): bool => true); // @phpstan-ignore argument.type $arrayOption->orElse(['1', 1]); $arrayOption->orElse([1, '1']); // @phpstan-ignore argument.type -// Use complexly generic option as input for function -$functionWithNonGenericInputs(string: $arrayOption->get()[0]); -$functionWithNonGenericInputs(string: $arrayOption->get()[1]); // @phpstan-ignore argument.type -$functionWithNonGenericInputs(int: $arrayOption->get()[0]); // @phpstan-ignore argument.type -$functionWithNonGenericInputs(int: $arrayOption->get()[1]); +// Use filtered complexly generic option as input for function +$functionWithNonGenericInputs(string: $arrayOptionFiltered->get()[0]); +$functionWithNonGenericInputs(string: $arrayOptionFiltered->get()[1]); // @phpstan-ignore argument.type +$functionWithNonGenericInputs(int: $arrayOptionFiltered->get()[0]); // @phpstan-ignore argument.type +$functionWithNonGenericInputs(int: $arrayOptionFiltered->get()[1]); // --------------------------------------------------------------------------------------------------------------------- From b253c9de4f4fee7c73fd50125504fc6bb5359fc6 Mon Sep 17 00:00:00 2001 From: Petr Knap <8299754+petrknap@users.noreply.github.com> Date: Sun, 11 Jan 2026 08:53:51 +0100 Subject: [PATCH 7/9] wip --- src/Exception/TypedOptionalException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Exception/TypedOptionalException.php b/src/Exception/TypedOptionalException.php index 3709716..6338cde 100644 --- a/src/Exception/TypedOptionalException.php +++ b/src/Exception/TypedOptionalException.php @@ -5,7 +5,7 @@ namespace PetrKnap\Optional\Exception; /** - * @todo BC extend {@see Exception}, not {@see OptionalException} + * @todo extend {@see Exception}, not {@see OptionalException} */ interface TypedOptionalException extends OptionalException { From 540bb92ac1b4b82073e2f75f70d5c257567232ea Mon Sep 17 00:00:00 2001 From: Petr Knap <8299754+petrknap@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:56:18 +0100 Subject: [PATCH 8/9] wip --- README.md | 1 + phpdoc_check.php | 4 ++-- src/JavaSe8/Optional.php | 4 ++-- src/Optional.php | 21 +++++++++++---------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7eae3d9..766145b 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ namespace PetrKnap\Optional; * @extends OptionalObject */ class YourOptional extends OptionalObject { + use NonGenericOptional; protected static function getInstanceOf(): string { return Some\DataObject::class; } diff --git a/phpdoc_check.php b/phpdoc_check.php index 7e86cb8..bff902a 100644 --- a/phpdoc_check.php +++ b/phpdoc_check.php @@ -123,7 +123,7 @@ $intOptionMapped = $stringOption->map(static fn (string $value): int => 0); $intOptionMappedFiltered = $intOptionMapped->filter(static fn (int $value): bool => true); $intOptionMapped->filter(static fn (string $value): bool => true); // @phpstan-ignore argument.type -$intOptionFlatMapped = $stringOption->flatMap(static fn (string $value): OptionalInt => OptionalInt::of(0)); +$intOptionFlatMapped = $stringOption->flatMap(static fn (string $value): OptionalInt => OptionalInt::of(0), empty: OptionalInt::empty()); $intOptionFlatMappedFiltered = $intOptionFlatMapped->filter(static fn (int $value): bool => true); $intOptionFlatMapped->filter(static fn (string $value): bool => true); // @phpstan-ignore argument.type @@ -132,7 +132,7 @@ $functionWithNonGenericInputOptions(int: $intOptionMappedFiltered); // @phpstan-ignore argument.type $functionWithNonGenericInputs(int: $intOptionMappedFiltered->get()); $functionWithGenericInputOptions(int: $intOptionFlatMappedFiltered); -$functionWithNonGenericInputOptions(int: $intOptionFlatMappedFiltered); // @phpstan-ignore argument.type +$functionWithNonGenericInputOptions(int: $intOptionFlatMappedFiltered); $functionWithNonGenericInputs(int: $intOptionFlatMappedFiltered->get()); // --------------------------------------------------------------------------------------------------------------------- diff --git a/src/JavaSe8/Optional.php b/src/JavaSe8/Optional.php index 8e50d1f..bb7ea20 100644 --- a/src/JavaSe8/Optional.php +++ b/src/JavaSe8/Optional.php @@ -61,7 +61,7 @@ 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}. * - * @template U of mixed + * @template U of mixed type of non-null value * * @param callable(T): self $mapper * @@ -93,7 +93,7 @@ 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. * - * @template U of mixed + * @template U of mixed type of non-null value * * @param callable(T): U $mapper * diff --git a/src/Optional.php b/src/Optional.php index 00c44d3..064c54d 100644 --- a/src/Optional.php +++ b/src/Optional.php @@ -172,24 +172,25 @@ public function filter(callable $predicate): self } /** - * @template U of mixed + * @template U of mixed type of non-null value + * @template V of Optional * - * @param callable(T): JavaSe8\Optional $mapper + * @param ($empty is null ? callable(T): JavaSe8\Optional : callable(T): JavaSe8\Optional) $mapper + * @param V|null $empty * - * @return Optional + * @return ($empty is null ? Optional : V) * - * @note do not use {@see self} to indicate that it uses {@see Optional}, not {@see static} + * @note do not use {@see self}/{@see static}, it maps itself to another {@see Optional} */ - public function flatMap(callable $mapper): Optional + public function flatMap(callable $mapper, Optional|null $empty = null): Optional { if ($this->value === null) { - /** @var Optional */ - return Optional::empty(); + /** @var V */ + return $empty ?? Optional::empty(); } /** @var JavaSe8\Optional|"" $mapped */ $mapped = $mapper($this->value); - /** @var Optional */ return match (true) { $mapped instanceof Optional => $mapped, $mapped instanceof JavaSe8\Optional => $mapped->isPresent() ? Optional::of($mapped->get()) : Optional::empty(), @@ -220,13 +221,13 @@ public function isPresent(): bool } /** - * @template U of mixed + * @template U of mixed type of non-null value * * @param callable(T): U $mapper * * @return Optional * - * @note do not use {@see self} to indicate that it uses {@see Optional}, not {@see static} + * @note do not use {@see self}/{@see static}, it maps itself to another {@see Optional} */ public function map(callable $mapper): Optional { From 10e72337b4aac6c6d58d3d84f95206b228e7709e Mon Sep 17 00:00:00 2001 From: Petr Knap <8299754+petrknap@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:47:23 +0100 Subject: [PATCH 9/9] wip --- src/Optional.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Optional.php b/src/Optional.php index 064c54d..6b5d0bd 100644 --- a/src/Optional.php +++ b/src/Optional.php @@ -175,7 +175,7 @@ public function filter(callable $predicate): self * @template U of mixed type of non-null value * @template V of Optional * - * @param ($empty is null ? callable(T): JavaSe8\Optional : callable(T): JavaSe8\Optional) $mapper + * @param ($empty is null ? callable(T): JavaSe8\Optional : callable(T): V) $mapper * @param V|null $empty * * @return ($empty is null ? Optional : V)