Skip to content

Commit

Permalink
feat: implemented typed optionals
Browse files Browse the repository at this point in the history
  • Loading branch information
petrknap committed May 12, 2024
1 parent 8c41033 commit e61b714
Show file tree
Hide file tree
Showing 16 changed files with 455 additions and 130 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ It is an easy way to make sure that everyone has to check if they have (not) rec
```php
namespace PetrKnap\Optional;

/** @var Optinal<string> $optionalString */
$optionalString = Optional::of('value');
$optionalString = OptionalString::of('value');

echo $optionalString->isPresent() ? $optionalString->get() : 'empty';
echo $optionalString->orElse('empty');
Expand Down
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"allow-plugins": false,
"sort-packages": true
},
"conflict": {
"petrknap/shorts": "<2.1.1"
},
"description": "Optional (like in Java Platform SE 8 but in PHP)",
"funding": [
{
Expand All @@ -28,11 +31,11 @@
"license": "LGPL-3.0-or-later",
"name": "petrknap/optional",
"require": {
"php": ">=8.1"
"php": ">=8.1",
"petrknap/shorts": "^2.1"
},
"require-dev": {
"nunomaduro/phpinsights": "^2.11",
"petrknap/shorts": "^2.1",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.7"
Expand Down
141 changes: 141 additions & 0 deletions src/AbstractOptional.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

declare(strict_types=1);

namespace PetrKnap\Optional;

use InvalidArgumentException;
use Throwable;

/**
* @template T of mixed type of non-null value
*
* @see https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html
*/
abstract class AbstractOptional
{
private bool|null $wasPresent = null;

/**
* @deprecated will be changed to protected - use {@see self::ofNullable()}/{@see self::of()}/{@see self::empty()}
*
* @param T|null $value
*/
final public function __construct(
protected readonly mixed $value,
) {
if ($this->value !== null && !static::isSupported($this->value)) {
throw new InvalidArgumentException('Value is not supported.');
}
}

public static function empty(): static
{
return new static(null);
}

/**
* @param T $value
*/
public static function of(mixed $value): static
{
return $value !== null ? new static($value) : throw new InvalidArgumentException('Value must not be null.');
}

/**
* @param T|null $value
*/
public static function ofNullable(mixed $value): static
{
return new static($value);
}

public function equals(mixed $obj): bool
{
if ($obj instanceof static) {
$obj = $obj->isPresent() ? $obj->get() : null;
}
return ($obj === null || static::isSupported($obj)) && $this->value == $obj;
}

/**
* @return T
*
* @throws Exception\NoSuchElement
*/
public function get(): mixed
{
if ($this->wasPresent === null) {
trigger_error(
'Call `isPresent()` before accessing the value.',
error_level: E_USER_NOTICE,
);
}
return $this->orElseThrow(static fn (): Exception\NoSuchElement => new Exception\NoSuchElement());
}

/**
* @param callable(T): void $consumer
*/
public function ifPresent(callable $consumer): void
{
if ($this->value !== null) {
$consumer($this->value);
}
}

public function isPresent(): bool
{
return $this->wasPresent = $this->value !== null;
}

/**
* @param T $other
*
* @return T
*/
public function orElse(mixed $other): mixed
{
return $this->orElseGet(static fn (): mixed => $other);
}

/**
* @param callable(): T $otherSupplier
*
* @return T
*/
public function orElseGet(callable $otherSupplier): mixed
{
if ($this->value !== null) {
return $this->value;
}
$other = $otherSupplier();
return static::isSupported($other) ? $other : throw new InvalidArgumentException('Other supplier must return supported other.');
}

/**
* @template E of Throwable
*
* @param callable(): E $exceptionSupplier
*
* @return T
*
* @throws E
*/
public function orElseThrow(callable $exceptionSupplier): mixed
{
return $this->orElseGet(static function () use ($exceptionSupplier): never {
/** @var Throwable|mixed $exception */
$exception = $exceptionSupplier();
if ($exception instanceof Throwable) {
throw $exception;
}
throw new InvalidArgumentException('Exception supplier must return ' . Throwable::class . '.');
});
}

/**
* @param T|mixed $value not null
*/
abstract protected static function isSupported(mixed $value): bool;
}
24 changes: 24 additions & 0 deletions src/AbstractOptionalObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace PetrKnap\Optional;

/**
* @template T of object
*
* @template-extends AbstractOptional<T>
*/
abstract class AbstractOptionalObject extends AbstractOptional
{
protected static function isSupported(mixed $value): bool
{
$expectedObjectClassName = static::getObjectClassName();
return $value instanceof $expectedObjectClassName;
}

/**
* @return class-string
*/
abstract protected static function getObjectClassName(): string;
}
24 changes: 24 additions & 0 deletions src/AbstractOptionalResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace PetrKnap\Optional;

/**
* @template-extends AbstractOptional<resource>
*/
abstract class AbstractOptionalResource extends AbstractOptional
{
protected static function isSupported(mixed $value): bool
{
$expectedResourceType = static::getResourceType();
return is_resource($value) && get_resource_type($value) === $expectedResourceType;
}

/**
* @see get_resource_type()
*
* @return non-empty-string
*/
abstract protected static function getResourceType(): string;
}
133 changes: 7 additions & 126 deletions src/Optional.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,135 +8,16 @@
use Throwable;

/**
* @template T of mixed type of non-null value
* Please use another implementation of {@see AbstractOptional} if possible.
*
* @see https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html
* @todo make it final
*
* @template T of mixed
*
* @template-extends AbstractOptional<T>
*/
class Optional
class Optional extends AbstractOptional
{
private bool|null $wasPresent = null;

/**
* @deprecated will be changed to protected - use {@see self::ofNullable()}/{@see self::of()}/{@see self::empty()}
*
* @param T|null $value
*/
final public function __construct(
protected readonly mixed $value,
) {
if ($this->value !== null && !static::isSupported($this->value)) {
throw new InvalidArgumentException('Value is not supported.');
}
}

public static function empty(): static
{
return new static(null);
}

/**
* @param T $value
*/
public static function of(mixed $value): static
{
return $value !== null ? new static($value) : throw new InvalidArgumentException('Value must not be null.');
}

/**
* @param T|null $value
*/
public static function ofNullable(mixed $value): static
{
return new static($value);
}

public function equals(mixed $obj): bool
{
if ($obj instanceof static) {
$obj = $obj->isPresent() ? $obj->get() : null;
}
return ($obj === null || static::isSupported($obj)) && $this->value == $obj;
}

/**
* @return T
*
* @throws Exception\NoSuchElement
*/
public function get(): mixed
{
if ($this->wasPresent === null) {
trigger_error(
'Call `isPresent()` before accessing the value.',
error_level: E_USER_NOTICE,
);
}
return $this->orElseThrow(static fn (): Exception\NoSuchElement => new Exception\NoSuchElement());
}

/**
* @param callable(T): void $consumer
*/
public function ifPresent(callable $consumer): void
{
if ($this->value !== null) {
$consumer($this->value);
}
}

public function isPresent(): bool
{
return $this->wasPresent = $this->value !== null;
}

/**
* @param T $other
*
* @return T
*/
public function orElse(mixed $other): mixed
{
return $this->orElseGet(static fn (): mixed => $other);
}

/**
* @param callable(): T $otherSupplier
*
* @return T
*/
public function orElseGet(callable $otherSupplier): mixed
{
if ($this->value !== null) {
return $this->value;
}
$other = $otherSupplier();
return static::isSupported($other) ? $other : throw new InvalidArgumentException('Other supplier must return supported other.');
}

/**
* @template E of Throwable
*
* @param callable(): E $exceptionSupplier
*
* @return T
*
* @throws E
*/
public function orElseThrow(callable $exceptionSupplier): mixed
{
return $this->orElseGet(static function () use ($exceptionSupplier): never {
/** @var Throwable|mixed $exception */
$exception = $exceptionSupplier();
if ($exception instanceof Throwable) {
throw $exception;
}
throw new InvalidArgumentException('Exception supplier must return ' . Throwable::class . '.');
});
}

/**
* @param T|mixed $value not null
*/
protected static function isSupported(mixed $value): bool
{
trigger_error(
Expand Down
19 changes: 19 additions & 0 deletions src/OptionalArray.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace PetrKnap\Optional;

/**
* @template K of array-key
* @template V of mixed
*
* @template-extends AbstractOptional<array<K, V>>
*/
final class OptionalArray extends AbstractOptional
{
protected static function isSupported(mixed $value): bool
{
return is_array($value);
}
}
Loading

0 comments on commit e61b714

Please sign in to comment.