📜 ⬆️ ⬇️

DDD in practice. Design a wish list

There is a lot of scattered material on DDD on the Internet. Except for the blue book, these are mostly short articles with a theory, drawn from the same book, and which intersects little with practice. It is possible, of course, that I was simply looking badly, but I have long wanted to find some integral example, as they say, “from and to”. And I decided to create such an example on Symfony 3 and VueJS. Just want to say that I have been studying DDD recently, so I took a fairly simple subject area - the wish list.


A wish list


Anyone ever buys something. Be it a new phone, a gift, a trip abroad or even an apartment. The wish list, as an addition to the “hard money” money box, is designed to help track the accumulated funds for each of the wishes and make these funds constantly increase. For example, today I decided to start saving money on a new laptop: I’ll add a wish and start saving money. And tomorrow I want to calculate how much money will need to be put off daily, so that in six months I could buy a good gift for my wife.


A wish


The desires that we consider can be satisfied by buying something for money. From this it follows that every desire has a cost , an initial fund (if you started saving money before you decided to add a wish to the list) and accumulated funds - a fund that is expressed by the sum of all contributions . A deposit is a lump sum of money for a specific desire. Since desires require regular investment, it would be nice to determine the base rate , below which the amount of the deposit can not be. In addition, we should be able to track contributions to any of the desires, in order to withdraw them, if necessary. With the accumulation of a sufficient amount of funds desire becomes fulfilled. If there is an excess of cash, then it can be redistributed to his other desires (about this in one of the following articles).


Design Entities


Based on the above requirements, we can encode two entities: Wish (desire) and Deposit (contribution).


Desire: Entity Designer


Let's start with a desire and think about what fields we need and how we design the entity constructor. The first thing that comes to mind is something like this code:


 <?php namespace Wishlist\Domain; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; class Wish { private $id; private $name; private $price; private $fee; private $deposits; private $initialFund; private $createdAt; private $updatedAt; public function __construct( string $name, int $price, int $fee, int $initialFund ) { $this->name = $name; $this->price = $price; $this->fee = $fee; $this->initialFund = $initialFund; $this->deposits = new ArrayCollection(); $this->createdAt = $createdAt ?? new DateTimeImmutable(); $this->updatedAt = $createdAt ?? new DateTimeImmutable(); } } 

However, there are a number of problems:


  1. Surrogate key used
  2. No validation fields
  3. If validation is encoded in the constructor, it will become even more monstrous.
  4. No information about the currency in which the calculations are carried out
  5. The constructor is overloaded with arguments.

What are we going to do? There is a solution to use value objects. Then the constructor of our entity will be transformed as follows:


 <?php namespace Wishlist\Domain; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; class Wish { private $id; private $name; private $expense; private $deposits; private $published = false; private $createdAt; private $updatedAt; public function __construct( WishId $id, WishName $name, Expense $expense, DateTimeImmutable $createdAt = null ) { $this->id = $id; $this->name = $name; $this->expense = $expense; $this->deposits = new ArrayCollection(); $this->createdAt = $createdAt ?? new DateTimeImmutable(); $this->updatedAt = $createdAt ?? new DateTimeImmutable(); } } 

We used three value objects:


  1. WishId , which is a UUID generated using the ramsey / uuid library
  2. WishName - the name of the desire
  3. Expense , which represents "spending" on desire: cost, base rate and initial fund (perhaps not the most successful name, but I did not invent another one)

You may ask: why did the date of its creation hit the entity constructor? I will answer you: this is done to facilitate the writing of tests and is not used anywhere except for tests. Perhaps not the best solution, of course.


Well, since we used value objects, it would be nice to look at their implementation. To begin with, we will think about how to implement identifiers (looking ahead, I’ll say that in addition to WishId , we will also have DepositId ). To do this, we will write a simple test using the example of one of them (the essence is the same, so there is no point in writing two different tests):


 <?php namespace Wishlist\Tests\Domain; use Wishlist\Domain\WishId; use PHPUnit\Framework\TestCase; class IdentityTest extends TestCase { public function testFromValidString() { $string = '550e8400-e29b-41d4-a716-446655440000'; $wishId = WishId::fromString($string); static::assertInstanceOf(WishId::class, $wishId); static::assertEquals($string, $wishId->getId()); static::assertEquals($string, (string) $wishId); } public function testEquality() { $string = '550e8400-e29b-41d4-a716-446655440000'; $wishIdOne = WishId::fromString($string); $wishIdTwo = WishId::fromString($string); $wishIdThree = WishId::next(); static::assertTrue($wishIdOne->equalTo($wishIdTwo)); static::assertFalse($wishIdTwo->equalTo($wishIdThree)); } } 

Based on these tests, we can make a basic class of identifiers that contains common logic:


 <?php namespace Wishlist\Domain; use Ramsey\Uuid\Exception\InvalidUuidStringException; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use Wishlist\Domain\Exception\InvalidIdentityException; abstract class AbstractId { protected $id; private function __construct(UuidInterface $id) { $this->id = $id; } public static function fromString(string $id) { try { return new static(Uuid::fromString($id)); } catch (InvalidUuidStringException $exception) { throw new InvalidIdentityException($id); } } public static function next() { return new static(Uuid::uuid4()); } public function getId(): string { return $this->id->toString(); } public function equalTo(AbstractId $id): bool { return $this->getId() === $id->getId(); } public function __toString(): string { return $this->getId(); } } 

And the "real" identifiers are just to "get rid of" him:


 <?php namespace Wishlist\Domain; final class WishId extends AbstractId { // } 

And the same with DepositId:


 <?php namespace Wishlist\Domain; final class DepositId extends AbstractId { // } 

Now consider WishName . This is the simplest object value, and we only need the name not to be empty. Let's first write the tests:


 <?php namespace Wishlist\Tests\Domain; use Wishlist\Domain\WishName; use PHPUnit\Framework\TestCase; class WishNameTest extends TestCase { /** * @expectedException \InvalidArgumentException */ public function testShouldNotCreateWithEmptyString() { new WishName(''); } public function testGetValueShouldReturnTheName() { $expected = 'A bucket of candies'; $name = new WishName($expected); static::assertEquals($expected, $name->getValue()); static::assertEquals($expected, (string) $name); } } 

Now let's actually code WishName . By the way, for checking here and further, we will use the very convenient webmozart / assert library:


 <?php namespace Wishlist\Domain; use Webmozart\Assert\Assert; final class WishName { private $name; public function __construct(string $name) { Assert::notEmpty($name, 'Name must not be empty.'); $this->name = $name; } public function getValue(): string { return $this->name; } public function __toString(): string { return $this->getValue(); } } 

We now turn to a more interesting object-value - Expense . It is designed to monitor the correct values ​​of the cost, base rate and initial fund. We will help him in this by defining the requirements:


  1. Cost can only be a positive number.
  2. The same applies to the base rate.
  3. Initial fund cannot be a negative number if specified

In addition, the following restrictions apply to properties:


  1. Base rate must be less than the cost
  2. The initial fund must also be less than the cost.

Since we also need a currency, we will not use “bare” int 's to work with money, but use the moneyphp / money library. Considering the above, about Expense , we will write the following tests:


 <?php namespace Wishlist\Tests\Domain; use Money\Currency; use Money\Money; use Wishlist\Domain\Expense; use PHPUnit\Framework\TestCase; class ExpenseTest extends TestCase { /** * @expectedException \InvalidArgumentException * @dataProvider nonsensePriceDataProvider */ public function testPriceAndFeeMustBePositiveNumber($price, $fee, $initialFund) { Expense::fromCurrencyAndScalars(new Currency('USD'), $price, $fee, $initialFund); } public function nonsensePriceDataProvider() { return [ 'Price must be greater than zero' => [0, 0, 0], 'Fee must be greater than zero' => [1, 0, 0], 'Price must be positive' => [-1, -1, 0], 'Fee must be positive' => [1, -1, 0], 'Initial fund must be positive' => [2, 1, -1], ]; } /** * @expectedException \InvalidArgumentException */ public function testFeeMustBeLessThanPrice() { Expense::fromCurrencyAndScalars(new Currency('USD'), 100, 150); } /** * @expectedException \InvalidArgumentException */ public function testInitialFundMustBeLessThanPrice() { Expense::fromCurrencyAndScalars(new Currency('USD'), 100, 50, 150); } /** * @expectedException \InvalidArgumentException */ public function testNewPriceMustBeOfTheSameCurrency() { $expense = Expense::fromCurrencyAndScalars(new Currency('USD'), 100, 50, 25); $expense->changePrice(new Money(200, new Currency('RUB'))); } public function testChangePriceMustReturnANewInstance() { $expense = Expense::fromCurrencyAndScalars(new Currency('USD'), 100, 50, 25); $actual = $expense->changePrice(new Money(200, new Currency('USD'))); static::assertNotSame($expense, $actual); static::assertEquals(200, $actual->getPrice()->getAmount()); } /** * @expectedException \InvalidArgumentException */ public function testNewFeeMustBeOfTheSameCurrency() { $expense = Expense::fromCurrencyAndScalars(new Currency('USD'), 100, 50, 25); $expense->changeFee(new Money(200, new Currency('RUB'))); } public function testChangeFeeMustReturnANewInstance() { $expense = Expense::fromCurrencyAndScalars(new Currency('USD'), 100, 10, 25); $actual = $expense->changeFee(new Money(20, new Currency('USD'))); static::assertNotSame($expense, $actual); static::assertEquals(20, $actual->getFee()->getAmount()); } } 

In them, among other things, laid the possibility of changing the value and the base rate of desire. Therefore, the class contains two additional tests for matching currencies.


Now we can encode Expense :


 <?php namespace Wishlist\Domain; use Money\Currency; use Money\Money; use Webmozart\Assert\Assert; final class Expense { private $price; private $fee; private $initialFund; private function __construct(Money $price, Money $fee, Money $initialFund) { $this->price = $price; $this->fee = $fee; $this->initialFund = $initialFund; } public static function fromCurrencyAndScalars( Currency $currency, int $price, int $fee, int $initialFund = null ) { foreach ([$price, $fee] as $argument) { Assert::notEmpty($argument); Assert::greaterThan($argument, 0); } Assert::lessThan($fee, $price, 'Fee must be less than price.'); if (null !== $initialFund) { Assert::greaterThanEq($initialFund, 0); Assert::lessThan($initialFund, $price, 'Initial fund must be less than price.'); } return new static( new Money($price, $currency), new Money($fee, $currency), new Money($initialFund ?? 0, $currency) ); } public function getCurrency(): Currency { return $this->price->getCurrency(); } public function getPrice(): Money { return $this->price; } public function changePrice(Money $amount): Expense { Assert::true($amount->getCurrency()->equals($this->getCurrency())); return new static($amount, $this->fee, $this->initialFund); } public function getFee(): Money { return $this->fee; } public function changeFee(Money $amount): Expense { Assert::true($amount->getCurrency()->equals($this->getCurrency())); return new static($this->price, $amount, $this->initialFund); } public function getInitialFund(): Money { return $this->initialFund; } } 

So, we looked at all the value objects that are used by the Wish entity, with its constructor determined, so now it's time to go directly to the business logic.


Desire: saving money


Imagine an ordinary piggy bank. There put coins or pieces of paper of a certain denomination and currency. Those. Contribution is made to the piggy bank. As soon as the piggy bank is filled to the top, it is broken. So it is with us with our desires: we invest some amount of money in them, and when a sufficient amount is accumulated, we believe that the wish is fulfilled (you can go to the store :) and therefore it is already meaningless to make contributions to it. There is another small limitation: contributions can be made only if the wish is published (for example, you can postpone it until better times).


It's time to write tests again.


 <?php namespace Wishlist\Tests\Domain; use DateInterval; use DateTimeImmutable; use Money\Currency; use Money\Money; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Wishlist\Domain\DepositId; use Wishlist\Domain\Expense; use Wishlist\Domain\Wish; use Wishlist\Domain\WishId; use Wishlist\Domain\WishName; class WishTest extends TestCase { /** * @expectedException \Wishlist\Domain\Exception\DepositIsTooSmallException */ public function testMustDeclineDepositIfItIsLessThanFee() { $wish = $this->createWishWithPriceAndFee(1000, 100); $wish->publish(); $wish->deposit(new Money(50, new Currency('USD'))); } public function testExtraDepositMustFulfillTheWish() { $wish = $this->createWishWithPriceAndFund(1000, 900); $wish->publish(); $wish->deposit(new Money(150, new Currency('USD'))); static::assertTrue($wish->isFulfilled()); } /** * @expectedException \Wishlist\Domain\Exception\WishIsUnpublishedException */ public function testMustNotDepositWhenUnpublished() { $wish = $this->createWishWithEmptyFund(); $wish->deposit(new Money(100, new Currency('USD'))); } /** * @expectedException \Wishlist\Domain\Exception\WishIsFulfilledException */ public function testMustNotDepositWhenFulfilled() { $fulfilled = $this->createWishWithPriceAndFund(500, 450); $fulfilled->publish(); $fulfilled->deposit(new Money(100, new Currency('USD'))); $fulfilled->deposit(new Money(100, new Currency('USD'))); } public function testDepositShouldAddDepositToInternalCollection() { $wish = $this->createWishWithEmptyFund(); $wish->publish(); $depositMoney = new Money(150, new Currency('USD')); $wish->deposit($depositMoney); $deposits = $wish->getDeposits(); static::assertCount(1, $deposits); static::assertArrayHasKey(0, $deposits); $deposit = $deposits[0]; static::assertTrue($deposit->getMoney()->equals($depositMoney)); static::assertSame($wish, $deposit->getWish()); } /** * @expectedException \InvalidArgumentException */ public function testDepositAndPriceCurrenciesMustMatch() { $wish = $this->createWishWithEmptyFund(); $wish->publish(); $wish->deposit(new Money(125, new Currency('RUB'))); } private function createWishWithEmptyFund(): Wish { return new Wish( WishId::next(), new WishName('Bicycle'), Expense::fromCurrencyAndScalars( new Currency('USD'), 1000, 100 ) ); } private function createWishWithPriceAndFund(int $price, int $fund): Wish { return new Wish( WishId::next(), new WishName('Bicycle'), Expense::fromCurrencyAndScalars( new Currency('USD'), $price, 10, $fund ) ); } } 

To make the tests work, we add several methods to the essence of Wish :


 <?php namespace Wishlist\Domain; // ... //    use   use Wishlist\Domain\Exception\DepositIsTooSmallException; use Wishlist\Domain\Exception\WishIsFulfilledException; use Wishlist\Domain\Exception\WishIsUnpublishedException; // ... public function deposit(Money $amount): Deposit { $this->assertCanDeposit($amount); $deposit = new Deposit(DepositId::next(), $this, $amount); $this->deposits->add($deposit); return $deposit; } private function assertCanDeposit(Money $amount) { if (!$this->published) { throw new WishIsUnpublishedException($this->getId()); } if ($this->isFulfilled()) { throw new WishIsFulfilledException($this->getId()); } if ($amount->lessThan($this->getFee())) { throw new DepositIsTooSmallException($amount, $this->getFee()); } Assert::true( $amount->isSameCurrency($this->expense->getPrice()), 'Deposit currency must match the price\'s one.' ); } public function isFulfilled(): bool { return $this->getFund()->greaterThanOrEqual($this->expense->getPrice()); } public function publish() { $this->published = true; $this->updatedAt = new DateTimeImmutable(); } public function unpublish() { $this->published = false; $this->updatedAt = new DateTimeImmutable(); } public function getFund(): Money { return array_reduce($this->deposits->toArray(), function (Money $fund, Deposit $deposit) { return $fund->add($deposit->getMoney()); }, $this->expense->getInitialFund()); } 

Consider all these methods in turn.


  1. deposit - checks whether a deposit can be made, and if it can, it makes a contribution to the desire for the specified amount of money. To do this, create an entity of the entity Deposit and save it to the internal collection of deposits.
  2. isFulfilled - indicates whether the desire is fulfilled. Well, we have previously determined that the desire is considered fulfilled if its savings are greater than or equal to the value.
  3. publish/unpublish - publishes or puts into drafts, respectively.
  4. getFund - returns the fund, i.e. accumulated funds.

You must have noticed that the eponymous entity is used in the Wish::deposit method. Now, in order to continue to develop the business logic of desire further, we need to program the essence of Deposit . Let's do it and take care of, the good is that it is much simpler and it will not take much time.


Deposit: Designer


The contribution will have only four properties:


  1. ID, as this is an entity, and you also need to be able to manage deposits
  2. The desire this contribution relates to
  3. Amount of deposit
  4. Date of deposit

It is also necessary to take into account that the contribution cannot be zero, since it is meaningless, as if we were putting imaginary money in our piggy bank, and in our hands we would not even have a toy equivalent of money :).


As always, let's start with the tests:


 <?php namespace Wishlist\Tests\Domain; use Mockery; use Money\Currency; use Money\Money; use PHPUnit\Framework\TestCase; use Wishlist\Domain\Deposit; use Wishlist\Domain\DepositId; use Wishlist\Domain\Wish; class DepositTest extends TestCase { /** * @expectedException \InvalidArgumentException */ public function testDepositAmountMustNotBeZero() { $wish = Mockery::mock(Wish::class); $amount = new Money(0, new Currency('USD')); new Deposit(DepositId::next(), $wish, $amount); } } 

In this test, we used the mockery / mockery library in order not to fully describe the desire, since we are interested in the logic of the contribution itself. Here there is a reason for the discussion of whether it is necessary to do in the Deposit constructor a desire check, similar to that done in the Wish::deposit method. I did not do this, since the Deposit entity is not used anywhere directly, all operations with deposits, which will be discussed in the article, are carried out only in the essence of Wish .


The result is such a simple entity:


 <?php namespace Wishlist\Domain; use DateTimeImmutable; use DateTimeInterface; use Money\Money; use Webmozart\Assert\Assert; class Deposit { private $id; private $wish; private $amount; private $createdAt; public function __construct(DepositId $id, Wish $wish, Money $amount) { Assert::false($amount->isZero(), 'Deposit must not be empty.'); $this->id = $id; $this->wish = $wish; $this->amount = $amount; $this->createdAt = new DateTimeImmutable(); } public function getId(): DepositId { return $this->id; } public function getWish(): Wish { return $this->wish; } public function getMoney(): Money { return $this->amount; } public function getDate(): DateTimeInterface { return $this->createdAt; } } 

Desire: we withdraw contribution


With the essence of Deposit figured out, you can now return to programming desire. By the condition of the task, we can not only accumulate money on desire, but also withdraw deposits already made. For example, if one of them was made by mistake.


Naturally, we first add a few tests to the WishTest class:


 /** * @expectedException \Wishlist\Domain\Exception\WishIsUnpublishedException */ public function testMustNotWithdrawIfUnpublished() { $wish = $this->createWishWithPriceAndFund(500, 0); $wish->publish(); $deposit = $wish->deposit(new Money(100, new Currency('USD'))); $wish->unpublish(); $wish->withdraw($deposit->getId()); } /** * @expectedException \Wishlist\Domain\Exception\WishIsFulfilledException */ public function testMustNotWithdrawIfFulfilled() { $wish = $this->createWishWithPriceAndFund(500, 450); $wish->publish(); $deposit = $wish->deposit(new Money(100, new Currency('USD'))); $wish->withdraw($deposit->getId()); } /** * @expectedException \Wishlist\Domain\Exception\DepositDoesNotExistException */ public function testWithdrawMustThrowOnNonExistentId() { $wish = $this->createWishWithEmptyFund(); $wish->publish(); $wish->withdraw(DepositId::next()); } public function testWithdrawShouldRemoveDepositFromInternalCollection() { $wish = $this->createWishWithEmptyFund(); $wish->publish(); $wish->deposit(new Money(150, new Currency('USD'))); $wish->withdraw($wish->getDeposits()[0]->getId()); static::assertCount(0, $wish->getDeposits()); } 

As you can see, the restrictions on the withdrawal of deposits are similar to those that we wrote to make them. Now we add the necessary logic to the desire class:


 <?php namespace Wishlish\Domain; // <...> public function withdraw(DepositId $depositId) { $this->assertCanWithdraw(); $deposit = $this->getDepositById($depositId); $this->deposits->removeElement($deposit); } private function assertCanWithdraw() { if (!$this->published) { throw new WishIsUnpublishedException($this->getId()); } if ($this->isFulfilled()) { throw new WishIsFulfilledException($this->getId()); } } private function getDepositById(DepositId $depositId): Deposit { $deposit = $this->deposits->filter( function (Deposit $deposit) use ($depositId) { return $deposit->getId()->equalTo($depositId); } )->first(); if (!$deposit) { throw new DepositDoesNotExistException($depositId); } return $deposit; } 

As they say, in any incomprehensible situation, throw an exept! The withdraw method turned out to be quite simple, however we took into account all the conditions of the problem:


  1. It will not be possible to withdraw a contribution that is not
  2. We will not be able to do this if the desire is in draft or has already been fulfilled.

Desire: expect surplus savings


The function is not the most important, in fact, but it is done in the event that one day it turns out that there is no sufficient amount at hand to replenish stocks, but there is a large one. Well, or, for example, if you set aside sufficiently large amounts for a wish, and then you simply “missed”, not keeping up with the amount of funds already available. To calculate the surplus is, in fact, simple: from the value of the desire we subtract its fund and take the absolute value. If the difference was positive, the excess can be considered equal to zero.


WishTest add WishTest class WishTest new tests:


 public function testSurplusFundsMustBe100() { $wish = $this->createWishWithPriceAndFund(500, 300); $wish->publish(); $wish->deposit(new Money(100, new Currency('USD'))); $wish->deposit(new Money(200, new Currency('USD'))); $expected = new Money(100, new Currency('USD')); static::assertTrue($wish->calculateSurplusFunds()->equals($expected)); } public function testSurplusFundsMustBeZero() { $wish = $this->createWishWithPriceAndFund(500, 250); $wish->publish(); $wish->deposit(new Money(100, new Currency('USD'))); $expected = new Money(0, new Currency('USD')); static::assertTrue($wish->calculateSurplusFunds()->equals($expected)); } 

Based on the above and the written tests, we can write this method, in essence, Wish :


 <?php namespace Wishlist\Domain; // <...> public function calculateSurplusFunds(): Money { $difference = $this->getPrice()->subtract($this->getFund()); return $difference->isNegative() ? $difference->absolute() : new Money(0, $this->getCurrency()); } 

:


:



: , , . , .


. , , , .


, :


 public function testFulfillmentDatePredictionBasedOnFee() { $price = 1500; $fee = 20; $wish = $this->createWishWithPriceAndFee($price, $fee); $daysToGo = ceil($price / $fee); $expected = (new DateTimeImmutable())->add(new DateInterval("P{$daysToGo}D")); static::assertEquals( $expected->getTimestamp(), $wish->predictFulfillmentDateBasedOnFee()->getTimestamp() ); } public function testFulfillmentDatePredictionBasedOnFund() { $price = 1500; $fund = 250; $fee = 25; $wish = $this->createWish($price, $fee, $fund); $daysToGo = ceil(($price - $fund) / $fee); $expected = (new DateTimeImmutable())->add(new DateInterval("P{$daysToGo}D")); static::assertEquals( $expected->getTimestamp(), $wish->predictFulfillmentDateBasedOnFund()->getTimestamp() ); } 

, :


 public function predictFulfillmentDateBasedOnFee(): DateTimeInterface { $daysToGo = ceil( $this->getPrice() ->divide($this->getFee()->getAmount()) ->getAmount() ); return $this->createFutureDate($daysToGo); } public function predictFulfillmentDateBasedOnFund(): DateTimeInterface { $daysToGo = ceil( $this->getPrice() ->subtract($this->getFund()) ->divide($this->getFee()->getAmount()) ->getAmount() ); return $this->createFutureDate($daysToGo); } private function createFutureDate($daysToGo): DateTimeInterface { return (new DateTimeImmutable())->add(new DateInterval("P{$daysToGo}D")); } 

:


, , — , :


  1. «»
  2. Change in value

, , WishTest . :


 public function testPublishShouldPublishTheWish() { $wish = $this->createWishWithEmptyFund(); $updatedAt = $wish->getUpdatedAt(); $wish->publish(); static::assertTrue($wish->isPublished()); static::assertNotSame($updatedAt, $wish->getUpdatedAt()); } public function testUnpublishShouldUnpublishTheWish() { $wish = $this->createWishWithEmptyFund(); $updatedAt = $wish->getUpdatedAt(); $wish->unpublish(); static::assertFalse($wish->isPublished()); static::assertNotSame($updatedAt, $wish->getUpdatedAt()); } 

, :


 <?php namespace Wishlist\Domain; // <...> class Wish { // <...> public function publish() { $this->published = true; $this->updatedAt = new DateTimeImmutable(); } public function unpublish() { $this->published = false; $this->updatedAt = new DateTimeImmutable(); } public function isPublished(): bool { return $this->published; } // <...> } 

, . :


 public function testChangePrice() { $wish = $this->createWishWithPriceAndFee(1000, 10); $expected = new Money(1500, new Currency('USD')); $updatedAt = $wish->getUpdatedAt(); static::assertSame($updatedAt, $wish->getUpdatedAt()); $wish->changePrice($expected); static::assertTrue($wish->getPrice()->equals($expected)); static::assertNotSame($updatedAt, $wish->getUpdatedAt()); } public function testChangeFee() { $wish = $this->createWishWithPriceAndFee(1000, 10); $expected = new Money(50, new Currency('USD')); $updatedAt = $wish->getUpdatedAt(); static::assertSame($updatedAt, $wish->getUpdatedAt()); $wish->changeFee($expected); static::assertTrue($wish->getFee()->equals($expected)); static::assertNotSame($updatedAt, $wish->getUpdatedAt()); } 

:


 <?php namespace Wishlist\Domain; // <...> class Wish { // <...> public function changePrice(Money $amount) { $this->expense = $this->expense->changePrice($amount); $this->updatedAt = new DateTimeImmutable(); } public function changeFee(Money $amount) { $this->expense = $this->expense->changeFee($amount); $this->updatedAt = new DateTimeImmutable(); } // <...> } 

, , . :


  1. — Wish::deposit(Money $amount)
  2. — Wish::withdraw(DepositId $depositId)
  3. — Wish::predictFulfillmentDateBasedOnFee() Wish::predictFulfillmentDateBasedOnFund()
  4. — Wish::publish() Wish::unpublish()
  5. — Wish::changePrice(Money $amount) Wish::changeFee(Money $amount)

- , , Wish:


 <?php namespace Wishlist\Domain; interface WishRepositoryInterface { public function get(WishId $wishId): Wish; public function put(Wish $wish); public function slice(int $offset, int $limit): array; public function contains(Wish $wish): bool; public function containsId(WishId $wishId): bool; public function count(): int; public function getNextWishId(): WishId; } 

, , . Good luck!


PS: . , . , « » :)


')

Source: https://habr.com/ru/post/335834/


All Articles