Предметно-ориентированное проектирование
- 5 ответов
- 0 вопросов
4
Вклад в тег
<?= \vova07\imperavi\Widget::widget([
'name' => 'redactor',
'settings' => [
'lang' => 'ru',
'buttons' => ['formatting', 'bold', 'italic'], // Пропишите сюда любые дополнительные кнопки которые вам нужны.
'plugins' => [
'fontcolor',
'video',
],
],
]); ?>
final class CreateCustomer
{
/**
* @var IWriteRepository Интерфейс репозитория, где на самом деле хранистя конкретная имплементация записи сущности.
*/
private $repository;
public function __construct(IWriteRepository $repositorym Validator $validator)
{
$this->repository = $repository;
...
}
/**
* @param string $id ИД будущей записи.
* @param array $data ПОТ данные которые мы получаем в момент АПи запроса.
*/
public function handle(string $id, array $data) : void
{
/**
* Валидация выбрасывает исключение если данные не валидны или возвращает массив валидных данных.
*/
$dto = $this->validator->validate($data);
$customer = new CustomerEntity(new UUID($id));
/* Ентити это сущность которое содержить исключительно только бизнес логику и безнесс поведения.
* В вашем примере у вас были разные сеттеры, в правильном подходе это лишенно смысла,
* и совсем неправильно. Думайте об методах аггрегата (Сушность с которой работает репозиторий это ни * что инное как аггрегат) как проекция бизесс логики.
* для примера в реальном мире принято говорить `Новый клиент зарегистрировался в системе`, мы никогда * не перечисляем цепь проделанных событий. Кто-то говорит `Клиент заполнил свой аддрес, ФИО, потом
* телефон, и отправил данные` ?!
*
* Если вы заметили правильно, то в конструкторе сущности передается только идентификатор.
* Такой подход похож на реальную ситуацию, что позволяет легко проектировать бизнес логику.
* регистрация клиента можно сравнить с регистрацией карточки пользователя в магазине.
* - Берём бумагу (или специальный блокнот)
* - Указываем номер клиента (обычно его как-то генерят например дата, время, или номер карточки
* которую ему выдают)
* - начинаем спрашивать клиента кго персональные данные и заполняем все в ОДНОМ процессе.
* - Потом кидаем блокнот/бумагу оратно или в колецию где лежит другая похожая инфа.
*/
$customer->register($dto['name'], ...);
/**
* Именно этот метод делает сохранение нового клиента.
* Сама реализация интереса живет в персистентном слое (`persistence layer`).
*/
$this->repository->store($customer);
}
}
final class PostController extend Controller
{
public function __construct(IWriteRepository, CreateCustomer $createCustomUseCase, RetrieveCustomer $retrieveCustomerUseCase)
{
...
}
public function __invoke(array $data)
{
$id = $this->repository->pickNextId();
$this->createCustomUseCase->handle($id, $data);
return $this->retrieveCustomerUseCase->handle($id);
}
}
<?php
/**
* "Domain/Directories/File.php"
*/
final class File
{
/**
* Я использую название файла как его уникальный идентификатор.
* Надо учитывать что этот идентификатор уникален в рамках одной директории (папки).
* В случае если это сложно отслеживать можно использовать UUID как ИД и атрибут $name как допольнительнй.
*
* @var string
*/
private $name;
/**
* @var bool
*/
private $hidden = false;
/**
* @param string $name
*/
public function __construct(string $name)
{
$this->name = $name;
}
/**
* Скрываем временно файл.
*/
public function hide() : void
{
$this->hidden = true;
}
/**
* Востанавливаем доступ к срытому файлу.
*/
public function show() : void
{
$this->hidden = false;
}
/**
* @return string
*/
public function getName() : string
{
return $this->name;
}
/**
* @return bool
*/
public function isHidden() : bool
{
return $this->hidden;
}
}
/**
* Aggregate Root.
* По моегму видению файл не может существовать без родительской папке.
* По этому папка и есть наш аггрегат.
*
* "/Domain/Directories/Directory.php"
*/
final class Directory
{
/**
* Название директории (папки) играет роль уникального идентификатора.
* Так-же как и в случае с файлом этот идентифактор может быть уникален только в рамках другой директории (папки).
* В случае если это сложно отслеживать можно использовать UUID как ИД и атрибут $name как допольнительнй.
*
* @var string
*/
private $name;
/**
* @var File[]
*/
private $files = [];
/**
* @var array
*/
private $removedFiles = [];
/**
* @var string
*/
private $ownerId;
/**
* @var bool
*/
private $hidden = false;
/**
* @param string $name
*/
public function __construct(string $name, string $ownerId)
{
$this->name = $name;
$thi->owner = $ownerId;
}
/**
* Этот метод не должен использоватся нигде больше кроме в MySQLStorage или в других сторэджах.
* Это дополнительный метод который нужен потому что другие доменные методы которые описывают бизнес логику могут содержать например события.
* Чтобы не выбрасывать все события при восстановлении используется такого рода хак.
* Вы же можете сипользовать что-то другое если у вас есть более приемлемый вариант.
* Я пока лучше что-то не нашел.
*
* @internal
*/
public function reconstruct(bool $hidden, File ...$files) : void
{
$this->hidden = $hidden;
foreach ($files as $file) {
$this->files[$file->getName()] = $file;
}
}
/**
* @param File $file
*/
public function addFile(File $file) : void
{
$this->files[$file->getName()] = $file;
}
/**
* @param File[] $files
*/
public function addFiles(Files ...$files) : void
{
foreach ($files as $file) {
$this->addFile($file);
}
}
/**
* Это то что вы называете "запретить навсегда".
* Надо трактировать удаление файле как `sof delete`.
* То есть файл не удаляется реально а просто ставится какой небудь флаг типа `deletedAt` которое показывает время удаления.
* Хочу заметить что это один из вариантов реализации, так как их много.
* Может возникнуть вопрос почему мы не делаем это удаление через сам файл `File.php`
* это потому что возможно этот файл удален в одной директиве а в другой нет.
* То есть возможно реальный файл есть на диске а его сслки в БД дублируются или еще что, и каждая директория сама контролирует это для себя.
* Вы можете удалить этот метод, и реальизовать свою логику как вам угодно.
*
* @param string $name
*/
public function removeFile(string $name) : void
{
if (!isset($this->files[$name])) {
throw new OutOfBoundsException('Invalid file ID');
}
$this->removedFiles[] = $name;
}
/**
* Скрываем временно дирекорию.
*/
public function hide() : void
{
$this->hidden = true;
}
/**
* Востанавливаем доступ к срытой директории.
*/
public function show() : void
{
$this->hidden = false;
}
/**
* @return string
*/
public function getName() : string
{
return $this->name;
}
/**
* @return string
*/
public function getOwnerId() : string
{
return $this->ownerId;
}
/**
* @return File[]
*/
public function getFiles() : array
{
return $this->files;
}
/**
* @return array
*/
public function getRemovedFiles() : array
{
return $this->removedFiles;
}
/**
* @return bool
*/
public function isHidden() : bool
{
return $this->hidden;
}
}
/**
* "Domain/Directories/Repository.php"
*/
final class Repository
{
/**
* @var Storage
*/
private $storage;
/**
* @param Storage $storage
*/
public function __construct(Storage $storage)
{
$this->storage = $storage;
}
/**
* @param Directory $directory
*/
public function store(Directory $directory) : void
{
$this->storage->store($directory);
}
/**
* @return array
*/
public function getDirectoriesByOwner(string $ownerId) : array
{
$this->storage->getDirectoriesByOwner($ownerId);
}
}
/**
* "Domain/Directories/Storage.php"
*/
interface Storage
{
/**
* @param Directory $directory
*/
public function store(Directory $directory) : void;
/**
* @return array
*/
public function getDirectoriesByOwner(string $ownerId) : array;
}
/**
* "Infrastructure/Persistence/Directories/MySQLStorage.php"
*/
final class MySQLStorage implments Storage
{
/**
* Название поля в БД.
*/
private CONST TABLE = 'Direcotry';
/**
* @var PDO
*/
private $database;
/**
* @param PDO $database
*/
public function __construct(PDO $database)
{
$this->database = $database;
}
/**
* @param Directory $directory
*/
public function store(Directory $directory) : void
{
// Провекра на сущестование директории в БД.
if (/* Ваша проверка на наличие в БД */) {
// Подгоавливаем директорию для сохранения.
$this->prepareRowData($direcotry);
// TODO: Обновите директорию в БД.
} else {
// Подгоавливаем директорию для сохранения.
$this->prepareRowData($direcotry);
/**
* TODO: Создайте новую директорию в БД.
* Этот процесс вероятнее всего в релационной БД будет содержать несколько шагов.
* Добалвение самой директории (папки), добалвение всех файлов в этой директории возможно в отдельной таблице.
* Ну и все другие операции которые уместны в этом сценарии.
*/
}
}
/**
* @return array
*/
public function getDirectoriesByOwner(string $ownerId) : array
{
/**
* Именно в этом методе и происходит выборка и фильтрация нужных директорий (папок) и файлов.
* Вы легко можете выбрать все файлы кторые не удаленны и не скрытие. Или все, или те которые вы посчитаете нужным.
* `WHERE Directory.isHidden = false AND File.isDeleted = false AND File.isHidden = false`
* То есть хочу заметить что вы свободны использовать любую структуру БД, которая вам подходит.
* Для целесности этой цепочки ниже после выборки данных вызывается метод преобразования из массива в Entity.
*/
// Запрашиваем данные из БД6 после чего преобразовываем все в доменные сущности и возвращаем результат.
$result = [];
foreach ($rows as $row) {
$result = $this->buildEntity($row);
}
return $result;
}
/**
* Этот метод конвертирует Entity в массив для дальнейшего его сохранения.
*
* @return array
*/
private function prepareRowData(Directory $directory) : array
{
$files = [];
foreach ($direcotry->getFiles() as $file) {
$files[] = [
'name' => $file->getName(),
'isHidden' => $file->isHidden(),
'isDeleted' => in_array($file->getName(), $direcotry->getremovedFiles()),
];
}
return [
'name' => $direcotry->getName(),
'ownerId' => $direcotry->getOwnerId(),
'isHidden' => $direcotry->isHidden(),
'files' => $files,
];
}
/**
* @param array $row
*
* @return Directory
*/
private function buildEntity(array $row) : Directory
{
$files = [];
foreach ($row['files'] as $item) {
$files[] = new File($item['name']);
}
$directory = new Directory($row['name']);
$directory->reconstruct($row['isHidden'], ...$files);
return $directory;
}
}
/**
* "Application/Directories/GetDirectoriesByOwner.php"
* Это так называемый `Use Case` или в книжках `Application Service`.
* Все наше приложение работает только через такие юз кэйсы, API, Console, Backend все что у нас есть вызывает такие юз кэйсы.
* Пример:
* $service = new GetDirectoriesByOwner(
* new Repository(
* new MySQLStorage($container[PDO::class])
* )
* );
*/
final class GetDirectoriesByOwner
{
/**
* @var Repository
*/
private $repository;
/**
* @param Repository $repository
*/
public function __construct(Repository $repository)
{
$this->repository = $repository;
}
/**
* @param string $ownerId
*
* @return array
*/
public function handle(string $ownerId) : array
{
return $this->repository->getDirectoriesByOwner($ownerId);
}
}