D
D
Delphinum2017-03-31 14:00:29
Problem-Based Design
Delphinum, 2017-03-31 14:00:29

How to organize access according to DDD canons?

Tell me how to properly organize access to files without violating the principles of DDD?
The requirements are as follows:
Iyv9B2vMy2_FIosgvb9Gq5R8o4n9TSlCIItMq0JpDomain Model

  • The system organizes the loading, storage and access to files
  • The owner of the file has access to all his files, and to the files of other owners available to him
  • The user has access to the files available to him
  • Access to files can be granted both by the directory in which the file is loaded, and by the owner. In other words, the user either selects a directory or an owner, thereby getting all the files available to him
  • Access to files is determined by the file's hidden field , as well as whether the hideFiles methods are called on the author and directory

Task:
How to properly organize the file access system in order to:
  1. It was possible to grant and withdraw access to a specific file
  2. It was possible to give and take away access to all files of the author or directory

What exactly is not clear:
The simplest solution would be to put the hidden = true field on all files when calling the hideFiles methods of the author or directory, but what if you need to block this file from viewing forever, but temporarily hide all the files of the author? That is, when the time comes to show all the files of the author, the file whose viewing is permanently prohibited will also be shown, and therefore you cannot use the same hidden field both to hide the file and to hide all the files of the author. On the other hand, it is not very convenient to implement three fields for a file: hidden , owner_hidden , directory_hidden , because it is easier to take out the fields: author_ownerand directory_hidden to the Owner and Directory classes , or is this the wrong approach?
At the same time, I want to stay in the context of DDD and use Simple Object, and not go into the area of ​​database queries.

Answer the question

In order to leave comments, you need to log in

1 answer(s)
V
vova07, 2017-04-06
@vova07

Greetings!
It seems to me that you are a little bit wrong in following the principles of DDD.
More precisely, you design your code incorrectly.
If you write using DDD, then you don't need to think about the fields in the database or other infrastructure things.
You need to write the domain as it is within your understanding.
Based on your text, I would write this approximately as in the example that I gave. Although it was all written on the knee without analyzing the user's user cases, without analyzing the domain, and so on. So treat my code as an example, or as a draft.

<?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);
  }
}

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question