othercodes / ddd-value-object
Small library to easily apply the Value Object Pattern.
Requires
- ext-json: *
- konekt/enum: ^3.0
- nesbot/carbon: ^2.40
- ramsey/uuid: ^4.1
Requires (Dev)
- fzaninotto/faker: ^1.9
- mockery/mockery: ^1.4
- phpunit/phpunit: ^9.3
This package is auto-updated.
Last update: 2021-06-16 09:08:00 UTC
README
Small library to easily manage the Value Object Pattern.
Installation
Use the following command to install with composer
:
composer require othercode/ddd-value-object
This will automatically get the latest version and configure a composer.json
file.
Alternatively you can create the following composer.json
file and run composer install
to install it.
{ "require": { "othercode/ddd-value-object": "*" } }
Usage
Build a Value object is quite simple, you just need to extend the ValueObject
Next, initialize the values in the
constructor by using the initialize
method. Finally, add invariant rules as protected method, using the prefix
invariant
(this prefix can be customized) and execute them with the checkInvariants
method.
<?php declare(strict_types=1); class Speed extends OtherCode\DDDValueObject\ValueObject { public const KILOMETERS_PER_HOUR = 'km/h'; public const MILES_PER_HOUR = 'm/h'; public function __construct(int $amount, string $magnitude) { $this->initialize([ 'amount' => $amount, 'magnitude' => $magnitude ]); $this->checkInvariants(); } protected function invariantSpeedMustBeGreaterThanZero(): bool { return $this->amount() > 0; } protected function invariantMagnitudeMustBeValid(): bool { return in_array($this->magnitude(), [ self::KILOMETERS_PER_HOUR, self::MILES_PER_HOUR ]); } public function amount(): int { return $this->get('amount'); } public function magnitude(): string { return $this->get('magnitude'); } public function increase(Speed $speed): self { if ($speed->magnitude() !== $this->magnitude()) { throw new InvalidArgumentException('The given magnitude is not valid.'); } return new self($this->amount() + $speed->amount(), $this->magnitude()); } public function __toString(): string { return $this->amount() . $this->magnitude(); } }
Equality
Value equality is computed by serializing the object and hashing it with the sha256 algorithm. Alternatively, you can
override equalityHash
to calculate a proper hash for the object. This hash is used to check if the value objects are
equals or not.
<?php declare(strict_types=1); class Speed extends OtherCode\DDDValueObject\ValueObject { // ... public function equalityHash(): string { return md5(sprintf('$s %s', $this->amount(), $this->magnitude()); } // ... }
Immutability
The immutability property blocks any attempt of value modification, that will end in exception:
$s = new Speed(120, 'km/h'); $s->amount = 123; // PHP Fatal error: Uncaught OtherCode\DDDValueObject\Exceptions\ImmutableValueException: Illegal attempt to change immutable value.
You can customize the exception that will be thrown by overriding the immutabilityException
property. The same happens
with the error message, you just need to override the immutabilityMessages
property.
<?php declare(strict_types=1); class Speed extends OtherCode\DDDValueObject\ValueObject { // ... protected string $immutabilityException = SomeCustomException::class; protected array $immutabilityMessages = [ 'default' => 'You shall not update this value!.', ]; // ... }
Invariants
The invariants methods must return a boolean value, true
if the invariant is successfully, false
otherwise. If any
in variant is violated you will get an exception:
<?php $s = new Speed(-1, 'm/s'); // PHP Fatal error: Uncaught InvalidArgumentException: Unable to create Speed value object due: // invariant speed must be greater than zero // invariant magnitude must be valid
By default, the invariant name is parsed and used as message error on invariant violation, but this can be easily
customized, you just need to throw an exception with your custom message instead returning false
in the invariants:
<?php declare(strict_types=1); class Speed extends OtherCode\DDDValueObject\ValueObject { // ... protected function invariantSpeedMustBeGreaterThanZero(): bool { if($this->amount() < 0) { throw new InvalidArgumentException('The given speed value is not valid'); } return true; } // ... } $s = new Speed(-1, 'm/s'); // PHP Fatal error: Uncaught InvalidArgumentException: Unable to create Speed value object due: // The given speed value is not valid // invariant magnitude must be valid
Additionally, you can fully customize how the invariant violation are managed by passing a custom function to the
checkInvariants
method:
<?php declare(strict_types=1); class Speed extends OtherCode\DDDValueObject\ValueObject { // ... public function __construct(int $amount, string $magnitude) { $this->initialize([ 'amount' => $amount, 'magnitude' => $magnitude ]); $this->checkInvariants(function (array $violations) { throw new RuntimeException("Epic fail due:\n-" . implode("\n-", $violations) . "\n"); }); } protected function invariantSpeedMustBeGreaterThanZero(): bool { if($this->amount() < 0) { throw new InvalidArgumentException('The given speed value is not valid'); } return true; } // ... } $s = new Speed(-120, 'clicks/s'); // PHP Fatal error: Uncaught RuntimeException: Epic fail due: // - The given speed value is not valid // - invariant magnitude must be valid