مقاله

ساخت Value Object با readonly و validation در constructor — invariant domain به‌جای string و float پراکنده.

Value Object در PHP — Email، Money و domain types امن

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];
}

Eloquent Casts.

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 استفاده کنند.

domain modeling