Php value object example

Writing value objects in PHP

When designing a value object, you should pay attention to its three main characteristics: immutability, structural equality, and self-validation.

 declare(strict_types=1); final class Price  const USD = 'USD'; const CAD = 'CAD'; /** @var float */ private $amount; /** @var string */ private $currency; public function __construct(float $amount, string $currency = 'USD')  if ($amount  0)  throw new \InvalidArgumentException("Amount should be a positive value: $amount>."); > if (!in_array($currency, $this->getAvailableCurrencies()))  throw new \InvalidArgumentException("Currency should be a valid one: $currency>."); > $this->amount = $amount; $this->currency = $currency; > private function getAvailableCurrencies(): array  return [self::USD, self::CAD]; > public function getAmount(): float  return $this->amount; > public function getCurrency(): string  return $this->currency; > > 

Immutability

Once you instantiate a value object, it should be the same for the rest of the application lifecycle. If you need to change its value, it should be done by entirely replacing that object.

Using mutable value objects is acceptable if you are using them entirely within a local scope, with only one reference to the object. Otherwise, you may have problems.

Taking the previous example, here’s how you can update the amount of a Price type:

 declare(strict_types=1); final class Price  // . private function hasSameCurrency(Price $price): bool  return $this->currency === $price->currency; > public function sum(Price $price): self  if (!$this->hasSameCurrency($price))  throw \InvalidArgumentException( "You can only sum values with the same currency: $this->currency> !== $price->currency>." ); > return new self($this->amount + $price->amount, $this->currency); > > 

Structural Equality

Value objects don’t have an identifier. In other words, if two value objects have the same internal values, they must be considered equal. As PHP doesn’t have a way to override the equality operator, you should implement it by yourself.

You can create a specialized method to do that:

 declare(strict_types=1); final class Price  // . public function isEqualsTo(Price $price): bool  return $this->amount === $price->amount && $this->currency === $price->currency; > > 

Another option is to create a hash based on its properties:

 declare(strict_types=1); final class Price  // . private function hash(): string  return md5("$this->amount>$this->currency>"); > public function isEqualsTo(Price $price): bool  return $this->hash() === $price->hash(); > > 

Self-Validation

The validation of a value object should occur on its creation. If any of its properties are invalid, it should throw an exception. Putting this together with immutability, once you create a value object, you can be sure it will always be valid.

Taking the Price type example once again, it doesn’t make sense to have a negative amount for the domain of the application:

 declare(strict_types=1); final class Price  // . public function __construct(float $amount, string $currency = 'USD')  if ($amount  0)  throw new \InvalidArgumentException("Amount should be a positive value: $amount>."); > if (!in_array($currency, $this->getAvailableCurrencies()))  throw new \InvalidArgumentException("Currency should be a valid one: $currency>."); > $this->amount = $amount; $this->currency = $currency; > > 

Using with Doctrine

Storing and retrieving value objects from the database using Doctrine is quite easy using Embeddable s. According to the documentation, Embeddable s are not entities. But, you embed them in entities, which makes them perfect for dealing with value objects.

Let’s suppose you have a Product class, and you would like to store the price in that class. You will end up with the following modeling:

 declare(strict_types=1); /** @Embeddable */ final class Price  /** @Column(type="float") */ private $amount; /** @Column(type="string") */ private $currency; public function __construct(float $amount, string $currency = 'USD')  // . $this->amount = $amount; $this->currency = $currency; > // . > /** @Entity */ class Product  /** @Embedded(class="Price") */ private $price; public function __construct(Price $price)  $this->price = $price; > > 

Doctrine will automatically create the columns from the Price class into the table of the Product class. By default, it prefixes the database columns after the Embeddable class name, in this case: price_amount and price_currency .

Conclusion

Value objects are useful for writing clean code. Instead of writing:

public function addPhoneNumber(string $phone): void <> 
public function addPhoneNumber(PhoneNumber $phone): void <> 

Which makes it easy to read and reason about it, also you don’t need to figure out which phone format you should use.

Since their attributes define them, and you can share them with other different entities, they can be cacheable forever.

They can help you to reduce duplication. Instead of having multiples amount and currency fields, you can use a pure Price class.

Of course, like everything in life, you should not abuse of value objects. Imagine you converting tons of objects to primitive values to store them in the database, or converting those back to value objects when fetching them from the database.Indeed, you can have performance issues. Also, having tons of granular value objects can bloat your codebase.

With value objects, you can reduce the primitive obsession. Use them to represent a field or group of fields of your domain that require validation or can cause ambiguity if you use primitive values.

Thanks for reading, and happy coding!

Further Reading

Источник

DDD в PHP: Value Object или Объект-Значение Перевод

Объект-Значение (Value Object) — это объект, который представляет собой понятие из предметной области. В DDD (Domain Driven Development — разработка на основе предметной области, или предметно-ориентированное программирование) важно то, что Value Object поддерживает и обогащает Единый Язык вашей Предметной Области. Это не только примитивы, которые представляют собой некоторые значения, — они являются полноправными гражданами Предметной Области, которые формируют поведение вашего приложения.

Хорошие примеры Value Object-ов, упомянутые у Мартина, — деньги и время. При создании ГИС-приложений вы можете прийти к такому Объекту-Значению, как Location($lat, $long) , который будет инкапсулировать широту/долготу и подобное. Вопрос, который вы, вероятно, захотите задать — почему это лучше, чем просто передать два float`а в массиве и называть это $location ?

Преимущества использования Value Objects

Самое главное заключается в том, что эти объекты отражают язык, на котором вы разговариваете с другими разработчиками — когда вы говорите «Место»(Location) все знают, что это значит. Второе преимущество заключается в том, что Value Object может валидировать значение — подходит оно или нет для того, чтобы создать такой объект.

Третьим преимуществом является то, что вы можете полагаться на тип — вы знаете, что если такой Value Object был принят в качестве аргумента, он будет всегда в допустимом состоянии и вам не нужно беспокоиться об этом. И также Value Object может содержать некоторые специализированные методы, которые имеют смысл только в контексте этого значения и могут быть расположены в этом объекте (не нужно создавать странные классы-утилиты).

Пример Value Object

В качестве примера Value Object-а, который является распространённым для всех веб-приложений, я создал EmailAddress:

class EmailAddress < private $address; public function __construct($address) < if (!filter_var($address, FILTER_VALIDATE_EMAIL)) < throw new InvalidArgumentException(sprintf('"%s" is not a valid email', $address)); >$this->address = $address; > public function __toString() < return $this->address; > public function equals(EmailAddress $address) < return strtolower((string) $this) === strtolower((string) $address); >>
  • обеспечивает, что Объект-Значение EmailAddress всегда находится в допустимом состоянии;
  • позволяет использовать подсказки типов (тайп-хинтинг) и убрать проверки email-ов (впоследствии упростить логику приложения);
  • предоставляет возможность приведения к строке;
  • предоставляет метод для сравнения его с другими EmailAddress.

Одержимость примитивами

Вы можете неохотно относиться к использованию объектов в качестве контейнеров для примитивных значений, но подобные вещи описаны как «код с запашком» под названием «Одержимость примитивами»:

Одержимость примитивами — это использование примитивных типов данных для представления сущностей Предметной области. Например, мы используем String для представления сообщения, Integer в качестве суммы денег, или Struct/Dictionary/Hash для представления конкретного объекта.

Использование Value Object-ов является одной из стратегий борьбы с этим запахом. Ключевая идея здесь — это собрать поведение данных вокруг своего объекта. В противном случае такие действия будут разбросаны по всему коду, что может привести к ненужной сложности и заставит вас относиться с опаской к значениям, переданным в методы.

Неизменяемость

Очень важно помнить и понимать, что Value Object является неизменяемым. Почему это такое важное понятие? Задумайтесь о реальности вокруг вас и какие значения вы используете — число один, красный цвет и так далее. Вы не можете изменить эти значения — это не имеет смысла, менять красный цвет на зеленый, и продолжать называть его красным — это больше не красный, и называние зеленого красным будет путать людей. Таким же образом Value Object в вашем приложении должны быть неизменными.

Например, у вас есть сущность «Собрание» в вашей Предметной Области, и у этого Собрания есть некоторая Дата (Value Object). Теперь дата встречи изменилась, но не сама дата — 22 марта все еще 22 марта. Это собрание, которое требует назначения новой даты, а не изменения самой даты, поэтому выкиньте старую дату и создайте новую.

Резюме

Value Object-ы являются важными гражданами вашей Предметной Области, которые отражают его концепции. Убедитесь, что вы приложите соответствующее поведение к таким объектам, которое впоследствии будет делать код более организованным и лучше передавать реальность, потому что ключевым аспектом и целью объектно-ориентированного программирования является моделирование реального мира.

Последние посты

  • 2023-02-03Новая мажорная версия Flysystem
  • 2020-12-31Конечные автоматы для Eloquent
  • 2020-12-30Создаём REST API с помощью Laravel Orion
  • 2020-12-29При разработке локально используйте queue:listen вместо queue:work
  • 2020-12-28Laravel Desktop Notifier

Источник

Читайте также:  Python gui open file
Оцените статью