はじめに
多くのPHPプロジェクトでは、ドメインロジックが曖昧になり、バグの温床となることがあります。特に値の検証やビジネスルールの適用が複数の場所に散らばっていると、コードの保守が困難になりがちです。
そこで今回、ドメイン駆動設計(DDD)の重要な概念である「値オブジェクト」をPHPで簡単に実装できるライブラリ「PHP Value Object」を開発しました。
このライブラリを使用することで、型安全で自己検証機能を持つ堅牢なドメインモデルを構築できます。
値オブジェクトとは?
値オブジェクトは、ドメイン駆動設計における基本的な構成要素の一つです。これには以下の特性があります:
- 不変性: 一度作成されたら変更できない
- 自己検証: 自身の妥当性を常に保証する
- 型安全性: 厳格な型チェックにより予期しない値を防ぐ
- 値による等価性: IDではなく、保持する値自体で同一性を判断する
例えば、メールアドレスは単なる文字列ではなく、特定の形式を持つ値です。値オブジェクトとして実装することで、アプリケーション全体で一貫した検証と操作が可能になります。
ライブラリの特徴
PHP Value Objectライブラリは、PHP 8.4以上で動作し、以下の機能を提供します:
- 型安全性: すべての値オブジェクトは厳格な型チェックと検証を提供
- 不変性: すべての値オブジェクトはimmutable(readonly)
- 自己検証: 値オブジェクトは自身の妥当性を保証
- 演算機能: 数値型には演算や比較機能を提供
- コレクション: リストや連想配列のコレクションをサポート
- モナド: Result型によるエラーハンドリング
インストール方法
Composerを使用して簡単にインストールできます:
composer require wiz-develop/php-value-object
主要なコンポーネント
基本データ型
Boolean値
use WizDevelop\PhpValueObject\Boolean\BooleanValue;
use WizDevelop\PhpMonad\Result;
// 直接作成(検証なし)
$bool = BooleanValue::from(true);
// 検証付き作成(Result型を返す)
$result = BooleanValue::tryFrom(true);
if ($result->isOk()) {
$bool = $result->unwrap();
} else {
$error = $result->unwrapErr(); // エラー情報を取得
}
// 等価性の比較
$anotherBool = BooleanValue::from(true);
$areEqual = $bool->equals($anotherBool); // true
文字列値
use WizDevelop\PhpValueObject\String\StringValue;
use WizDevelop\PhpValueObject\String\EmailAddress;
// 基本的な文字列
$str = StringValue::from("Hello, World!");
echo $str; // 自動的に文字列変換
// メールアドレス(検証付き)
$emailResult = EmailAddress::tryFrom("example@example.com");
if ($emailResult->isOk()) {
$email = $emailResult->unwrap();
// メールアドレスとして保証された値を使用できる
}
数値型
use WizDevelop\PhpValueObject\Number\IntegerValue;
use WizDevelop\PhpValueObject\Number\PositiveIntegerValue;
use WizDevelop\PhpValueObject\Number\DecimalValue;
use BcMath\Number;
// 整数値
$int = IntegerValue::from(42);
// 正の整数値(0未満の値は検証エラー)
$positiveInt = PositiveIntegerValue::tryFrom(10);
// 少数値(BCMath利用で高精度計算が可能)
$decimal = DecimalValue::from(new Number("3.14159"));
// 算術演算の例
$pi = DecimalValue::from(new Number("3.14159"));
$radius = DecimalValue::from(new Number("5"));
$area = $pi->multiply($radius->square()); // πr²
日付時刻型
use WizDevelop\PhpValueObject\DateTime\LocalDate;
use WizDevelop\PhpValueObject\DateTime\LocalTime;
use WizDevelop\PhpValueObject\DateTime\LocalDateTime;
use DateTimeImmutable;
use DateTimeZone;
// 日付の作成
$date = LocalDate::of(2025, 5, 14);
$tomorrow = $date->addDays(1);
// 時刻の作成
$time = LocalTime::of(13, 30, 0);
$laterTime = $time->addHours(2);
// 日時の作成
$dateTime = LocalDateTime::of($date, $time);
// 現在時刻からの作成
$now = LocalDateTime::now(new DateTimeZone('Asia/Tokyo'));
// DateTimeImmutableとの相互変換
$nativeDate = $dateTime->toDateTimeImmutable();
$backToLocalDateTime = LocalDateTime::from($nativeDate);
コレクション
ArrayList(順序付きリスト)
use WizDevelop\PhpValueObject\Collection\ArrayList;
// 不変のリスト作成
$list = ArrayList::from([1, 2, 3, 4, 5]);
// 様々な操作(すべて新しいインスタンスを返す)
$filteredList = $list->filter(fn($value) => $value > 2); // [3, 4, 5]
$mappedList = $list->map(fn($value) => $value * 2); // [2, 4, 6, 8, 10]
$sortedList = $list->sort(fn($a, $b) => $b <=> $a); // [5, 4, 3, 2, 1]
$concatList = $list->concat(ArrayList::from([6, 7, 8])); // [1, 2, 3, 4, 5, 6, 7, 8]
Map(連想配列)
use WizDevelop\PhpValueObject\Collection\Map;
// キーと値のペアを扱う不変のマップ
$map = Map::make(['name' => 'John', 'age' => 30]);
// 操作例
$hasKey = $map->has('name'); // true
$values = $map->values(); // ArrayList::from(['John', 30])
$keys = $map->keys(); // ArrayList::from(['name', 'age'])
$filteredMap = $map->filter(fn($value) => is_string($value)); // ['name' => 'John']
$updatedMap = $map->put('age', 31); // ['name' => 'John', 'age' => 31]
値オブジェクトのコレクション
use WizDevelop\PhpValueObject\Collection\ArrayList;
use WizDevelop\PhpValueObject\ValueObjectList;
use WizDevelop\PhpValueObject\String\StringValue;
// 値オブジェクトのリスト作成
$stringList = ArrayList::from([
StringValue::from('apple'),
StringValue::from('banana'),
StringValue::from('orange')
]);
// ValueObjectListへの変換 - 値オブジェクトの等価性に基づいた操作
$valueObjectList = new ValueObjectList($stringList->toArray());
$hasApple = $valueObjectList->has(StringValue::from('apple')); // true
カスタム値オブジェクトの作成
既存の値オブジェクトを拡張して、ドメイン固有の値オブジェクトを簡単に作成できます:
use Override;
use WizDevelop\PhpValueObject\String\StringValue;
use WizDevelop\PhpValueObject\ValueObjectMeta;
#[ValueObjectMeta(name: '商品コード')]
final readonly class ProductCode extends StringValue
{
#[Override]
final public static function minLength(): int
{
return 5;
}
#[Override]
final public static function maxLength(): int
{
return 5;
}
#[Override]
final protected static function regex(): string
{
return '/^P[0-9]{4}$/';
}
}
この例では、商品コードという特定のフォーマット(先頭がPで始まり、その後に4桁の数字が続く5文字固定の文字列)を表現する値オブジェクトを定義しています。
エラーハンドリング
このライブラリでは php-monad ライブラリのResult型を活用して、エラーハンドリングを型安全に行います:
php-monad についてはこちらの記事を参照してください。
use WizDevelop\PhpValueObject\String\EmailAddress;
// 検証付き作成
$validResult = EmailAddress::tryFrom("valid@example.com");
$invalidResult = EmailAddress::tryFrom("invalid-email");
// 成功した場合
if ($validResult->isOk()) {
$email = $validResult->unwrap();
// 値オブジェクトを使用
}
// 失敗した場合
if ($invalidResult->isErr()) {
$error = $invalidResult->unwrapErr();
echo $error->message; // エラーメッセージを取得
}
ユースケース
例1: フォームバリデーション
use WizDevelop\PhpValueObject\String\EmailAddress;
use WizDevelop\PhpValueObject\String\StringValue;
use WizDevelop\PhpValueObject\Number\PositiveIntegerValue;
// フォームからのデータ
$formData = [
'name' => $_POST['name'] ?? '',
'email' => $_POST['email'] ?? '',
'age' => $_POST['age'] ?? '',
];
// 値オブジェクトによる検証
$nameResult = StringValue::tryFrom($formData['name']);
$emailResult = EmailAddress::tryFrom($formData['email']);
$ageResult = PositiveIntegerValue::tryFrom((int)$formData['age']);
// エラーの収集
$errors = [];
if ($nameResult->isErr()) {
$errors['name'] = $nameResult->unwrapErr()->message;
}
if ($emailResult->isErr()) {
$errors['email'] = $emailResult->unwrapErr()->message;
}
if ($ageResult->isErr()) {
$errors['age'] = $ageResult->unwrapErr()->message;
}
// 検証が通った場合のみ処理を続行
if (empty($errors)) {
$user = createUser(
$nameResult->unwrap(),
$emailResult->unwrap(),
$ageResult->unwrap()
);
// ...
}
例2: ドメインロジックの表現
use WizDevelop\PhpValueObject\Number\DecimalValue;
use BcMath\Number;
// ドメイン固有の金額値オブジェクト
final readonly class Money extends DecimalValue
{
public static function yen(string|int|float|Number $amount): self
{
return self::from(Number::fromString((string)$amount));
}
public function applyTax(float $rate): self
{
$taxRate = Number::fromString((string)(1 + $rate));
return $this->multiply($taxRate);
}
public function __toString(): string
{
return '¥' . $this->value;
}
}
// 使用例
$price = Money::yen(1000);
$withTax = $price->applyTax(0.1); // 10%の消費税を適用
echo $withTax; // ¥1100
まとめ
PHP Value Objectライブラリは、ドメイン駆動設計における値オブジェクトの実装を簡素化し、PHPアプリケーションの堅牢性と保守性を向上させます。このライブラリを使用することで以下のメリットが得られます:
- 型安全な設計: 厳格な型チェックによりバグを早期に発見
- ドメインの制約を表現: ビジネスルールをコードに直接組み込み
- 一貫したエラーハンドリング: Result型による予測可能なエラー処理
- テストしやすいコード: 純粋関数的なアプローチによる副作用の分離
- ドメインの知識を集約: ビジネスロジックが値オブジェクト内に集約され、重複を防止
PHP 8.4以上のプロジェクトでご利用いただけますので、ぜひお試しください。詳細はGitHubリポジトリをご覧ください。
ライセンス
PHP Value Objectライブラリは、MITライセンスの下で公開されています。詳細はLICENSEファイルをご覧ください。