Value Object در PHP — Email، Money و domain types امن
primitive obsession یعنی همهجا string $email و float $price — تا bug فرمت، واحد پول اشتباه و validation تکراری. Value Object (VO) type با معناست که invariant خودش را حفظ میکند: اگر Email ساخته شد، معتبر است.
تعریف Value Object
- بدون identity (دو Email با همان value برابرند)
- Immutable — تغییر = instance جدید
- validation در constructor
- equality بر اساس value نه reference
Email ساده
readonly class Email
{
public function __construct(public string $value)
{
if (! filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email: {$value}");
}
}
public static function fromString(string $value): self
{
return new self($value);
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
public function __toString(): string
{
return $this->value;
}
}
Money — واحد و گرد کردن
readonly class Money
{
public function __construct(
public int $amountInRials,
public string $currency = 'IRR',
) {
if ($amountInRials < 0) {
throw new InvalidArgumentException('Amount cannot be negative');
}
}
public function add(self $other): self
{
$this->assertSameCurrency($other);
return new self($this->amountInRials + $other->amountInRials, $this->currency);
}
private function assertSameCurrency(self $other): void
{
if ($this->currency !== $other->currency) {
throw new InvalidArgumentException('Currency mismatch');
}
}
}
مبلغ به rials integer — از float برای پول دوری کنید.
VO vs DTO
DTO container چند field برای transfer؛ VO یک concept با rule. CreateOrderData میتواند Email $customerEmail داشته باشد.
Eloquent Cast سفارشی
class EmailCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes): ?Email
{
return $value ? Email::fromString($value) : null;
}
public function set($model, string $key, $value, array $attributes): ?string
{
if ($value instanceof Email) {
return $value->value;
}
return $value ? (string) Email::fromString($value) : null;
}
}
protected function casts(): array
{
return ['contact_email' => EmailCast::class];
}
Slug و Phone ایران
readonly class IranianMobile
{
public function __construct(public string $digits)
{
if (! preg_match('/^09\d{9}$/', $digits)) {
throw new InvalidArgumentException('Invalid Iranian mobile');
}
}
}
enum vs Value Object
enum برای مجموعه ثابت (Status). VO برای validated string/number با عملیات (Money.add). گاه هر دو کنار هم.
تست VO
it('rejects invalid email', function () {
expect(fn () => new Email('not-an-email'))->toThrow(InvalidArgumentException::class);
});
سریع — بدون DB. Unit test.
کی VO نسازیم؟
فیلد یکبار مصرف بدون rule — string کافی. VO وقتی validation یا رفتار domain دارد.
جمعبندی
VO primitive obsession را کم میکند. با Clean Code و SOLID همراستا. Action/Service از VO در signature استفاده کنند.