Skip to content

ErrorValue に details を追加 #36

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

Merged
merged 4 commits into from
Jul 6, 2025
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
15 changes: 9 additions & 6 deletions src/Error/CollectionValueError.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ public static function invalidRange(
): ValueObjectError {
$displayName = ValueObjectError::getDisplayName($className);

if ($min === $max) {
return ValueObjectError::of(
code: 'value_object.collection.invalid_count_exact',
message: "{$displayName}は{$min}個である必要があります。(要素数:{$count})",
);
}

return ValueObjectError::of(
code: 'value_object.collection.invalid_range',
message: "{$displayName}は{$min}個以上、{$max}個以下である必要があります。(要素数:{$count})",
Expand All @@ -71,14 +78,10 @@ public static function invalidElementValues(string $className, IErrorValue ...$e
{
$displayName = ValueObjectError::getDisplayName($className);

$errorsStr = implode(
', ',
array_map(static fn (IErrorValue $error) => $error->getMessage(), $errors),
);

return ValueObjectError::of(
code: 'value_object.collection.invalid_element_values',
message: "{$displayName}に無効な要素が含まれています。(無効な要素の詳細: {$errorsStr})",
message: "{$displayName}に無効な要素が含まれています。",
details: $errors,
);
}
}
8 changes: 8 additions & 0 deletions src/Error/DateTimeValueError.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ public static function invalidRange(
?string $maxValue = null,
): ValueObjectError {
$displayName = ValueObjectError::getDisplayName($className);

if ($minValue === $maxValue) {
return ValueObjectError::of(
code: 'value_object.datetime.invalid_range_exact',
message: "{$displayName}は{$minValue}である必要があります。(値:{$value})",
);
}

$message = "{$displayName}は有効な{$attributeName}の範囲内である必要があります。(値:{$value})";

if ($minValue !== null && $maxValue !== null) {
Expand Down
94 changes: 68 additions & 26 deletions src/Error/ErrorValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,31 @@
namespace WizDevelop\PhpValueObject\Error;

use Override;
use WizDevelop\PhpValueObject\IValueObject;
use WizDevelop\PhpValueObject\ValueObjectDefault;

/**
* エラー値オブジェクト
*/
readonly class ErrorValue implements IErrorValue
{
use ValueObjectDefault;

/**
* @param IErrorValue[] $details
*/
final private function __construct(
private string $code,
private string $message,
private array $details,
) {
}

final public static function of(string $code, string $message): static
{
return new static($code, $message);
}

#[Override]
final public function equals(IValueObject $other): bool
{
return $this->code === $other->code;
}

#[Override]
final public function __toString(): string
{
return $this->serialize();
}

/**
* @return array<mixed>
* @param IErrorValue[] $details
*/
#[Override]
final public function jsonSerialize(): array
final public static function of(string $code, string $message, array $details = []): static
{
return get_object_vars($this);
return new static($code, $message, $details);
}

#[Override]
Expand All @@ -56,19 +44,73 @@ final public function getMessage(): string
return $this->message;
}

#[Override]
final public function getDetails(): array
{
return $this->details;
}

#[Override]
final public function serialize(): string
{
return $this->code . static::SEPARATOR . $this->message;
$result = $this->code . static::SEPARATOR . $this->message;

if (count($this->details) > 0) {
$result .= static::SEPARATOR . count($this->details);
foreach ($this->details as $detail) {
$result .= static::SEPARATOR . $detail->serialize();
}
}

return $result;
}

#[Override]
final public static function deserialize(string $serialized): static
{
$exploded = explode(static::SEPARATOR, $serialized);
$parts = explode(static::SEPARATOR, $serialized);
assert(count($parts) >= 2, 'Invalid serialized error value format.');

[$code, $message] = $parts;
$details = [];

if (count($parts) > 2) {
$detailCount = (int)$parts[2];
assert($detailCount >= 0, 'Invalid detail count in serialized error value.');

$index = 3;
for ($i = 0; $i < $detailCount; ++$i) {
[$detail, $index] = self::parseDetail($parts, $index);
$details[] = $detail;
}
}

return new static($code, $message, $details);
}

/**
* @param array<int, string> $parts
* @return array{IErrorValue, int}
*/
private static function parseDetail(array $parts, int $index): array
{
assert($index + 1 < count($parts), 'Invalid index for detail parsing.');

$code = $parts[$index];
$message = $parts[$index + 1];
$index += 2;
$details = [];

if ($index < count($parts) && is_numeric($parts[$index])) {
$nestedCount = (int)$parts[$index];
++$index;

assert(count($exploded) === 2);
for ($i = 0; $i < $nestedCount; ++$i) {
[$detail, $index] = self::parseDetail($parts, $index);
$details[] = $detail;
}
}

return new static(...$exploded);
return [new static($code, $message, $details), $index];
}
}
13 changes: 10 additions & 3 deletions src/Error/IErrorValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ public function getCode(): string;
*/
public function getMessage(): string;

/**
* エラーの詳細を取得する
*
* @return IErrorValue[] エラーの詳細
*/
public function getDetails(): array;

/**
* エラーをシリアライズする
*/
Expand All @@ -36,8 +43,8 @@ public function serialize(): string;
/**
* シリアライズされたエラーをデシリアライズする
*
* @param string $serialized シリアライズされたエラー
* @return static デシリアライズされたエラー
* @param string $serialized シリアライズされたエラー
* @return IErrorValue デシリアライズされたエラー
*/
public static function deserialize(string $serialized): static;
public static function deserialize(string $serialized): self;
}
8 changes: 8 additions & 0 deletions src/Error/NumberValueError.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ public static function invalidRange(
bool $isMaxInclusive = true,
): ValueObjectError {
$displayName = ValueObjectError::getDisplayName($className);

if ($min === $max) {
return ValueObjectError::of(
code: 'value_object.number.invalid_range_exact',
message: "{$displayName}は{$min}である必要があります。(値:{$value})",
);
}

$minText = $isMinInclusive ? '以上' : 'より大きい';
$maxText = $isMaxInclusive ? '以下' : '未満';

Expand Down
7 changes: 7 additions & 0 deletions src/Error/StringValueError.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ public static function invalidLength(
): ValueObjectError {
$displayName = ValueObjectError::getDisplayName($className);

if ($min_length === $max_length) {
return ValueObjectError::of(
code: 'value_object.string.invalid_length_exact',
message: "{$displayName}は{$min_length}文字である必要があります。(値:{$value})",
);
}

return ValueObjectError::of(
code: 'value_object.string.invalid_length',
message: "{$displayName}は{$min_length}文字以上{$max_length}文字以下である必要があります。(値:{$value})",
Expand Down
2 changes: 1 addition & 1 deletion src/ValueObjectDefault.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ final public function equals(IValueObject $other): bool
#[Override]
final public function __toString(): string
{
return json_encode($this->jsonSerialize(), JSON_THROW_ON_ERROR);
return json_encode($this->jsonSerialize(), JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
}

/**
Expand Down
6 changes: 3 additions & 3 deletions tests/Unit/Collection/ArrayListFromResultsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public function 一つでも失敗したResultが含まれる場合はエラー
$error = $result->unwrapErr();
$this->assertInstanceOf(ValueObjectError::class, $error);
$this->assertStringContainsString('無効な要素が含まれています', $error->getMessage());
$this->assertStringContainsString('テストエラー', $error->getMessage());
$this->assertStringContainsString('テストエラー', $error->serialize());
}

#[Test]
Expand All @@ -146,8 +146,8 @@ public function 複数の失敗したResultが含まれる場合は全てのエ
$error = $result->unwrapErr();
$this->assertInstanceOf(ValueObjectError::class, $error);
$this->assertStringContainsString('無効な要素が含まれています', $error->getMessage());
$this->assertStringContainsString('エラー1', $error->getMessage());
$this->assertStringContainsString('エラー2', $error->getMessage());
$this->assertStringContainsString('エラー1', $error->serialize());
$this->assertStringContainsString('エラー2', $error->serialize());
}

#[Test]
Expand Down
12 changes: 6 additions & 6 deletions tests/Unit/Collection/MapFromResultsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ public function キーに失敗したResultが含まれる場合はエラーが
$error = $result->unwrapErr();
$this->assertInstanceOf(ValueObjectError::class, $error);
$this->assertStringContainsString('無効な要素が含まれています', $error->getMessage());
$this->assertStringContainsString('キーエラー', $error->getMessage());
$this->assertStringContainsString('キーエラー', $error->serialize());
}

#[Test]
Expand All @@ -163,7 +163,7 @@ public function 値に失敗したResultが含まれる場合はエラーが返
$error = $result->unwrapErr();
$this->assertInstanceOf(ValueObjectError::class, $error);
$this->assertStringContainsString('無効な要素が含まれています', $error->getMessage());
$this->assertStringContainsString('値エラー', $error->getMessage());
$this->assertStringContainsString('値エラー', $error->serialize());
}

#[Test]
Expand All @@ -187,10 +187,10 @@ public function キーと値の両方に失敗したResultが含まれる場合
$error = $result->unwrapErr();
$this->assertInstanceOf(ValueObjectError::class, $error);
$this->assertStringContainsString('無効な要素が含まれています', $error->getMessage());
$this->assertStringContainsString('キーエラー1', $error->getMessage());
$this->assertStringContainsString('値エラー1', $error->getMessage());
$this->assertStringContainsString('キーエラー2', $error->getMessage());
$this->assertStringContainsString('値エラー2', $error->getMessage());
$this->assertStringContainsString('キーエラー1', $error->serialize());
$this->assertStringContainsString('値エラー1', $error->serialize());
$this->assertStringContainsString('キーエラー2', $error->serialize());
$this->assertStringContainsString('値エラー2', $error->serialize());
}

#[Test]
Expand Down
Loading
Loading