Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented typed optionals #8

Merged
merged 1 commit into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
143 changes: 143 additions & 0 deletions src/AbstractOptional.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

declare(strict_types=1);

namespace PetrKnap\Optional;

use InvalidArgumentException;
use Throwable;

/**
* @todo make constructor protected
*
* @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;
}
135 changes: 9 additions & 126 deletions src/Optional.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,135 +8,18 @@
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
*
* @deprecated will be converted to 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
Loading