Сущности и действия реального мира, которые нужно автоматизировать
Набор подходов для перенесения знаний о домене в код
Работа с доменом начинается с анализа предметной области и составления требований
Создаём счёт, добавляем в него товары и выставляем его клиенту.
Набор терминов, применяемых в модели домена для объяснения связей и действий
Создаём счёт, добавляем в него товары и выставляем его клиенту.
class InvoiceController { public function actionCreate($client_id, array $item_ids) { $client = Client::find()->byId($client_id)->one(); if (!$client) { throw new NotFoundException('No client'); } $items = Item::find()->byId($item_ids)->all(); if (!$items) { throw new NotFoundException('No items'); } $invoice = new Invoice(); $invoice->client = $client; $invoice->status = 'new'; $invoice->items = $items; $invoice->save(); $this->notifyInvoiceCreated($client->email, $invoice); return $this->render('invoice', compact('invoice')]); } }
Само собой, товары в счёт можно добавлять и удалять, если счёт еще не выставлен
class InvoiceController { public function actionAddItem($invoice_id, $item_id = null) { $invoice = Invoice::find()->byId($invoice_id)->one(); if (!$invoice) { throw new NotFoundException('No invoice'); } $item = Item::find()->byId($item_id)->one(); if (!$item) { throw new NotFoundException('No item'); } if ($invoice->status !== 'new') { throw new HttpException(403); } $invoice->items[] = $item; $invoice->save(); return $this->render('invoice', ['invoice' => $invoice]); } }
Набор подходов для перенесения знаний о домене в код
class Client { private $id; private $name; private $address; private $phone; public function __construct($id, $name, $address, $phone) { if (!preg_match('/^\d+$/', $phone)) { throw new ValidationException($phone); } $this->phone = $phone; // ... name, address } }
new Client( UUID::create(), 'Фамилия Имя Отчество', 'ул. Кодеров, д. 0xFF', '380441234567' );
class Address { private $country; private $city; private $zip; private $lines; public function __construct($country, $city, $zip, $lines) { $this->zip = $this->validateZip($zip); // TODO: city, zip, lines } private function validateZip($zip) { if (!preg_match('/^\d+$/', $zip) { throw new ZipValidationException($zip); } return $zip; } }
new Client( UUID::create(), new Name('Фамилия', 'Имя', 'Отчество'), new Address('Украина', 'Киев', '01001', ['ул. Кодеров, д. 0xFF']), new Phone('380', '44', '1234567') );
Каждого товара может быть больше чем 1 единица в счёте!
class Item { private $id; private $name; private $description; private $price; public function __construct($id, $name, Money $price) { $this->id = $id; $this->name = $name; $this->price = $price; } // TODO: Getters and setters }
$item = new Item(UUID::create(), 'Silver bullet', Money::USD(1000)); $item->setDescription('This bullet is a killing feature!');
class LineItem { protected $item; protected $quantity; public function __construct(Item $item, $quantity = 1) { $this->item = $item; $this->quantity = $quantity; } // TODO: Getters (no setters in VO) }
class Invoice { protected $id; protected $client; protected $lineItems = []; protected $status; public function __construct($id, Client $client) { $this->id = $id; $this->client = $client; } public function getId() {} public function getClient() {} public function getLineItems() {} public function setLineItems() {} public function getStatus() {} public function setStatus($status) {} }
$item = new Item(UUID::create(), 'Silver bullet', Money::USD(1000)); $quantity = 2; $invoice = new Invoice(UUID::create(), $client); $invoice->setStatus('new'); $invoice->setLineItems([new LineItem($item, $quantity)]);
// Заменим позицию в счёте $anotherLine = new LineItem( new Item(UUID::create(), 'Rage cup', Money::USD(500)) ); $invoice->setLineItems([$anotherLine]);
$invoice->setLineItems([$anotherLineItem]); $invoice->setStatus('processing'); // VS $invoice->lineItems = [$anotherLineItem]; $invoice->status = 'processing';
abstract class Status { /** * @property array Class names of next possible statuses */ protected $next = []; public function canBeChangedTo(self $status): bool { $className = get_class($status); return in_array($className, $this->next, true); } public function allowsModification(): bool { return true; } }
class NewStatus extends Status { protected $next = [ProcessingStatus::class, RejectedStatus::class]; }
class Invoice { public function changeStatus(Status $status) { if (!$this->status->canBeChangedTo($status)) { throw new WrongStatusChangeDirectionException(); } $this->status = $status; } }
class Invoice { public function addLineItem(LineItem $line) { if (!$this->status->allowsModification()) { throw new ModificationProhibitedException(); } $this->line->push($line); } public function removeLineItem(LineItem $line) { // Copy-paste status check? } }
abstract class Status { public function ensureCanBeChangedTo(self $status): void { if (!$this->canBeChangedTo($status)) { throw new WrongStatusChangeDirectionException(); } } public function ensureAllowsModification(): void { if (!$this->allowsModification()) { throw new ModificationProhibitedException(); } } }
class Invoice { public function changeStatus(Status $status) { $this->status->ensureCanBeChangedTo($status); $this->status = $status; } public function addLineItem(LineItem $lineItem) { $this->status->ensureAllowsModification(); $this->lineItmes->push($lineItem); } public function removeLineItem(LineItem $lineItem) { $this->status->ensureAllowsModification(); // ... } }
$invoice = new Invoice(UUID::create(), $client); $invoice->addLineItem(new LineItem($item, $quantity)); $invoice->changeStatus(new NewStatus()); // save...?
interface InvoiceRepositoryInterface { /** @throws InvoiceNotFoundException when no invoice exists */ public function findById($id); public function add(Invoice $invoice); public function update(Invoice $invoice); }
class SqlInvoiceRepository implements InvoiceRepositoryInterface { public function __construct(Connection $db) { $this->db = $db; } public function create(Invoice $invoice) { $this->db->transactionBegin(); $this->db->insert('invoice', [ 'id' => $invoice->getId(), 'client_id' => $invoice->getClient()->getId(), 'status' => $invoice->getStatus() ]); foreach ($invoice->getLineItems() as $lineItem) { $this->db->insert('invoice_lines', [ 'invoice_id' => $invoice->getId(), 'item_id' => $lineItem->getItem()->getId(), 'quantity' => $lineItem->getQuantity(), ]); } $this->db->transactionCommit(); } }
class InvoiceController { public function actionCreate($client_id) { $client = $this->clientRepository->findById($client_id); $invoice = new Invoice(UUID::create(), $client); $invoice->changeStatus(new NewStatus()); $this->invoiceRepository->add($invoice); return $invoice->getId(); } }
class InvoiceServiceInterface { public function create($clientId); public function addLineItem($id, $itemId, $quantity); public function reject($id); public function process($id); }
class InvoiceService implements InvoiceServiceInterface { public function __construct( ClientRepositoryInterface $clientRepository, InvoiceRepositoryInterface $invoiceRepository ) { $this->clientRepository = $clientRepository; $this->invoiceRepository = $invoiceRepository; } public function create($clientId) { $client = $this->clientRepository->findById($clientId); $invoice = new Invoice(UUID::create(), $client); $invoice->changeStatus(new NewStatus()); $this->invoiceRepository->create($invoice); return $invoice->getId(); } }
class InvoiceController { public function actionCreate($client_id) { $invoiceId = $this->invoiceService->add($client_id); return $invoiceId; } }
class InvoiceService { public function reject($invoiceId) { $invoice = $this->invoiceRepository->findById($id); if ($invoice === null) { return null; } $invoice->changeStatus(new RejectedStatus()); if ($this->invoiceRepository->update($invoice) === false) { return null; } return true; } }
class InvoiceService { function reject() { $invoice = $this->invoiceRepository->findById($id); $invoice->changeStatus(new ProcessingStatus()); $this->invoiceRepository->update($invoice); return true; } }
trait EventTrait { private $events = []; public function registerEvent($event) { $this->events[] = $event; } public function releaseEvents() { $events = $this->events; $this->events = []; return $events; } }
class Invoice { use EventTrait; public function changeStatus(Status $status) { $this->status->ensureCanBeChangedTo($status); $this->status = $status; $this->registerEvent( new InvoiceStatusWasChangedEvent($this, $status) ); } }
class InvoiceService implements InvoiceServiceInterface { public function reject($id) { $invoice = $this->invoiceRepository->findById($id); $invoice->changeStatus(new RejectedStatus()); $this->invoiceRepository->update($invoice); $this->dispatcher->dispatchEvents($invoice->releaseEvents()); } }
class ClientService implements ClientServiceInterface { public function changeAddress($id, $country, $city, $zip, $lines) { $client = $this->clientRepository->findById($id); $address = new Address($country, $city, $zip, $lines); $client->changeAddress($address); $this->clientRepository->update($client); } }
class AddressDto
{
public $country;
public $city;
public $zip;
public $lines = [];
public static function fromArray($data)
{
$self = new self();
$self->country = $data['country'];
$self->city = $data['city'];
$self->zip = $data['zip'];
$self->lines = $data['lines'];
return $self;
}
}
class ClientController { public function actionChangeAddress($client_id) { $dto = AddressDto::fromRequest($this->request->post()); $this->clientService->changeAddress($client_id, $dto); return $client_id; } }
class ClientService implements ClientServiceInterface { public function changeAddress($id, AddressDto $dto) { $client = $this->clientRepository->findById($id); $address = new Address( $dto->country, $dto->city, $dto->zip, $dto->lines ); $client->changeAddress($address); $this->clientRepository->update($client); } }