Введение

Хэй, всем привет. Мы начинаем небольшой блог по разработке пет-проекта. О чем будет проект? Хороший вопрос...
На данный момент о статистике дрифта. Да, именно о статистике дрифта. Проект, на самом деле, большой, но мы будем делать его по "запчастям", чтобы медленно и уверенно расширять его. Этот подход поможет нам совершенствовать навыки разработки и архитектуры, а также не позволит "утонуть" в потоках мыслей и объемах работ.

Ну хватит с введением, давайте приступать. Первая часть статистики - встречи пилотов. Что? Не-не, не те всякие современные "европейские встречи", а сколько раз и где пилоты соревновались друг с другом. Для чего нам нужна такая статистика? Все очень просто! Когда идет этап, комментаторы часто говорят, что эти пилоты уже не раз встречались, а их счет встреч N:M. И я вот подумал, а что если просто взять данные, составить их структуру, сделать открытый доступ к данным через красивый визуальный интерфейс? Прикольно же, когда ты можешь в режиме реального времени зайти на сайт, выбрать двух пилотов и посмотреть подробную или краткую статистику их встреч. Ну вот и давайте попробуем это сделать максимально интересно, технологично (тут как выйдет) и практично.

С чего мы начнем? Файловой структуры проекта. Учтите, что в процессе разработки что-то может меняться, а что-то кардинально изменяться. Это норма, так как я не пишу готовую статью, где все уже "вылизано до блеска". Это лайф-тайм блог по проекту.

Файловая структура

Наша файловая структура сейчас не будет привязана ни к какому фреймворку, поскольку мы делаем изолированный проект на "честном слове". Что? Да, я решил пойти по принципу - написать внутренность проекта, а потом натянуть на это что-то готовое из фреймворков. В чем плюс? Пока точно не знаю, но мне нравится писать изолированные вещи, которые работают без привязки к чему-то, а потом страдать с переносом этого на что-то готовое. На самом деле, как показывает практика, такой подход отлично подходит, чтобы "изучить" ядро проекта, а потом "изучить" работу фреймворка на готовом ядре. А еще прикол этого подхода в том, не будет использоваться огромная часть готового решения, так как поймем, что она нам просто не нужна, а глаз мозолит.

Давайте я сразу накидаю всю структуру, а потом ее детально опишу?

- config
- src
  - App
  - Domain
  - Infrastructure
  - Shared
- tests
- var
  - cache
  - reports
    - tests
- vendor

В config будут лежать какие-то конфиги приложения.

В src будет лежать основной код приложения. Эта папка содержит в себе 3 слоя:

  • App - команды, события, мапперы, DTO и прочие вещи. По сути - тут лежат файлы "первой точки обращения";

  • Domain - доменные сущности, интерфейсы и прочие запчасти сущностей (не путать с моделями базы данных);

  • Infrastructure - репозитории, работа с базой данных и прочие вещи.

А что за Shared папка? Да все просто - общие элементы: базовые команды, обертки и прочая "общая" ерундистика.

В tests - тесты, которые будут разбиты также, как и папки в src. Ну или почти также.

В var - отчеты, кэш и прочая служебная история.

В vendor будут лежать необходимые пакеты для разработки.

Надеюсь, что структуру вы поняли, но про src я немного дополню приблизительной логикой:

  1. Делается запрос к нашему проекту (будет сделано в самый последний момент).

  2. Что-то, что отвечает за обращения, вызывает из App слоя команду.

  3. Команда вызывает репозитории для работы с данными что-то обрабатывает и т.д., а в ответ возвращает DTO результата.

    1. Репозиторий стучится в БД, чтобы получить данные.

    2. Маппит "сырые" данные в сущности Domain и возвращает их в команду

Как бы все. Это просто и понятно. Хотя, вам может показаться это избыточным, но такая архитектура помогает четко разграничить области ответственности по слоям. Вы точно будете знать, что, где и для чего находится. Ну и что делает, соответственно.

Теперь, давайте поговорим про настройки и необходимые пакеты для старта нашего интересного проекта.

Настройки PHPStorm и composer пакеты

Начнем с простого и очевидного - пакеты composer. На данный момент, я подразумеваю, что мне понадобится вот это:

{
  "require-dev": {
    "phpunit/phpunit": "^11.0",
    "qossmic/deptrac-shim": "^1.0"
  },
  "require": {
    "php-di/php-di": "^7.0"
  }
}

Ну как бы тут есть понятно, phpunit для тестов, php-di для контейнера зависимостей. А что за deptrac-shim?
Ой, относительно крутая штука, которая немного контролирует вас в слоях. Помните, я писал про слои: App, Domain, Infrastructure? Так вот, между ними есть "особое" взаимодействие. Точнее, кто что может "дергать". Например, App слой может использовать все напрямую. Но чаще всего, ему надо работать с Infrastructure слоем, который может в Domain слой. Почему App всемогущий? Потому что ему надо и вызвать штуку по работе с БД, но и передать ей данные в нужном формате (в нашем случае будут ValueObject доменного слоя).

Немного сложно, но это нормально для начала. В процессе все поймете. Если коротко - сильно разделяем слои и их использование между собой. Давайте настроим это дело!

Deptrac

Выполняем команду vendor/bin/deptrac init, чтобы создать конфигурационный файл deptrac.yaml. В него вписываем такую конфигурацию:

Код deptrac.yaml

Полный код можно посмотреть в оригинальной статье: Пихта DEV - Проект «Статистика дрифта». Часть 1. Настройка.

Что тут за магия такая происходит? Все очень просто. В начале мы указали, где смотреть (paths) и что исключить (exclude_files). Далее указали наши слои и настроили их. Например, вот так мы создали слой App, который имеет тип проверки directory и находится в src/App/:

name: App
collectors:
  - type: directory
    value: src/App/.*

Потом в секции ruleset мы просто указали какой слой какие слои может использовать. Как бы все. И по выполнению команды vendor/bin/deptrac analyse мы будем получать отчет, который покажет нам корректность использования слоев.

Но сам deptrac будет писать cache файл прям в корень проекта. А мне такое не нравится! Понятно, что можно исключить его из git, но я же сделал для этого отдельную папку var/cache. Проблема в том, что я не нашел как поменять конфигом путь до cache файла. Но, если запустить команду vendor/bin/deptrac analyse --cache-file=var/cache/.deptrac.cache, то все хорошо. Это не очень удобно, так как мы можем забыть про дополнительные ключи CLI команды. Давайте создадим еще одну новую директорию bin, в которой будем хранить исполняемые файлы?

И наш первый файл - deptrac.sh, в котором вот такое простое содержание:

../vendor/bin/deptrac analyse --config-file=../deptrac.yaml --cache-file=../var/cache/.deptrac.cache

Это позволит нам запускать скрипт с корректными для нас настройками, а также расширять его в будущем. Возможно, когда-нибудь можно будет подключить его к сборке проекта (билд), поставить в pre-commit событие git и вообще круто сделать чистую сборку или push.

А теперь, предлагаю настроить phpunit, чтобы также круто запускать тесты, как проверки deptrac.

PHP Unit

Создаем файл в корневой директории phpunit.xml с очень простым содержимым:

Код phpunit.xml

Полный код можно посмотреть в оригинальной статье: Пихта DEV - Проект «Статистика дрифта». Часть 1. Настройка.

Тут особо нечего рассказывать, кроме того, что в этих секциях: bootstrap="vendor/autoload.php", colors="true" и cacheDirectory="var/cache" - мы указали загрузчик, включили цвета (для красоты вывода) и указали директорию кэша. В testsuites указали местоположение наших тестов, а в source указали местоположение нашего основного кода. А еще я люблю покрытие. Дааа, я тот самый шибанутый, который любит максимальное покрытие кода... Не судите строго, каждый псих по своему. Поэтому, я добавил секцию coverage по умолчанию и указал 2 формата: html и xml - экспорта в папку var/reports/tests. Зачем? Html буду смотреть в браузере, там красиво и удобно, а xml может понадобиться нам в будущем для сборки.

Теперь, создайте папку tests в корне проекта и допишите в composer.json такую штуку:

{
  "autoload": {
    "psr-4": {
      "tests\\": "tests/"
    }
  }
}

Это позволит использовать внутри папки tests namespace tests\*. А еще это надо прописать в IDE, чтобы не ругался. Идем в File -> Settings -> Directories, справа есть папка tests. Нажимаем на карандаш и пишем tests\. Сохраняемся и выходим. Теперь, нажимаем по нашей папке tests правой кнопкой и выбираем Mark Directory as -> Test Source Root. Теперь она светится зеленым цветом и нам хорошо. А да, раз уж про директории зашла речь, то по известному алгоритму пометьте src как Source Root, а папки var и vendor как Excluded. Вот теперь супер все настроено.

Ну и напоследок создайте .gitignore файл, в котором напишите:

vendor
.idea
var
test.php

Что мы тут сделали? Мы объяснили нашему git репозиторию, что нам не надо заносить под контроль версий папки vendor, var и .idea, а также файл test.php. Так, стоп! А что за файл test.php? А это, ребята, стандарт одного из видов крутого атомарного тестирования! Шучу, просто иногда надо что-то быстро запустить проверить и проще писать в test.php
файле вызовы, чем строчить тест или идти на какой-нибудь онлайн ресурс.

Комментарии (15)


  1. SerafimArts
    04.09.2024 01:30
    +3

    О, гексагоналка! Начало уже интригующее и выглядит как годный материал, наконец-то)

    У меня следующий вопрос остался.

    Правильно ли я понимаю, что это НЕ классический вариант с

    • App\Example\{Domain, Application, Infrastructure, Presentation} + App\Some\{Domain, ...etc}, а в обратную сторону:

    • App\Domain\Example + App\Infrastructure\Example + ... + App\Domain\Some + ...etc (иначе зачем Shared)? Или предполагается что контекст/предметная область/домен у нас один единственный на данный момент?

    Вопрос вот к чему: Известно что один домен не может лазать в другой и наоборот. Это считай что разные приложения должны быть, которые можно в будущем разнести вообще на разные сервисы.

    Так что интересно как будет решена проблема с VO, которые пересекаются между разными доменами. Ну например, есть у нас App\Domain\Car\... внутри которого всякие доменные сервисы, энтити и прочая шалупонь. Есть VO App\Domain\Car\Name + его сервисы App\Domain\Car\Name\NameParser (например).

    Потом добавляется статистика и какие-нибудь события: App\Domain\Statistics\Event\CarHasBeenDestroyed, которые в свою очередь могут дёргать App\Shared\Domain\CarId (потому что шаред), но не могут дёрнуть Name, т.к. это другой домен. Предполагается двигать Name тоже в Shared? Но это всё же VO не общее, а принадлежащее исключительно машинам (домену Car). Что в таких случаях предполагается делать?

    P.S. А в чём была причина отказываться от Presentation слоя, куда складываются хттп контроллеры, консольные команды, реквест/респонз дто и проч.? Ради упрощения (я так понимаю вы его с Application объеденили) и дабы не оверинженерить лишние слои?

    P.P.S. Вместо *.sh скриптов можно использовать composer ;)
    Просто туда добавить эту простыню из команды и повесить алиас, чтоб можно было вызывать как composer deptrac и всё: https://getcomposer.org/doc/articles/scripts.md


    1. mepihin Автор
      04.09.2024 01:30
      +1

      В папке Domain и других лежат "модули". То есть у нас как бы одно единственное приложение, которое не разделено на структуры типа Car\{App, Domain и Infrastrucutre}, а наоборот - все объединено в App, Domain и Infrastrucutre и имеет внутри то, с чем работаем. Данный подход на текущем этапе достаточен.

      По поводу работы VO "разных доменов" тут получается так, что мы можем можем использовать в рамках Car все, что внутри, если нам надо использовать в Racer что-то из VO Car, то для этого формируется VO на стороне Racer, который на инфраструктурном слое парсится из CarVO -> RacerCarVO. Другой подход - прямое использование, так как по факту приложение одно и "область видимости" тоже. Вот если бы были разные структуры (описал выше), то тогда да, маппинг через anticorruption layer или что-то в таком духе.

       В чём была причина отказываться от Presentation слоя

      Никто не отказывался, мы просто пока не дошли до этого момента. Я же пишу в формате "живого блога". Поэтому, когда мы дойдем до запросов (а это вообще последнее, что будет), то добавим нужные слои. Но, на самом деле, это вообще не важно, так как мы будем писать команды, которые по факту будут вызываться в Presentation слое.

      Вместо *.sh скриптов можно использовать composer

      Дельное замечание, забыл про это совсем. Подумаю над переносом в composer.


      1. SerafimArts
        04.09.2024 01:30
        +1

        если нам надо использовать в Racer что-то из VO Car, то для этого формируется VO на стороне Racer, который на инфраструктурном слое парсится из CarVO -> RacerCarVO. Другой подход - прямое использование, так как по факту приложение одно и "область видимости" тоже.

        Понял, если разные домены, то предлагается копипастить. Благо DTO и не критично. А что делать в таком случае с сервисами, относящимися к этому VO? Ну там парсеры, валидаторы, ещё мб что?


        1. mepihin Автор
          04.09.2024 01:30
          +2

          Например, также берем anticorruption layer и связываем все что хотим. Просто если мы были бы разными командами разработки, то напрямую ничего вашего трогать не могу. Тут цельная структура об одном и том же (контекст), так что можно использовать без проблем.

          С другой стороны, никто не мешает написать адаптеры на anticorruption layer, чтобы "законно" использовать что-то "чужое"


          1. SerafimArts
            04.09.2024 01:30
            +1

            Ну вот да, я тоже об этом. Что если мы совсем в разных отделах сидим, то трогать не получится. А копипаста уж очень избыточна.

            С другой стороны, никто не мешает написать адаптеры на anticorruption layer, чтобы "законно" использовать что-то "чужое"

            Хм, не совсем понимаю. Предполагается что эти адаптеры потом просто в шаред сложить? Или каким образом? Допустим у нас разные команды (т.е. не ваш вариант из статьи, а тот что предположили в комментариях), вот в таком случае:

            - Car\
              - Domain\
                - SomeCarVO
                - SomeCarVO\
                    - SomeCarVOParser
                    - SomeCarVOValidator
                    - SomeCarVOCreator
            - Racer\
              - Domain\
                - ... и тут очень хочется взять VO + сервисы SomeCarVO что запрещено
            

            И кроме как скопипастить этот VO, а общение с его сервисами настроить через CommandBus/QueryBus у меня идей не возникло, да и статей особо не нашёл (плохо гуглил возможно).


            1. mepihin Автор
              04.09.2024 01:30
              +1

              Не, ты делаешь в своем "модуле" этот слой, где повторяешь структуру, а классы в ней - адаптеры, которые используют чужие классы и мапят их на свои так, как тебе это надо. Это оптимальный вариант использования.


              1. SerafimArts
                04.09.2024 01:30
                +1

                Хм, т.е. нейминг "Adapter" (ну и предметная область соответственно) фактически "легализует" доступ до сторонних доменов если я правильно понял?


                1. mepihin Автор
                  04.09.2024 01:30
                  +1

                  Есть шаблон проектирования "Адаптер" посмотри.


  1. sibvic
    04.09.2024 01:30
    +1

    Лет 10 назад я такое уже писал. Правда не только для парных заездов. Но не взлетело. Надеюсь у вас взлетит


    1. mepihin Автор
      04.09.2024 01:30
      +1

      Если поделитесь опытом, то будет очень кстати. Проект реализуется от простого к сложному, потом будет и квалификация и прочее. Пока решил сделать так, чтобы не захламлять мозг и не путаться от масштаба.


  1. storoj
    04.09.2024 01:30

    Какая разница в каких папках лежат какие файлы?


    1. mepihin Автор
      04.09.2024 01:30
      +1

      Разница в архитектуре приложения, связанности классов, разделения логики. Например, вы обедаете на кухне, а спите в спальне. По хорошему, вы не должны обедать в спальне. Вот и такая история тут.


      1. storoj
        04.09.2024 01:30

        Потом всё равно это превратится в кашу, потому что иерархическая структура тут не подходит. Один "файл" может логически относиться к более чем одной "папке". Гораздо легче навигироваться просто по имени файла/символа. Я не помню когда в последний раз вообще пользовался деревом проекта, кроме как в ситуациях когда нужно создать новый файл и мучительно выбирать куда бы его положить.

        Прозвучит неожиданно, но избавившись от папок (просто кладя всё в "src") можно избавиться от головной боли по организации того, что организовать невозможно.


        1. mepihin Автор
          04.09.2024 01:30
          +1

          Что же, это ваш выбор, но уйма ООП приложений PHP работают именно по какой-то архитектуре директорий, символизирующие не только слои приложения, но и namespace.


        1. FanatPHP
          04.09.2024 01:30

          Один "файл" может логически относиться к более чем одной "папке"

          Можно пример? Желательно в рамках предложенной структуры, но если не получится, то тогда свой.