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/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..bff902a --- /dev/null +++ b/phpdoc_check.php @@ -0,0 +1,157 @@ + 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), empty: OptionalInt::empty()); +$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); +$functionWithNonGenericInputs(int: $intOptionFlatMappedFiltered->get()); + +// --------------------------------------------------------------------------------------------------------------------- + +// Create complexly generic option +/** @var array{string, int} $array */ +$array = ['', 0]; +$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 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]); + +// --------------------------------------------------------------------------------------------------------------------- diff --git a/src/JavaSe8/Optional.php b/src/JavaSe8/Optional.php index c2b2e8c..bb7ea20 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,13 +53,15 @@ 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}. * - * @template U of mixed + * @template U of mixed type of non-null value * * @param callable(T): self $mapper * @@ -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; @@ -81,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/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) { @@ -94,6 +122,7 @@ public static function ofSingle(iterable $value): static } $option = static::of($v); } + /** @var self */ return $option ?? static::empty(); } @@ -103,6 +132,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 +154,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); @@ -139,24 +172,27 @@ public function filter(callable $predicate): static } /** - * @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): V) $mapper + * @param V|null $empty * - * @return self + * @return ($empty is null ? Optional : V) + * + * @note do not use {@see self}/{@see static}, it maps itself to another {@see Optional} */ - public function flatMap(callable $mapper): self + public function flatMap(callable $mapper, Optional|null $empty = null): Optional { if ($this->value === null) { - /** @var self */ - return Optional::empty(); + /** @var V */ + return $empty ?? Optional::empty(); } - /** @var mixed $mapped */ + /** @var JavaSe8\Optional|"" $mapped */ $mapped = $mapper($this->value); - /** @var self */ 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 . '.'), }; @@ -185,17 +221,19 @@ public function isPresent(): bool } /** - * @template U of mixed + * @template U of mixed type of non-null value * * @param callable(T): U $mapper * - * @return self + * @return Optional + * + * @note do not use {@see self}/{@see static}, it maps itself to another {@see Optional} */ - 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..e93bd45 100644 --- a/src/OptionalArray.php +++ b/src/OptionalArray.php @@ -5,13 +5,82 @@ namespace PetrKnap\Optional; /** - * @template K of array-key - * @template V of mixed + * @template T of array * - * @extends Optional> + * @extends Optional */ final class OptionalArray extends Optional { + /** + * @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 + { + /** @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 { 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;