diff --git a/README.md b/README.md index 1cbd00c..c3c0456 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ It is an easy way to make sure that everyone has to check if they have (not) rec ```php use PetrKnap\Optional\Optional; -/** @var Optional $stringOption */ $stringOption = Optional::of('data'); if ($stringOption->isPresent()) { echo $stringOption->get(); @@ -30,48 +29,24 @@ Optional::ofFalsable(tmpfile())->ifPresent(function ($tmpFile): void { ``` -## Creating your own typed optional +## Creating your own optional -The library provides specialized traits to help you implement strictly typed wrappers with full static analysis support. +This library provides [`OptionalOverrides` trait](src/OptionalOverrides.php) to help you implement your own wrapper with full static analysis support (see [`Some\Optional` class](./tests/Some/Optional.php)): -### Quick Overview - -1. **[`NonGenericOptional` trait](./src/NonGenericOptional.php)** for fixed specific types like `bool` or `string` (see [`OptionalString` class](./tests/Some/OptionalString.php)) - ```php - use PetrKnap\Optional\Optional; - use PetrKnap\Optional\NonGenericOptional; - - /** @extends Optional */ - final class OptionalBool extends Optional - { - use NonGenericOptional; - - protected static function isSupported(mixed $value): bool - { - return is_bool($value); - } - } - ``` -2. **[`GenericOptional` trait](./src/GenericOptional.php)** for generic structures like `array` (see [`OptionalArray` class](./tests/Some/OptionalArray.php)) -3. **[`AbstractOptional` trait](./src/AbstractOptional.php)** for extendable factory optionals (use this when you need a base class for other typed optionals, see [`OptionalObject` class](./tests/Some/OptionalObject.php)) - -### Type registration - -You can register your custom typed optionals into [`TypedOptional` helper](./src/TypedOptional.php). -Once registered, calling the base [`Optional` class](./src/Optional.php) will automatically look up and return the correct specific subclass if the value matches its criteria. ```php -use PetrKnap\Optional\TypedOptional; use PetrKnap\Optional\Optional; - -TypedOptional::register(OptionalBool::class); - -printf( - "Class name of optional which holds `true` is `%s`.\n", - get_class(Optional::of(true)), -); -``` -``` -Class name of optional which holds `true` is `OptionalBool`. +use PetrKnap\Optional\OptionalOverrides; + +/** + * @template-covariant T of mixed type of non-null value + * + * @extends Optional + */ +final class MyOptional extends Optional +{ + /** @use OptionalOverrides */ + use OptionalOverrides; +} ``` --- diff --git a/composer.json b/composer.json index ca4c88c..3bfce7f 100755 --- a/composer.json +++ b/composer.json @@ -7,10 +7,7 @@ "autoload-dev": { "psr-4": { "PetrKnap\\Optional\\": "tests" - }, - "files": [ - "tests/Some/typed_optionals.php" - ] + } }, "config": { "allow-plugins": false, diff --git a/phpdoc_check.php b/phpdoc_check.php index 3ca5f1d..c97926b 100644 --- a/phpdoc_check.php +++ b/phpdoc_check.php @@ -7,33 +7,25 @@ declare(strict_types=1); use PetrKnap\Optional\Optional; -use PetrKnap\Optional\Some\DataObject; -use PetrKnap\Optional\Some\OptionalArray; -use PetrKnap\Optional\Some\OptionalObject; -use PetrKnap\Optional\Some\OptionalString; +use PetrKnap\Optional\Some; $check = (new class { /** - * @param Optional $object + * @param Optional> $abstract + * @param Some\Optional> $concrete */ - public function covariantInputOption(Optional $object): void {} + public function covariantInputOptions(Optional $abstract, Some\Optional $concrete): void {} /** - * @param Optional|null $string - * @param Optional|null $object - * @param Optional>|null $array + * @param Optional $abstract + * @param Some\Optional $concrete */ - public function genericInputOptions(Optional $string = null, Optional $object = null, Optional $array = null): void {} - /** - * @param OptionalObject|null $object - * @param OptionalArray>|null $array - */ - public function nonGenericInputOptions(OptionalString $string = null, OptionalObject $object = null, OptionalArray $array = null): void {} - public function nonGenericInputs(string $string = '', object $object = new stdClass()): void {} + public function genericInputOptions(Optional $abstract = null, Some\Optional $concrete = null): void {} + public function rawInputs(int $int = 0, string $string = ''): void {} }); // --------------------------------------------------------------------------------------------------------------------- -// Call all factories +// Factories Optional::of(''); try { Optional::of(null); @@ -54,27 +46,32 @@ public function nonGenericInputs(string $string = '', object $object = new stdCl null; // @phpstan-ignore deadCode.unreachable } catch(Throwable) {} Optional::ofSingle([]); - -// Call main typed factories -OptionalArray::of([]); -OptionalArray::of(null); // @phpstan-ignore argument.type, argument.templateType -OptionalArray::of(false); // @phpstan-ignore argument.type, argument.templateType -OptionalArray::ofNullable([]); -OptionalArray::ofNullable(null); // @phpstan-ignore argument.templateType -OptionalArray::ofNullable(false); // @phpstan-ignore argument.type, argument.templateType -OptionalString::of(''); -OptionalString::of(null); // @phpstan-ignore argument.type -OptionalString::of(false); // @phpstan-ignore argument.type -OptionalString::ofNullable(''); -OptionalString::ofNullable(null); -OptionalString::ofNullable(false); // @phpstan-ignore argument.type +Some\Optional::of(''); +try { + Some\Optional::of(null); + null; // @phpstan-ignore deadCode.unreachable +} catch(Throwable) {} +Some\Optional::ofFalsable(''); +try { + Some\Optional::ofFalsable(null); + null; // @phpstan-ignore deadCode.unreachable +} catch(Throwable) {} +Some\Optional::ofFalsable(false); +Some\Optional::ofNullable(''); +Some\Optional::ofNullable(null); +Some\Optional::ofNullable(false); +Some\Optional::ofSingle(['']); +try { + Some\Optional::ofSingle([null]); + null; // @phpstan-ignore deadCode.unreachable +} catch(Throwable) {} +Some\Optional::ofSingle([]); // Check return types of factory `of` -$stringOption = OptionalString::of(''); -$objectOption = OptionalObject::of(new stdClass()); -$arrayOption = OptionalArray::of(['', new stdClass()]); -$check->genericInputOptions($stringOption, $objectOption, $arrayOption); -$check->nonGenericInputOptions($stringOption, $objectOption, $arrayOption); +$check->genericInputOptions( + abstract: Optional::of(''), + concrete: Some\Optional::of(''), +); // --------------------------------------------------------------------------------------------------------------------- @@ -96,121 +93,101 @@ public function nonGenericInputs(string $string = '', object $object = new stdCl // --------------------------------------------------------------------------------------------------------------------- -// 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 -$check->genericInputOptions(string: $stringOption); -$check->nonGenericInputOptions(string: $stringOption); // @phpstan-ignore argument.type -$check->nonGenericInputs(string: $stringOption->get()); -$check->nonGenericInputs(string: $stringOption->orElseThrow()); -$check->nonGenericInputs(string: $stringOptionOrElse); -$check->nonGenericInputs(string: $stringOptionOrElseNull); // @phpstan-ignore argument.type -$check->nonGenericInputs(string: $stringOptionOrElseNull ?? ''); -$check->nonGenericInputs(string: $stringOptionOrElseGet); - -// Re-map generic option & call filter on it to check new generic -$objectOptionMapped = $stringOption->map(static fn (string $value): object => new DataObject($value)); -$objectOptionMappedFiltered = $objectOptionMapped->filter(static fn (object $value): bool => true); -$objectOptionMapped->filter(static fn (string $value): bool => true); // @phpstan-ignore argument.type -$objectOptionFlatMapped = $stringOption->flatMap(static fn (string $value): Optional => Optional::of(new DataObject($value))); -$objectOptionFlatMappedFiltered = $objectOptionFlatMapped->filter(static fn (object $value): bool => true); -$objectOptionFlatMapped->filter(static fn (string $value): bool => true); // @phpstan-ignore argument.type - -// Use re-mapped filtered options as input for functions -$check->genericInputOptions(object: $objectOptionMappedFiltered); -$check->nonGenericInputOptions(object: $objectOptionMappedFiltered); // @phpstan-ignore argument.type -$check->nonGenericInputs(object: $objectOptionMappedFiltered->get()); -$check->genericInputOptions(object: $objectOptionFlatMappedFiltered); -$check->nonGenericInputOptions(object: $objectOptionFlatMappedFiltered); // @phpstan-ignore argument.type -$check->nonGenericInputs(object: $objectOptionFlatMappedFiltered->get()); - -// --------------------------------------------------------------------------------------------------------------------- - -// Create non-generic option -$stringOption = OptionalString::of(''); +// Options +$abstract = Optional::of(0); +$concrete = Some\Optional::of(0); // 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 -$check->genericInputOptions(string: $stringOption); -$check->nonGenericInputOptions(string: $stringOption); -$check->nonGenericInputs(string: $stringOption->get()); -$check->nonGenericInputs(string: $stringOption->orElseThrow()); -$check->nonGenericInputs(string: $stringOptionOrElse); -$check->nonGenericInputs(string: $stringOptionOrElseNull); // @phpstan-ignore argument.type -$check->nonGenericInputs(string: $stringOptionOrElseNull ?? ''); -$check->nonGenericInputs(string: $stringOptionOrElseGet); - -// Re-map typed option & call filter on it to check new generic -$objectOptionMapped = $stringOption->map(static fn (string $value): object => new DataObject($value)); -$objectOptionMappedFiltered = $objectOptionMapped->filter(static fn (object $value): bool => true); -$objectOptionMapped->filter(static fn (string $value): bool => true); // @phpstan-ignore argument.type -$objectOptionFlatMapped = $stringOption->flatMap(static fn (string $value): OptionalObject => OptionalObject::of(new DataObject($value)), empty: OptionalObject::empty()); -$objectOptionFlatMappedFiltered = $objectOptionFlatMapped->filter(static fn (object $value): bool => true); -$objectOptionFlatMapped->filter(static fn (string $value): bool => true); // @phpstan-ignore argument.type - -// Use re-mapped filtered options as input for functions -$check->genericInputOptions(object: $objectOptionMappedFiltered); -$check->nonGenericInputOptions(object: $objectOptionMappedFiltered); // @phpstan-ignore argument.type -$check->nonGenericInputs(object: $objectOptionMappedFiltered->get()); -$check->genericInputOptions(object: $objectOptionFlatMappedFiltered); -$check->nonGenericInputOptions(object: $objectOptionFlatMappedFiltered); -$check->nonGenericInputs(object: $objectOptionFlatMappedFiltered->get()); +// - filter +$abstract->filter(static fn (int $value): bool => true); +$abstract->filter(static fn (string $value): bool => true); // @phpstan-ignore argument.type +$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 +// - 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 +// - orElse +$abstractOrElse = $abstract->orElse(0); +$abstractOrElseNull = $abstract->orElse(null); +$abstract->orElse(''); // @phpstan-ignore argument.type +$concreteOrElse = $concrete->orElse(0); +$concreteOrElseNull = $concrete->orElse(null); +$concrete->orElse(''); // @phpstan-ignore argument.type +// - orElseGet +$abstractOrElseGet = $abstract->orElseGet(static fn (): int => 0); +$abstract->orElseGet(static fn (): string => ''); // @phpstan-ignore argument.type +$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()); +// - 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()); // --------------------------------------------------------------------------------------------------------------------- // Create complexly generic option -/** @var array{string, object} $array */ -$array = ['', new DataObject()]; -$arrayOption = OptionalArray::of($array); +/** @var array{int, string} $array */ +$array = [0, '']; +$arrayOption = Optional::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', new stdClass()]); -$arrayOption->orElse([new stdClass(), '1']); // @phpstan-ignore argument.type +$arrayOption->orElse([0, '']); +$arrayOption->orElse(['', 0]); // @phpstan-ignore argument.type // Use filtered complexly generic option as input for function -$check->nonGenericInputs(string: $arrayOptionFiltered->get()[0]); -$check->nonGenericInputs(string: $arrayOptionFiltered->get()[1]); // @phpstan-ignore argument.type -$check->nonGenericInputs(object: $arrayOptionFiltered->get()[0]); // @phpstan-ignore argument.type -$check->nonGenericInputs(object: $arrayOptionFiltered->get()[1]); +$check->rawInputs(int: $arrayOptionFiltered->get()[0]); +$check->rawInputs(string: $arrayOptionFiltered->get()[0]); // @phpstan-ignore argument.type +$check->rawInputs(int: $arrayOptionFiltered->get()[1]); // @phpstan-ignore argument.type +$check->rawInputs(string: $arrayOptionFiltered->get()[1]); // --------------------------------------------------------------------------------------------------------------------- -// Create covariant option -$stdClassOption = Optional::of(new stdClass()); +// Create covariant options +$stdClassArrayAbstractOption = Optional::of([new stdClass()]); +$stdClassArrayConcreteOption = Some\Optional::of([new stdClass()]); -// Pass covariant option as input argument -$check->covariantInputOption(object: $stdClassOption); +// Pass covariant options as input argument +$check->covariantInputOptions( + abstract: $stdClassArrayAbstractOption, + concrete: $stdClassArrayConcreteOption, +); // --------------------------------------------------------------------------------------------------------------------- diff --git a/src/AbstractOptional.php b/src/AbstractOptional.php deleted file mode 100644 index d3f38b6..0000000 --- a/src/AbstractOptional.php +++ /dev/null @@ -1,51 +0,0 @@ - */ - use GenericOptional; - - /** - * @template U of T - * - * @param U|null $value - * - * @return self - */ - public static function ofNullable(mixed $value): self // @phpstan-ignore generics.notSubtype - { - if (static::class === self::class) { - if ($value !== null) { - try { - /** @var self */ // @phpstan-ignore generics.notSubtype - return TypedOptional::of($value, static::class); - } catch (Exception\CouldNotFindTypedOptionalForValue) { - } - } - /** @var self */ - return self::createInstance($value); // @phpstan-ignore generics.notSubtype, argument.type, argument.templateType - } - /** @var self */ - return parent::ofNullable($value); // @phpstan-ignore generics.notSubtype, argument.type, argument.templateType - } - - /** - * @template U of T - * - * @param U|null $value - * - * @return self - */ - abstract protected static function createInstance(mixed $value): self; -} diff --git a/src/GenericOptional.php b/src/GenericOptional.php deleted file mode 100644 index 405f53f..0000000 --- a/src/GenericOptional.php +++ /dev/null @@ -1,85 +0,0 @@ - - */ - 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 // @phpstan-ignore generics.notSubtype - { - /** @var self */ - return parent::of($value); // @phpstan-ignore generics.notSubtype, argument.type, argument.templateType - } - - /** - * @template U of T - * - * @param U|false $value - * - * @return self - */ - public static function ofFalsable(mixed $value): self // @phpstan-ignore generics.notSubtype - { - /** @var self */ - return parent::ofFalsable($value); // @phpstan-ignore generics.notSubtype, argument.type, argument.templateType - } - - /** - * @template U of T - * - * @param U|null $value - * - * @return self - */ - public static function ofNullable(mixed $value): self // @phpstan-ignore generics.notSubtype - { - /** @var self */ - return parent::ofNullable($value); // @phpstan-ignore generics.notSubtype, argument.type, argument.templateType - } - - /** - * @template U of T - * - * @param iterable $value - * - * @return self - */ - public static function ofSingle(iterable $value): self // @phpstan-ignore generics.notSubtype - { - /** @var self */ - return parent::ofSingle($value); // @phpstan-ignore generics.notSubtype, argument.type, argument.templateType - } - - /** - * @return self - */ - public function filter(callable $predicate): self // @phpstan-ignore generics.notSubtype - { - /** @var self */ - return parent::filter($predicate); // @phpstan-ignore generics.notSubtype, argument.type, argument.templateType - } -} diff --git a/src/NonGenericOptional.php b/src/NonGenericOptional.php deleted file mode 100644 index a4057cd..0000000 --- a/src/NonGenericOptional.php +++ /dev/null @@ -1,49 +0,0 @@ -value !== null && !@static::isSupported($this->value)) { - throw new InvalidArgumentException('Value is not supported.'); - } } /** @@ -84,19 +81,8 @@ public static function ofFalsable(mixed $value): self public static function ofNullable(mixed $value): self { if (static::class === Optional::class) { - if ($value !== null) { - try { - /** @var self */ - return TypedOptional::of($value, Optional::class); - } catch (Exception\CouldNotFindTypedOptionalForValue) { - } - } /** @var self */ return new class ($value) extends Optional { - protected static function isSupported(mixed $value): bool - { - return true; - } }; } return new static($value); @@ -221,11 +207,9 @@ public function isPresent(): bool * * @param callable(T): U $mapper * - * @return Optional - * - * @note do not use {@see self}/{@see static}, it maps itself to another {@see Optional} + * @return self */ - public function map(callable $mapper): Optional + public function map(callable $mapper): self { /** @var callable(T): Optional $flatMapper */ $flatMapper = static function (mixed $value) use ($mapper): Optional { @@ -246,11 +230,8 @@ public function orElseGet(callable $otherSupplier): mixed if ($this->value !== null) { return $this->value; } - /** @var T|null $other */ - $other = $otherSupplier(); - return $other === null || static::isSupported($other) - ? $other - : throw new InvalidArgumentException('Other supplier must return supported other.'); + /** @var T|null */ + return $otherSupplier(); } /** @@ -294,11 +275,6 @@ public function orElseThrow( }); } - /** - * @param mixed $value not null - */ - abstract protected static function isSupported(mixed $value): bool; - private static function triggerNotice(string $message): void { trigger_error( diff --git a/src/OptionalOverrides.php b/src/OptionalOverrides.php new file mode 100644 index 0000000..7d02609 --- /dev/null +++ b/src/OptionalOverrides.php @@ -0,0 +1,109 @@ + + */ + public static function empty(): self + { + /** @var self */ + return parent::empty(); + } + + /** + * @template U of T + * + * @param U $value + * + * @return (U is null ? never : self) + * + * @throws InvalidArgumentException + */ + public static function of(mixed $value): self + { + /** @var self */ + return parent::of($value); + } + + /** + * In many cases a failure returns `false`, this method serves as a factory for them. + * + * @template U of T + * + * @param U|false $value + * + * @return (U is null ? never : self) + * + * @throws InvalidArgumentException + */ + 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); + } + + /** + * In many cases you will receive an `iterable` of single value, this method serves as a factory for them. + * + * @template U of T + * + * @param iterable $value + * + * @return (U is null ? ($value is empty ? self : never) : self) + * + * @throws InvalidArgumentException + */ + 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); + } + + /** + * @template U of mixed type of non-null value + * + * @param callable(T): U $mapper + * + * @return self + */ + public function map(callable $callback): self + { + return self::ofNullable(parent::map($callback)->orElse(null)); + } +} diff --git a/src/TypedOptional.php b/src/TypedOptional.php deleted file mode 100644 index 66bbce6..0000000 --- a/src/TypedOptional.php +++ /dev/null @@ -1,53 +0,0 @@ - stored in reverse order, see {@see self::register()} */ - private static array $typedOptionals = []; - - /** - * @internal use {@see Optional::of()} - * - * @template T of mixed type of non-null value - * - * @param T $value - * @param class-string $subclassOf - * - * @return Optional - * - * @throws Exception\CouldNotFindTypedOptionalForValue - */ - public static function of(mixed $value, string $subclassOf): Optional - { - /** @var class-string> $typedOptional */ - foreach (self::$typedOptionals as $typedOptional) { - if ($typedOptional === $subclassOf || !is_a($typedOptional, $subclassOf, allow_string: true)) { - continue; - } - try { - return $typedOptional::of($value); - } catch (InvalidArgumentException) { - } - } - throw new Exception\CouldNotFindTypedOptionalForValue($value); - } - - /** - * @param class-string $typedOptionalClassName - * - * @throws Exception\CouldNotRegisterNonOptional - */ - public static function register(string $typedOptionalClassName): void - { - if (!is_a($typedOptionalClassName, Optional::class, allow_string: true)) { - throw new Exception\CouldNotRegisterNonOptional($typedOptionalClassName); - } - array_unshift(self::$typedOptionals, $typedOptionalClassName); - } -} diff --git a/tests/IdeTest.php b/tests/IdeTest.php index 699f87a..c349275 100644 --- a/tests/IdeTest.php +++ b/tests/IdeTest.php @@ -16,19 +16,19 @@ public function testCheckThisInYourIde(): void self::expectNotToPerformAssertions(); $instance = new Some\DataObject(); - $optional = Some\OptionalObject\OptionalDataObject::of($instance); + $option = Optional::of($instance); - if ($optional->isPresent()) { - $optional->get()->tryIt(); # <--- HERE + if ($option->isPresent()) { + $option->get()->tryIt(); # <--- HERE } Optional::empty()->orElse($instance)->tryIt(); # <--- HERE Optional::empty()->orElseGet(static fn (): Some\DataObject => $instance)->tryIt(); # <--- HERE - $optional->orElseThrow()->tryIt(); # <--- HERE + $option->orElseThrow()->tryIt(); # <--- HERE - $optional->filter(static fn (): bool => true)->orElseThrow()->tryIt(); # <--- HERE + $option->filter(static fn (): bool => true)->orElseThrow()->tryIt(); # <--- HERE - Optional::of(0)->flatMap(static fn (): Some\OptionalObject\OptionalDataObject => $optional)->orElseThrow()->tryIt(); # <--- HERE + Optional::of(0)->flatMap(static fn (): Optional => $option)->orElseThrow()->tryIt(); # <--- HERE Optional::of(0)->map(static fn (): Some\DataObject => $instance)->orElseThrow()->tryIt(); # <--- HERE } } diff --git a/tests/OptionalOverridesTest.php b/tests/OptionalOverridesTest.php new file mode 100644 index 0000000..ba6b05b --- /dev/null +++ b/tests/OptionalOverridesTest.php @@ -0,0 +1,53 @@ +getMethod($methodName)->getReturnType(); + self::assertInstanceOf(ReflectionNamedType::class, $methodReturnType); + self::assertSame('self', $methodReturnType->getName()); + } + + public static function dataOverridesReturnFromParentToChild(): iterable + { + $optional = new ReflectionClass(Optional::class); + $customOptional = new ReflectionClass(OptionalOverrides::class); + + foreach ($optional->getMethods() as $method) { + $methodReturnType = $method->getReturnType(); + if ($methodReturnType instanceof ReflectionNamedType && $methodReturnType->getName() === 'self') { + yield $method->getName() => [$customOptional, $method->getName()]; + } + } + } + + #[DataProvider('dataHasSameDocComment')] + public function testHasSameDocComment(string $optional, string $customOptional): void + { + self::assertSame($optional, $customOptional); + } + + public static function dataHasSameDocComment(): iterable + { + $optional = new ReflectionClass(Optional::class); + $customOptional = new ReflectionClass(OptionalOverrides::class); + + foreach ($customOptional->getMethods() as $customOptionalMethod) { + yield $customOptionalMethod->getName() => [ + $optional->getMethod($customOptionalMethod->getName())->getDocComment(), + $customOptionalMethod->getDocComment(), + ]; + } + } +} diff --git a/tests/OptionalTest.php b/tests/OptionalTest.php index 041d5fc..1fafdd8 100644 --- a/tests/OptionalTest.php +++ b/tests/OptionalTest.php @@ -36,9 +36,7 @@ public static function dataFactoriesReturnsSelf(): array { return [ Optional::class => [Optional::class, self::VALUE], - AbstractOptional::class => [Some\OptionalObject::class, new stdClass()], - GenericOptional::class => [Some\OptionalArray::class, []], - NonGenericOptional::class => [Some\OptionalString::class, ''], + OptionalOverrides::class => [Some\Optional::class, self::VALUE], ]; } @@ -168,15 +166,10 @@ public static function dataMethodEqualsWorks(): array 'optional (equal object)' => Optional::of($equalObject), 'optional (other object)' => Optional::of($otherObject), 'optional (different object)' => Optional::of($differentObject), - // registered typed optional - 'registered typed optional (empty)' => Some\OptionalString::empty(), - 'registered typed optional (value)' => Some\OptionalString::of(self::VALUE), - 'registered typed optional (other value)' => Some\OptionalString::of(self::OTHER), - // unregistered typed optional - 'unregistered typed optional (empty)' => Some\OptionalObject\OptionalDataObject::empty(), - 'unregistered typed optional (same object)' => Some\OptionalObject\OptionalDataObject::of($sameObject), - 'unregistered typed optional (equal object)' => Some\OptionalObject\OptionalDataObject::of($equalObject), - 'unregistered typed optional (other object)' => Some\OptionalObject\OptionalDataObject::of($otherObject), + // some optionals + 'some optional (empty)' => Some\Optional::empty(), + 'some optional (value)' => Some\Optional::of(self::VALUE), + 'some optional (other value)' => Some\Optional::of(self::OTHER), ]; foreach ($set as $name => $value) { $shouldEqual = in_array($name, $equals); @@ -197,51 +190,34 @@ public static function dataMethodEqualsWorks(): array ...$set( 'optional (empty)', // ~ typeless (empty) Optional::empty(), - ['optional (empty)', 'registered typed optional (empty)', 'unregistered typed optional (empty)'], + ['optional (empty)', 'some optional (empty)'], ), ...$set( - 'optional (value)', // = registered typed optional (value) + 'optional (value)', // ~ typeless (value) Optional::of(self::VALUE), - ['optional (value)', 'registered typed optional (value)'], + ['optional (value)', 'some optional (value)'], ), ...$set( - 'optional (object)', // ~ unregistered typed optional (object) + 'optional (object)', Optional::of($object), - ['optional (same object)', 'optional (equal object)', 'unregistered typed optional (same object)', 'unregistered typed optional (equal object)'], - false, + ['optional (same object)', 'optional (equal object)'], + byReference: false, ), ...$set( - 'optional (object)', // ~ unregistered typed optional (object) + 'optional (object)', Optional::of($object), - ['optional (same object)', 'unregistered typed optional (same object)'], - true, + ['optional (same object)'], + byReference: true, ), ...$set( - 'registered typed optional (empty)', - Some\OptionalString::empty(), - ['optional (empty)', 'registered typed optional (empty)', 'unregistered typed optional (empty)'], + 'some optional (empty)', + Some\Optional::empty(), + ['optional (empty)', 'some optional (empty)'], ), ...$set( - 'registered typed optional (value)', // = optional (value) - Some\OptionalString::of(self::VALUE), - ['optional (value)', 'registered typed optional (value)'], - ), - ...$set( - 'unregistered typed optional (empty)', - Some\OptionalObject\OptionalDataObject::empty(), - ['optional (empty)', 'registered typed optional (empty)', 'unregistered typed optional (empty)'], - ), - ...$set( - 'unregistered typed optional (object)', - Some\OptionalObject\OptionalDataObject::of($object), - ['optional (same object)', 'optional (equal object)', 'unregistered typed optional (same object)', 'unregistered typed optional (equal object)'], - false, - ), - ...$set( - 'unregistered typed optional (object)', - Some\OptionalObject\OptionalDataObject::of($object), - ['optional (same object)', 'unregistered typed optional (same object)'], - true, + 'some optional (value)', + Some\Optional::of(self::VALUE), + ['optional (value)', 'some optional (value)'], ), ]; } @@ -335,7 +311,7 @@ public static function dataMethodIsPresentWorks(): array } #[DataProvider('dataMethodMapWorks')] - public function testMethodMapWorks(Optional $optional, mixed $expectedResult): void + public function testMethodMapWorks(Optional $optional, Optional $expectedResult): void { self::assertTrue($expectedResult->equals($optional->map(fn (string $v): string => $v . 'x'))); } @@ -343,7 +319,7 @@ public function testMethodMapWorks(Optional $optional, mixed $expectedResult): v public static function dataMethodMapWorks(): array { return self::makeDataSet([ - [Some\OptionalString::of(self::VALUE . 'x')], + [Some\Optional::of(self::VALUE . 'x')], [Optional::empty()], ]); } @@ -360,7 +336,7 @@ public static function dataMethodOrElseWorks(): array [self::OTHER, self::VALUE], [self::OTHER, self::OTHER], ]) + [ - 'typed null' => [Some\OptionalString::empty(), null, null], + 'typed null' => [Some\Optional::empty(), null, null], ]; } @@ -376,7 +352,7 @@ public static function dataMethodOrElseGetWorks(): array [self::OTHER, self::VALUE], [self::OTHER, self::OTHER], ]) + [ - 'typed null' => [Some\OptionalString::empty(), null, null], + 'typed null' => [Some\Optional::empty(), null, null], ]; } diff --git a/tests/ReadmeTest.php b/tests/ReadmeTest.php index 7af6cc3..e6dcca0 100644 --- a/tests/ReadmeTest.php +++ b/tests/ReadmeTest.php @@ -21,8 +21,7 @@ public static function getExpectedOutputsOfPhpExamples(): iterable { return [ 'Basic usage' => 'data', - 'Creating your own typed optional / Quick Overview / NonGenericOptional' => '', - 'Creating your own typed optional / Type registration' => self::OUTPUT_IN_MARKDOWN, + 'Creating your own typed optional' => '', ]; } } diff --git a/tests/Some/Optional.php b/tests/Some/Optional.php new file mode 100644 index 0000000..b10faa0 --- /dev/null +++ b/tests/Some/Optional.php @@ -0,0 +1,19 @@ + + */ +final class Optional extends Base +{ + /** @use OptionalOverrides */ + use OptionalOverrides; +} diff --git a/tests/Some/OptionalArray.php b/tests/Some/OptionalArray.php deleted file mode 100644 index fd757ae..0000000 --- a/tests/Some/OptionalArray.php +++ /dev/null @@ -1,26 +0,0 @@ - - */ -final class OptionalArray extends Optional -{ - /** @use GenericOptional */ - use GenericOptional; - - protected static function isSupported(mixed $value): bool - { - return is_array($value); - } -} diff --git a/tests/Some/OptionalObject.php b/tests/Some/OptionalObject.php deleted file mode 100644 index d86714b..0000000 --- a/tests/Some/OptionalObject.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ -abstract class OptionalObject extends Optional -{ - /** @use AbstractOptional */ - use AbstractOptional; - - /** - * @param T|null $value - * - * @return self - */ - protected static function createInstance(mixed $value): self - { - /** @var self */ - return new class ($value) extends OptionalObject { - protected static function isInstanceOfStatic(object $obj): bool - { - return $obj instanceof OptionalObject; - } - - protected static function isSupported(mixed $value): bool - { - return is_object($value); - } - }; - } -} diff --git a/tests/Some/OptionalObject/OptionalDataObject.php b/tests/Some/OptionalObject/OptionalDataObject.php deleted file mode 100644 index fe93f38..0000000 --- a/tests/Some/OptionalObject/OptionalDataObject.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ -final class OptionalDataObject extends OptionalObject -{ - use NonGenericOptional; - - protected static function isSupported(mixed $value): bool - { - return $value instanceof DataObject; - } -} diff --git a/tests/Some/OptionalObject/OptionalStdClass.php b/tests/Some/OptionalObject/OptionalStdClass.php deleted file mode 100644 index 630c140..0000000 --- a/tests/Some/OptionalObject/OptionalStdClass.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ -final class OptionalStdClass extends OptionalObject -{ - use NonGenericOptional; - - protected static function isSupported(mixed $value): bool - { - return $value instanceof stdClass; - } -} diff --git a/tests/Some/OptionalString.php b/tests/Some/OptionalString.php deleted file mode 100644 index 7c5786b..0000000 --- a/tests/Some/OptionalString.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ -final class OptionalString extends Optional -{ - use NonGenericOptional; - - protected static function isSupported(mixed $value): bool - { - return is_string($value); - } -} diff --git a/tests/Some/typed_optionals.php b/tests/Some/typed_optionals.php deleted file mode 100644 index 7d9f03e..0000000 --- a/tests/Some/typed_optionals.php +++ /dev/null @@ -1,14 +0,0 @@ - $optionalClassName - */ - #[DataProvider('dataCouldBeCreated')] - public function testCouldBeCreated(string $optionalClassName, mixed $value): void - { - self::assertInstanceOf($optionalClassName, Optional::of($value)); - self::assertInstanceOf($optionalClassName, Optional::ofNullable($value)); - self::assertInstanceOf($optionalClassName, TypedOptional::of($value, Optional::class)); - } - - public static function dataCouldBeCreated(): array - { - return [ - 'array' => [Some\OptionalArray::class, []], - 'object' => [Some\OptionalObject::class, new stdClass(), ['object(stdClass)']], - 'object(stdClass)' => [Some\OptionalObject\OptionalStdClass::class, new stdClass(), ['object']], - 'string' => [Some\OptionalString::class, ''], - ]; - } - - /** - * @param class-string $optionalClassName - */ - #[DataProvider('dataCouldNotBeCreatedWithWrongType')] - public function testCouldNotBeCreatedWithWrongType(string $optionalClassName, mixed $value): void - { - self::expectException(InvalidArgumentException::class); - $optionalClassName::of($value); - } - - public static function dataCouldNotBeCreatedWithWrongType(): iterable - { - $data = self::dataCouldBeCreated(); - - foreach ($data as $supportedCase => [$optionalClassName, $_, $alsoSupportedCases]) { - foreach ($data as $unsupportedCase => [$_, $value]) { - if (in_array($unsupportedCase, [$supportedCase, ...($alsoSupportedCases ?? [])])) { - continue; - } - yield "({$supportedCase}) {$unsupportedCase}" => [$optionalClassName, $value]; - } - } - } - - public function testTwoEmptiesOfSameTypeAreEqual(): void - { - self::assertTrue(Some\OptionalString::empty()->equals(Some\OptionalString::empty())); - } - - public function testTwoEmptiesOfDifferentTypesAreEqual(): void - { - self::assertTrue(Some\OptionalString::empty()->equals(Some\OptionalArray::empty())); - } -}