From 3ef4156bb6e43a8e553faea7367fd07a2678f9ac Mon Sep 17 00:00:00 2001 From: kakiuchi-shigenao Date: Sun, 4 May 2025 20:09:15 +0900 Subject: [PATCH 1/2] =?UTF-8?q?phpstan/phpstan-strict-rules=20=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 9 +- composer.lock | 100 +++++++++++++++++- examples/Number/Decimal/TestDecimalValue.php | 2 +- .../Decimal/TestNegativeDecimalValue.php | 2 +- .../Decimal/TestPositiveDecimalValue.php | 2 +- phpstan.neon.dist | 7 +- src/Collection/ArrayList.php | 18 ++-- src/Collection/Base/CollectionBase.php | 2 +- .../Exception/CollectionNotFoundException.php | 6 +- src/Collection/List/IArrayList.php | 9 +- src/Collection/Map.php | 23 ++-- src/Collection/Map/IMap.php | 9 +- src/Collection/Pair.php | 10 +- src/String/Base/StringValueBase.php | 3 +- src/String/EmailAddress.php | 3 +- src/ValueObjectDefault.php | 6 +- .../Unit/Number/Decimal/DecimalValueTest.php | 14 ++- .../Decimal/NegativeDecimalValueTest.php | 2 +- .../Decimal/PositiveDecimalValueTest.php | 2 +- .../Unit/Number/Integer/IntegerValueTest.php | 12 ++- 20 files changed, 183 insertions(+), 58 deletions(-) diff --git a/composer.json b/composer.json index 4fcccbc..1ef2de3 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,13 @@ "phpunit/php-code-coverage": "^11.0", "phpunit/phpunit": "^11.3", "wiz-develop/php-monad": "^2.2", - "phpstan/phpstan": "^2.1" + "phpstan/phpstan": "^2.1", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan-strict-rules": "^2.0" + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + } } } diff --git a/composer.lock b/composer.lock index 71b8498..e4137da 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "61f76b35c8f8f453a664c71c84c03cfb", + "content-hash": "9cd8f9e2e4e0e6725b993c9dbfca2e41", "packages": [], "packages-dev": [ { @@ -745,6 +745,54 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpstan/extension-installer", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.9.0 || ^2.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2.0", + "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPStan\\ExtensionInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPStan\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer plugin for automatic installation of PHPStan extensions", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/extension-installer/issues", + "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" + }, + "time": "2024-09-04T20:21:43+00:00" + }, { "name": "phpstan/phpstan", "version": "2.1.12", @@ -803,6 +851,54 @@ ], "time": "2025-04-16T13:19:18+00:00" }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "3e139cbe67fafa3588e1dbe27ca50f31fdb6236a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/3e139cbe67fafa3588e1dbe27ca50f31fdb6236a", + "reference": "3e139cbe67fafa3588e1dbe27ca50f31fdb6236a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0.4" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.4" + }, + "time": "2025-03-18T11:42:40+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "11.0.9", @@ -4311,7 +4407,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.3" + "php": ">=8.4" }, "platform-dev": {}, "plugin-api-version": "2.6.0" diff --git a/examples/Number/Decimal/TestDecimalValue.php b/examples/Number/Decimal/TestDecimalValue.php index 4fb81e6..a120332 100644 --- a/examples/Number/Decimal/TestDecimalValue.php +++ b/examples/Number/Decimal/TestDecimalValue.php @@ -4,7 +4,7 @@ namespace WizDevelop\PhpValueObject\Examples\Number\Decimal; -use BCMath\Number; +use BcMath\Number; use Override; use WizDevelop\PhpValueObject\Number\DecimalValue; use WizDevelop\PhpValueObject\ValueObjectMeta; diff --git a/examples/Number/Decimal/TestNegativeDecimalValue.php b/examples/Number/Decimal/TestNegativeDecimalValue.php index 0c0008e..896cb1a 100644 --- a/examples/Number/Decimal/TestNegativeDecimalValue.php +++ b/examples/Number/Decimal/TestNegativeDecimalValue.php @@ -4,7 +4,7 @@ namespace WizDevelop\PhpValueObject\Examples\Number\Decimal; -use BCMath\Number; +use BcMath\Number; use Override; use WizDevelop\PhpValueObject\Number\NegativeDecimalValue; use WizDevelop\PhpValueObject\ValueObjectMeta; diff --git a/examples/Number/Decimal/TestPositiveDecimalValue.php b/examples/Number/Decimal/TestPositiveDecimalValue.php index 9638bcf..4b76b0a 100644 --- a/examples/Number/Decimal/TestPositiveDecimalValue.php +++ b/examples/Number/Decimal/TestPositiveDecimalValue.php @@ -4,7 +4,7 @@ namespace WizDevelop\PhpValueObject\Examples\Number\Decimal; -use BCMath\Number; +use BcMath\Number; use Override; use WizDevelop\PhpValueObject\Number\PositiveDecimalValue; use WizDevelop\PhpValueObject\ValueObjectMeta; diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 2e14969..9feed1c 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -4,5 +4,10 @@ parameters: level: max paths: - src + - tests typeAliases: - NumberValueBase: '\WizDevelop\PhpValueObject\Number\Decimal\DecimalValueBase|\WizDevelop\PhpValueObject\Number\Integer\IntegerValueBase' \ No newline at end of file + NumberValueBase: '\WizDevelop\PhpValueObject\Number\Decimal\DecimalValueBase|\WizDevelop\PhpValueObject\Number\Integer\IntegerValueBase' + ignoreErrors: + - + # NOTE: PHP-CS-Fixer のルールと競合するため + identifier: staticMethod.dynamicCall diff --git a/src/Collection/ArrayList.php b/src/Collection/ArrayList.php index f5c8d4b..dc5eb38 100644 --- a/src/Collection/ArrayList.php +++ b/src/Collection/ArrayList.php @@ -190,7 +190,7 @@ final public function firstOrFail(?Closure $closure = null) #[Override] final public function sole(?Closure $closure = null) { - $items = $closure ? $this->filter($closure) : new static($this->elements); + $items = $closure === null ? new static($this->elements) : $this->filter($closure); $count = $items->count(); if ($count === 0) { @@ -306,16 +306,14 @@ final public function unique(?Closure $closure = null): static } /** - * @template TReduceInitial - * @template TReduceReturnType - * @param Closure(TReduceInitial|TReduceReturnType,TValue,int): TReduceReturnType $closure - * @param TReduceInitial $initial - * @return TReduceReturnType + * @template TCarry + * @param Closure(TCarry,TValue,int): TCarry $closure + * @param TCarry $initial + * @return TCarry */ #[Override] final public function reduce(Closure $closure, $initial = null) { - /** @var TReduceReturnType */ $carry = $initial; foreach ($this->elements as $index => $value) { @@ -367,7 +365,11 @@ final public function sort(?Closure $closure = null): static { $elements = $this->elements; - $closure ? uasort($elements, $closure) : asort($elements, SORT_REGULAR); + if ($closure === null) { + asort($elements, SORT_REGULAR); + } else { + uasort($elements, $closure); + } return new static($elements); } diff --git a/src/Collection/Base/CollectionBase.php b/src/Collection/Base/CollectionBase.php index e5bca02..1d3be72 100644 --- a/src/Collection/Base/CollectionBase.php +++ b/src/Collection/Base/CollectionBase.php @@ -38,7 +38,7 @@ final public function equals(IValueObject $other): bool #[Override] final public function __toString(): string { - return (string)$this->jsonSerialize(); + return $this->jsonSerialize(); } #[Override] diff --git a/src/Collection/Exception/CollectionNotFoundException.php b/src/Collection/Exception/CollectionNotFoundException.php index f14194e..a6bfcae 100644 --- a/src/Collection/Exception/CollectionNotFoundException.php +++ b/src/Collection/Exception/CollectionNotFoundException.php @@ -13,10 +13,10 @@ final class CollectionNotFoundException extends RuntimeException { public function __construct(?string $className = null, ?string $message = null) { - if ($message) { - parent::__construct($message); - } else { + if ($message === null) { parent::__construct("{$className} が見つかりませんでした。"); + } else { + parent::__construct($message); } } } diff --git a/src/Collection/List/IArrayList.php b/src/Collection/List/IArrayList.php index 083df7e..6ed538a 100644 --- a/src/Collection/List/IArrayList.php +++ b/src/Collection/List/IArrayList.php @@ -153,11 +153,10 @@ public function reject(Closure $closure): static; public function unique(?Closure $closure = null): static; /** - * @template TReduceInitial - * @template TReduceReturnType - * @param Closure(TReduceInitial|TReduceReturnType,TValue,int): TReduceReturnType $closure - * @param TReduceInitial $initial - * @return TReduceReturnType + * @template TCarry + * @param Closure(TCarry,TValue,int): TCarry $closure + * @param TCarry $initial + * @return TCarry */ public function reduce(Closure $closure, $initial = null); diff --git a/src/Collection/Map.php b/src/Collection/Map.php index 3af83c9..f8fc3af 100644 --- a/src/Collection/Map.php +++ b/src/Collection/Map.php @@ -248,7 +248,7 @@ final public function firstOrFail(?Closure $closure = null): Pair #[Override] final public function sole(?Closure $closure = null): Pair { - $items = $closure ? $this->filter($closure) : new static($this->elements); + $items = $closure === null ? new static($this->elements) : $this->filter($closure); $count = $items->count(); if ($count === 0) { @@ -316,14 +316,15 @@ final public function get($key, $default = null): Option * @param self $other * @return self */ + /** + * @phpstan-ignore-next-line + */ #[Override] final public function merge(IMap $other): self { - /** @var array> */ $elements = $this->elements; foreach ($other as $key => $value) { - /** @var Pair */ $puttingPair = Pair::of($key, $value); self::putPair($elements, $puttingPair); } @@ -389,16 +390,14 @@ final public function reject(Closure $closure): static } /** - * @template TReduceInitial - * @template TReduceReturnType - * @param Closure(TReduceInitial|TReduceReturnType,TValue,TKey): TReduceReturnType $closure - * @param TReduceInitial $initial - * @return TReduceReturnType + * @template TCarry + * @param Closure(TCarry,TValue,TKey): TCarry $closure + * @param TCarry $initial + * @return TCarry */ #[Override] final public function reduce(Closure $closure, $initial = null) { - /** @var TReduceReturnType */ $carry = $initial; foreach ($this->elements as $index => $pair) { @@ -419,10 +418,10 @@ final public function sort(?Closure $closure = null): static { $elements = $this->elements; - if ($closure) { - usort($elements, static fn ($a, $b) => $closure($a->value, $b->value)); - } else { + if ($closure === null) { usort($elements, static fn ($a, $b) => $a->value <=> $b->value); + } else { + usort($elements, static fn ($a, $b) => $closure($a->value, $b->value)); } return new static($elements); diff --git a/src/Collection/Map/IMap.php b/src/Collection/Map/IMap.php index 447ec82..953a21c 100644 --- a/src/Collection/Map/IMap.php +++ b/src/Collection/Map/IMap.php @@ -163,11 +163,10 @@ public function filter(Closure $closure): static; public function reject(Closure $closure): static; /** - * @template TReduceInitial - * @template TReduceReturnType - * @param Closure(TReduceInitial|TReduceReturnType,TValue,TKey): TReduceReturnType $closure - * @param TReduceInitial $initial - * @return TReduceReturnType + * @template TCarry + * @param Closure(TCarry,TValue,TKey): TCarry $closure + * @param TCarry $initial + * @return TCarry */ public function reduce(Closure $closure, $initial = null); diff --git a/src/Collection/Pair.php b/src/Collection/Pair.php index 765ee30..dc36705 100644 --- a/src/Collection/Pair.php +++ b/src/Collection/Pair.php @@ -13,7 +13,7 @@ * @template TKey * @template TValue */ -final readonly class Pair implements IValueObject +readonly class Pair implements IValueObject { use ValueObjectDefault; @@ -23,7 +23,7 @@ * @param TKey $key * @param TValue $value */ - private function __construct( + final private function __construct( public mixed $key, public mixed $value ) { @@ -37,7 +37,7 @@ private function __construct( * @param TOfValue $value * @return self */ - public static function of( + final public static function of( mixed $key, mixed $value ): self { @@ -49,7 +49,7 @@ public static function of( * * @return self */ - public function copy(): self + final public function copy(): self { return new self($this->key, $this->value); } @@ -57,7 +57,7 @@ public function copy(): self /** * @return array{key: TKey, value: TValue} */ - public function toArray(): array + final public function toArray(): array { return [ 'key' => $this->key, diff --git a/src/String/Base/StringValueBase.php b/src/String/Base/StringValueBase.php index 3b8c0e3..f784b9c 100644 --- a/src/String/Base/StringValueBase.php +++ b/src/String/Base/StringValueBase.php @@ -94,8 +94,9 @@ className: static::class, final protected static function isRegexValid(string $value): Result { $regex = static::regex(); + $matchResult = preg_match($regex, $value); - if ($regex !== self::REGEX && !preg_match($regex, $value)) { + if ($regex !== self::REGEX && $matchResult !== 1) { return Result\err(StringValueError::invalidRegex( className: static::class, regex: $regex, diff --git a/src/String/EmailAddress.php b/src/String/EmailAddress.php index 5c49ed7..9b03112 100644 --- a/src/String/EmailAddress.php +++ b/src/String/EmailAddress.php @@ -69,7 +69,8 @@ final protected static function regex(): string */ final protected static function isValidEmail(string $value): Result { - if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { + $filteredValue = filter_var($value, FILTER_VALIDATE_EMAIL); + if ($filteredValue === false) { return Result\err(StringValueError::invalidEmail( className: self::class, value: $value, diff --git a/src/ValueObjectDefault.php b/src/ValueObjectDefault.php index 5ad412c..92b8c75 100644 --- a/src/ValueObjectDefault.php +++ b/src/ValueObjectDefault.php @@ -13,13 +13,13 @@ trait ValueObjectDefault { #[Override] - public function equals(IValueObject $other): bool + final public function equals(IValueObject $other): bool { return (string)$this === (string)$other; } #[Override] - public function __toString(): string + final public function __toString(): string { return json_encode($this->jsonSerialize(), JSON_THROW_ON_ERROR); } @@ -28,7 +28,7 @@ public function __toString(): string * @return array */ #[Override] - public function jsonSerialize(): array + final public function jsonSerialize(): array { return get_object_vars($this); } diff --git a/tests/Unit/Number/Decimal/DecimalValueTest.php b/tests/Unit/Number/Decimal/DecimalValueTest.php index 899a364..2dabb35 100644 --- a/tests/Unit/Number/Decimal/DecimalValueTest.php +++ b/tests/Unit/Number/Decimal/DecimalValueTest.php @@ -4,7 +4,7 @@ namespace WizDevelop\PhpValueObject\Tests\Unit\Number\Decimal; -use BCMath\Number; +use BcMath\Number; use DivisionByZeroError; use Error; use Exception; @@ -325,7 +325,10 @@ public function 算術演算メソッドのテスト( // tryXxx系のメソッドを使用 $tryMethodName = 'try' . ucfirst($operation); - /** @var Result */ + /** + * @var Result + * @phpstan-ignore-next-line + */ $result = $decimal1->{$tryMethodName}($decimal2); $this->assertInstanceOf(Result::class, $result); @@ -340,7 +343,10 @@ public function 算術演算メソッドのテスト( // 例外を投げる通常メソッドのテスト if ($shouldSucceed) { try { - /** @var DecimalValueBase */ + /** + * @var DecimalValueBase + * @phpstan-ignore-next-line + */ $methodResult = $decimal1->{$operation}($decimal2); $this->assertInstanceOf(DecimalValueBase::class, $methodResult); $this->assertEquals($expected, (string)$methodResult->value); @@ -351,9 +357,11 @@ public function 算術演算メソッドのテスト( // 除算で0の場合は特別処理 if ($operation === 'div' && $value2 === '0') { $this->expectException(DivisionByZeroError::class); + // @phpstan-ignore-next-line $decimal1->{$operation}($decimal2); } else { try { + // @phpstan-ignore-next-line $decimal1->{$operation}($decimal2); $this->fail("演算 {$value1} {$operation} {$value2} は例外を投げるべき"); } catch (Throwable $e) { diff --git a/tests/Unit/Number/Decimal/NegativeDecimalValueTest.php b/tests/Unit/Number/Decimal/NegativeDecimalValueTest.php index 7c10c65..a5a71cb 100644 --- a/tests/Unit/Number/Decimal/NegativeDecimalValueTest.php +++ b/tests/Unit/Number/Decimal/NegativeDecimalValueTest.php @@ -4,7 +4,7 @@ namespace WizDevelop\PhpValueObject\Tests\Unit\Number\Decimal; -use BCMath\Number; +use BcMath\Number; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/Unit/Number/Decimal/PositiveDecimalValueTest.php b/tests/Unit/Number/Decimal/PositiveDecimalValueTest.php index a93f090..1ead819 100644 --- a/tests/Unit/Number/Decimal/PositiveDecimalValueTest.php +++ b/tests/Unit/Number/Decimal/PositiveDecimalValueTest.php @@ -4,7 +4,7 @@ namespace WizDevelop\PhpValueObject\Tests\Unit\Number\Decimal; -use BCMath\Number; +use BcMath\Number; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/Unit/Number/Integer/IntegerValueTest.php b/tests/Unit/Number/Integer/IntegerValueTest.php index 9df0978..e4b4559 100644 --- a/tests/Unit/Number/Integer/IntegerValueTest.php +++ b/tests/Unit/Number/Integer/IntegerValueTest.php @@ -310,7 +310,10 @@ public function 算術演算メソッドのテスト( // tryXxx系のメソッドを使用 $tryMethodName = 'try' . ucfirst($operation); - /** @var Result */ + /** + * @var Result + * @phpstan-ignore-next-line + */ $result = $integer1->{$tryMethodName}($integer2); $this->assertInstanceOf(Result::class, $result); @@ -325,7 +328,10 @@ public function 算術演算メソッドのテスト( // 例外を投げる通常メソッドのテスト if ($shouldSucceed) { try { - /** @var IntegerValueBase */ + /** + * @var IntegerValueBase + * @phpstan-ignore-next-line + */ $methodResult = $integer1->{$operation}($integer2); $this->assertInstanceOf(IntegerValueBase::class, $methodResult); $this->assertEquals($expected, $methodResult->value); @@ -336,9 +342,11 @@ public function 算術演算メソッドのテスト( // 除算で0の場合は特別処理 if ($operation === 'div' && $value2 === 0) { $this->expectException(DivisionByZeroError::class); + // @phpstan-ignore-next-line $integer1->{$operation}($integer2); } else { try { + // @phpstan-ignore-next-line $integer1->{$operation}($integer2); $this->fail("演算 {$value1} {$operation} {$value2} は例外を投げるべき"); } catch (Throwable $e) { From d4d9289dc524440ead9ce4351fd9251a5287f9c5 Mon Sep 17 00:00:00 2001 From: kakiuchi-shigenao Date: Mon, 5 May 2025 12:58:46 +0900 Subject: [PATCH 2/2] feat: Implement BooleanValue and related classes with comprehensive tests Fixes #1 --- examples/Boolean/TestBooleanValue.php | 18 ++ src/Boolean/Base/BooleanValueBase.php | 97 ++++++++ src/Boolean/Base/BooleanValueFactory.php | 57 +++++ src/Boolean/Base/IBooleanValueFactory.php | 49 ++++ src/Boolean/BooleanValue.php | 33 +++ src/Boolean/BooleanValueError.php | 24 ++ tests/Unit/Boolean/BooleanValueTest.php | 280 ++++++++++++++++++++++ 7 files changed, 558 insertions(+) create mode 100644 examples/Boolean/TestBooleanValue.php create mode 100644 src/Boolean/Base/BooleanValueBase.php create mode 100644 src/Boolean/Base/BooleanValueFactory.php create mode 100644 src/Boolean/Base/IBooleanValueFactory.php create mode 100644 src/Boolean/BooleanValue.php create mode 100644 src/Boolean/BooleanValueError.php create mode 100644 tests/Unit/Boolean/BooleanValueTest.php diff --git a/examples/Boolean/TestBooleanValue.php b/examples/Boolean/TestBooleanValue.php new file mode 100644 index 0000000..4e4e75c --- /dev/null +++ b/examples/Boolean/TestBooleanValue.php @@ -0,0 +1,18 @@ +isOk()); + } + + #[Override] + final public function equals(IValueObject $other): bool + { + return (string)$this === (string)$other; + } + + #[Override] + final public function __toString(): string + { + return $this->value ? 'true' : 'false'; + } + + #[Override] + final public function jsonSerialize(): bool + { + return $this->value; + } + + /** + * 有効な値かどうか + * NOTE: 実装クラスでのオーバーライド用メソッド + * @return Result + */ + protected static function isValid(bool $value): Result + { + return Result\ok(true); + } + + /** + * 真の値かどうか + */ + final public function yes(): bool + { + return $this->value === true; + } + + /** + * 偽の値かどうか + */ + final public function no(): bool + { + return $this->value === false; + } + + /** + * 否定値を取得 + */ + final public function not(): static + { + return static::from(!$this->value); + } + + /** + * 論理積 + */ + final public function and(self $other): static + { + return static::from($this->value && $other->value); + } + + /** + * 論理和 + */ + final public function or(self $other): static + { + return static::from($this->value || $other->value); + } + + /** + * 排他的論理和 + */ + final public function xor(self $other): static + { + return static::from($this->value xor $other->value); + } +} diff --git a/src/Boolean/Base/BooleanValueFactory.php b/src/Boolean/Base/BooleanValueFactory.php new file mode 100644 index 0000000..f296670 --- /dev/null +++ b/src/Boolean/Base/BooleanValueFactory.php @@ -0,0 +1,57 @@ +map(static fn ($result) => Option\some($result)); + } + + #[Override] + final public static function true(): static + { + return static::from(true); + } + + #[Override] + final public static function false(): static + { + return static::from(false); + } +} diff --git a/src/Boolean/Base/IBooleanValueFactory.php b/src/Boolean/Base/IBooleanValueFactory.php new file mode 100644 index 0000000..e257f17 --- /dev/null +++ b/src/Boolean/Base/IBooleanValueFactory.php @@ -0,0 +1,49 @@ + + */ + public static function fromNullable(?bool $value): Option; + + /** + * 信頼できないプリミティブ値からインスタンスを生成する + * @return Result + */ + public static function tryFrom(bool $value): Result; + + /** + * 信頼できないプリミティブ値からインスタンスを生成する(Null許容) + * @return Result,BooleanValueError> + */ + public static function tryFromNullable(?bool $value): Result; + + /** + * 真値のインスタンスを取得 + */ + public static function true(): static; + + /** + * 偽値のインスタンスを取得 + */ + public static function false(): static; +} diff --git a/src/Boolean/BooleanValue.php b/src/Boolean/BooleanValue.php new file mode 100644 index 0000000..197853f --- /dev/null +++ b/src/Boolean/BooleanValue.php @@ -0,0 +1,33 @@ +andThen(static fn () => Result\ok(static::from($value))); + } +} diff --git a/src/Boolean/BooleanValueError.php b/src/Boolean/BooleanValueError.php new file mode 100644 index 0000000..b9ae19c --- /dev/null +++ b/src/Boolean/BooleanValueError.php @@ -0,0 +1,24 @@ + + */ +final readonly class BooleanValueError extends ValueObjectError +{ + public static function invalid( + string $message, + ): static { + return new self( + code: __METHOD__, + message: $message, + ); + } +} diff --git a/tests/Unit/Boolean/BooleanValueTest.php b/tests/Unit/Boolean/BooleanValueTest.php new file mode 100644 index 0000000..12a9986 --- /dev/null +++ b/tests/Unit/Boolean/BooleanValueTest.php @@ -0,0 +1,280 @@ +assertTrue($boolTrue->value); + + $boolFalse = TestBooleanValue::from(false); + $this->assertFalse($boolFalse->value); + } + + #[Test] + public function 専用ファクトリメソッドでインスタンスが作成できる(): void + { + $boolTrue = TestBooleanValue::true(); + $this->assertTrue($boolTrue->value); + + $boolFalse = TestBooleanValue::false(); + $this->assertFalse($boolFalse->value); + } + + #[Test] + public function value関数で内部値を取得できる(): void + { + $boolTrue = TestBooleanValue::from(true); + $this->assertTrue($boolTrue->value); + + $boolFalse = TestBooleanValue::from(false); + $this->assertFalse($boolFalse->value); + } + + #[Test] + public function isTrue関数で真の値かどうかを判定できる(): void + { + $boolTrue = TestBooleanValue::from(true); + $boolFalse = TestBooleanValue::from(false); + + $this->assertTrue($boolTrue->yes()); + $this->assertFalse($boolFalse->yes()); + } + + #[Test] + public function isFalse関数で偽の値かどうかを判定できる(): void + { + $boolTrue = TestBooleanValue::from(true); + $boolFalse = TestBooleanValue::from(false); + + $this->assertFalse($boolTrue->no()); + $this->assertTrue($boolFalse->no()); + } + + // ------------------------------------------ + // 論理演算のテスト + // ------------------------------------------ + + #[Test] + public function not関数で否定値を取得できる(): void + { + $boolTrue = TestBooleanValue::from(true); + $boolFalse = TestBooleanValue::from(false); + + $this->assertTrue($boolTrue->not()->no()); + $this->assertTrue($boolFalse->not()->yes()); + } + + #[Test] + public function and関数で論理積を計算できる(): void + { + $boolTrue = TestBooleanValue::from(true); + $boolFalse = TestBooleanValue::from(false); + + $this->assertTrue($boolTrue->and($boolTrue)->yes()); + $this->assertTrue($boolTrue->and($boolFalse)->no()); + $this->assertTrue($boolFalse->and($boolTrue)->no()); + $this->assertTrue($boolFalse->and($boolFalse)->no()); + } + + #[Test] + public function or関数で論理和を計算できる(): void + { + $boolTrue = TestBooleanValue::from(true); + $boolFalse = TestBooleanValue::from(false); + + $this->assertTrue($boolTrue->or($boolTrue)->yes()); + $this->assertTrue($boolTrue->or($boolFalse)->yes()); + $this->assertTrue($boolFalse->or($boolTrue)->yes()); + $this->assertTrue($boolFalse->or($boolFalse)->no()); + } + + #[Test] + public function xor関数で排他的論理和を計算できる(): void + { + $boolTrue = TestBooleanValue::from(true); + $boolFalse = TestBooleanValue::from(false); + + $this->assertTrue($boolTrue->xor($boolTrue)->no()); + $this->assertTrue($boolTrue->xor($boolFalse)->yes()); + $this->assertTrue($boolFalse->xor($boolTrue)->yes()); + $this->assertTrue($boolFalse->xor($boolFalse)->no()); + } + + // ------------------------------------------ + // Nullableメソッドのテスト + // ------------------------------------------ + + #[Test] + public function fromNullable関数でNullを扱える(): void + { + // Null値の場合 + $option1 = TestBooleanValue::fromNullable(null); + $this->assertTrue($option1->isNone()); + + // 非Null値の場合 + $option2 = TestBooleanValue::fromNullable(true); + $this->assertTrue($option2->isSome()); + $this->assertTrue($option2->unwrap()->value); + + $option3 = TestBooleanValue::fromNullable(false); + $this->assertTrue($option3->isSome()); + $this->assertFalse($option3->unwrap()->value); + } + + #[Test] + public function tryFromNullable関数でNullを扱える(): void + { + // Null値の場合 + $result1 = TestBooleanValue::tryFromNullable(null); + $this->assertTrue($result1->isOk()); + $this->assertTrue($result1->unwrap()->isNone()); + + // 非Null値の場合 + $result2 = TestBooleanValue::tryFromNullable(true); + $this->assertTrue($result2->isOk()); + $this->assertTrue($result2->unwrap()->isSome()); + $this->assertTrue($result2->unwrap()->unwrap()->value); + + $result3 = TestBooleanValue::tryFromNullable(false); + $this->assertTrue($result3->isOk()); + $this->assertTrue($result3->unwrap()->isSome()); + $this->assertFalse($result3->unwrap()->unwrap()->value); + } + + // ------------------------------------------ + // 変換関数のテスト + // ------------------------------------------ + + #[Test] + public function 文字列表現のテスト(): void + { + $boolTrue = TestBooleanValue::from(true); + $boolFalse = TestBooleanValue::from(false); + + $this->assertEquals('true', (string)$boolTrue); + $this->assertEquals('false', (string)$boolFalse); + } + + #[Test] + public function jsonSerializeメソッドは真偽値を返す(): void + { + $boolTrue = TestBooleanValue::from(true); + $boolFalse = TestBooleanValue::from(false); + + $this->assertTrue($boolTrue->jsonSerialize()); + $this->assertFalse($boolFalse->jsonSerialize()); + + $jsonTrue = json_encode($boolTrue); + $jsonFalse = json_encode($boolFalse); + + $this->assertSame('true', $jsonTrue); + $this->assertSame('false', $jsonFalse); + } + + // ------------------------------------------ + // 比較演算のテスト + // ------------------------------------------ + + #[Test] + public function equals関数で同値性の比較ができる(): void + { + $boolTrue1 = TestBooleanValue::from(true); + $boolTrue2 = TestBooleanValue::from(true); + $boolFalse = TestBooleanValue::from(false); + + $this->assertTrue($boolTrue1->equals($boolTrue2)); + $this->assertFalse($boolTrue1->equals($boolFalse)); + $this->assertFalse($boolFalse->equals($boolTrue1)); + $this->assertTrue($boolFalse->equals($boolFalse)); + } + + // ------------------------------------------ + // アクセス制御のテスト + // ------------------------------------------ + + #[Test] + public function コンストラクタはprivateアクセス修飾子を持つことを確認(): void + { + $reflectionClass = new ReflectionClass(TestBooleanValue::class); + $constructor = $reflectionClass->getConstructor(); + + $this->assertNotNull($constructor, 'コンストラクタが見つかりませんでした'); + $this->assertTrue($constructor->isPrivate(), 'コンストラクタはprivateでなければならない'); + } + + #[Test] + public function privateコンストラクタへのアクセスを試みるとエラーとなることを確認(): void + { + $hasThrown = false; + + try { + // コンストラクタへの直接アクセスを試みる(通常これはPHPで許可されていない) + // 以下は単にエラーが発生することを確認するだけ + /** @phpstan-ignore-next-line */ + $newObj = new TestBooleanValue(true); + } catch (Error $e) { + $hasThrown = true; + $this->assertStringContainsString( + 'private', + $e->getMessage(), + 'エラーメッセージにprivateという文字列が含まれるべき' + ); + } + + $this->assertTrue($hasThrown, 'privateコンストラクタへのアクセス時にはエラーが発生するべき'); + } + + // ------------------------------------------ + // 追加テスト:複合的な論理演算 + // ------------------------------------------ + + #[Test] + public function 複合的な論理演算のテスト(): void + { + $trueVal = TestBooleanValue::true(); + $falseVal = TestBooleanValue::false(); + + // (true AND false) OR true = true + $result1 = $trueVal->and($falseVal)->or($trueVal); + $this->assertTrue($result1->yes()); + + // (false OR true) AND false = false + $result2 = $falseVal->or($trueVal)->and($falseVal); + $this->assertTrue($result2->no()); + + // NOT (true AND true) = false + $result3 = $trueVal->and($trueVal)->not(); + $this->assertTrue($result3->no()); + + // true XOR (false OR false) = true + $result4 = $trueVal->xor($falseVal->or($falseVal)); + $this->assertTrue($result4->yes()); + } +}