Как организовать внутреннюю структуру классов?

Доброго времени суток.

  • есть система учёта заказов (crm), в ней есть заказы, клиенты, склад, прочие прелести.

  • есть модели (сущности), каждой таблице в бд соответствует свой одноимённый класс унаследованный от Model.

  • есть контроллеры для каждой сущности, в них методы для создания, редактирования, удаления, методы для вывода данных (возвращают View с наборами данных, все данные берутся с помощью ORM прямо "на месте").

В данной схеме напрашиваются классы для сущностей, что бы к ним был доступ из любого контроллера (например, в контроллере заказа получить объект клиента по его id) (это и так можно сделать, но суть ниже).

Я предполагаю что нужно создать отдельную папку с классами и переносить туда всю логику с сущностями (что бы максимально освободить контроллер оставив там только вызов методов). Но в моделях так же можно создавать методы и работать с ними, а можно создать отдельный класс который моделью являться не будет, он будет в роли прослойки между контроллером и моделью.

Вопрос вот в чём: где лучше описывать методы сущностей? Если делать прослойку о которой написал выше, то как грамотно называть классы, ведь имена классов, например, Clients уже заняты моделями и какие методы должны быть в этой прослойке, а какие в модели? Либо вообще как то по другому сделать?

P.S. Выбрал следующую тактику: сделал классы под названиями classnameService, в них вся бизнес-логика(никаких orm-конструкций), а в них вызываются методы модели которые взаимодействуют с бд. А сами эти классы вызываются из контроллеров

Дополнение для конкурса:

  1. Зачем нужны репозитории, если есть модели ?
  2. Почему в laravel нет сеттеров-геттеров в традиционном их понимании? (нет методов для возвращения полей, они доступны по $object->field, IDE их к выбору не предлагает т.к. они protected, ещё и в массиве)
  3. Как организовать цепочную схему, что бы, например, можно было писать вызовы так: $object->getClient()->checkRegularCustomer() объединив в них методы моделей (для выборки данных) и сервисов (для бизнес логики)? в данный момент у меня есть модели и сервисы, модели отвечают за взаимодействие с бд (CRUD), а сервисы за бизнес-логику (получить товары по заказу, получить их остаток на складе, etc), но в цепочку можно вернуть только один объект.
  4. Как, в общем случае, делится ответственность классов и уровней? пример: есть метод создания клиента. что бы это сделать нужно взять номер телефона и привести к виду "89998887766" удалив всё лишнее что может быть. Это не вопрос валидации, а вопрос нормализации, за это должен отвечать контроллер, сервис или модель?

Ответы (2 шт):

Автор решения: Андрей Туманов

Очень хороший вопрос:

Как найти баланс между реализацией необходимой функциональности по обработке данных между контроллером и моделью?

Исходя из моего опыта разработки подобных систем (а у меня их было много) я могу дать несколько советов (чисто мое субъективное мнение):

  1. Добавлять лишние сущности-прослойки это значит усложнять понимание системы. Лучше этого не делать без особой необходимости. Необходимость в таких классах может появиться в случае появления дополнительных требований к системе по обработке данных, например, при необходимости вести какую-то синхронизацию с другими системами или логирование... Но это все равно очень сильно усложняет систему и нужно, по возможности, избегать создание новых сущностей.
  2. Обработку данных в модели нужно делать в случае, если эта обработка касается прямого доступа к полям и функциональности доступа к данным. Например, делаются дополнительные SQL запросы к базе при формировании ответа модели контроллеру. Основные критерии помещения функциональности в модель это: производительность, использование внутренней реализации модели и скрытие приватной реализации от контроллера.
  3. Обработку данных в контроллере нужно делать в случае обобщенной обработки нескольких моделей или создание дополнительной функциональности по контролю связанных данных в модели. Критерии помещения кода в контроллер: невозможность или сложность объединения нескольких сущностей в модели, дополнительная обработка данных не связанная напрямую с функциональностью модели и обобщение данных модели для отображения.
→ Ссылка
Автор решения: Romka

зачем нужны репозитории, если есть модели ?

Репозитории отвечают за взаимодействие с уровнем данных(бд в вашем случае). Они обеспечивают инкапсуляцию. Если вам вдруг нужно будет поменять хранилище, вы реализуете другой репозиторий с таким же интерфейсом, который будет ходить в другое хранилище. Это удобно. Ваша бизнес логика ничего не знает о том, где хранятся данные и это хорошо.

Модели - описывают поля одного объекта из коллекции. Ничего другого они делать не должны. Да в laravel легко делать запросы Order::query, но неверно делать их в модели. Это нарушение SRP. Операции с наборами данных из хранилища должны быть в репозиториях.

почему в laravel нет сеттеров-геттеров в традиционном их понимании?

Странный вопрос, возможно вы имели ввиду, что они не генерируются автоматический? Использование геттеров и сеттеров хорошая практика и я вам рекомендую работать с полями модели именно через них.

как организовать цепочную схему, что бы, например, можно было писать вызовы так: $object->getClient()->checkRegularCustomer()...

Не вижу проблемы. Давайте приведу пример:

class Order
{
    private OrderModel $model;
    private ClientRepository $clientRepository;
    private ClientFactory $clientFactory;

    public function __construct(
        OrderModel $model,
        ClientRepository $clientRepository,
        ClientFactory $clientFactory
    ) {
        $this->model = $model;
        $this->clientRepository = $clientRepository;
        $this->clientFactory = $clientFactory;
    }

    public function getClient(): Client
    {
        $clientModel = $this->clientRepository->getActiveClient($this->model->getId());
        return $this->clientFactory->create($clientModel);
    }
}


class Client
{
    private ClientModel $model;
    public function __construct(OrderModel $model)
    {
        $this->model = $model;
    }

    public function checkRegularCustomer(): void;
}

Классы Order и Client являются классами доменного уровня, то есть реализуют бизнес логику. При этом они не ходят сами в бд(это делает репозиторий). Модель имеет только геттеры и сеттеры и не содержит никаких запросов и бизнес логики.

как, в общем случае, делится ответственность классов и уровней? пример: есть метод создания клиента. что бы это сделать нужно взять номер телефона и привести к виду "89998887766" удалив всё лишнее что может быть. это не вопрос валидации, а вопрос нормализации, за это должен отвечать контроллер, сервис или модель?

Нет какого то универсального решения для деления, но у каждого класса должна быть своя ответственность.

В данном конкретном примере могу предложить такой вариант:

  1. Контроллер принимает запрос и передает управление валидатору.
  2. В случае успешной валидации, данные передаются сервису.
  3. Сервис сам не содержит бизнес логики, но знает как ее вызвать.
  4. Сервис передает управление классу PhoneNormalizer.
  5. После успешного выполнения PhoneNormalizer->normilize сервис передает результат работы в метод createClient(в котором через репозиторий происходит сохранение)
  6. После успешного выполнения метода createClient сервис возвращает результат контроллеру.

выбрал следующую тактику: сделал классы под названиями classnameService

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

Так же, рекомендовал бы вам делать сервисы исходя их бизнес сущностей, а не моделей.

Но в моделях так же можно создавать методы и работать с ними, а можно создать отдельный класс который моделью являться не будет, он будет в роли прослойки между контроллером и моделью.

Лучше создавать отдельный класс, который не будет прослойкой, а будет содержать только бизнес логику без лишних подробностей(не будет знать что такое http запрос, что такое база данных и т д). Частой проблемой является, то что по мере увеличения кодовой базы бизнес логика расползается по всему проекту - это плохо. "Если вы не найдете место бизнес логике, она найдет его сама".

где лучше описывать методы сущностей?

В отдельных классах если это бизнес логика. В модели только описание полей.

как грамотно называть классы, ведь имена классов, например, Clients уже заняты моделями и какие методы должны быть в этой прослойке, а какие в модели?

Они могут называться одинаково, но находится в разных неймспейсах:

App\Domain\Client
App\Service\Client
App\Model\Client
App\Repository\Client

Но я бы рекомендовал еще и использовать постфиксы:

App\Service\ClientService
App\Model\ClientModel
App\Repository\ClientRepository

все данные берутся с помощью ORM прямо "на месте"

Контроллеры - это уровень взаимодействия с пользователем. В них должно быть минимум логики и вообще не должно бизнес логики. В идеале, в методе контроллера, должен быть вызван метод сервиса и все. То есть контроллер знает только кому передать управление.

Итого советую вам:

  1. Не писать запросы в моделях, использовать репозитории.
  2. Бизнес логику описывать в отдельных классах, доступ к этим классам получать через сервисы.
  3. Держать контроллеры "тонкими".

Все эти рекомендации можно не выполнять и проблем не будет если у вас маленький проект. Но если выполнять, то при увеличении кодовой базы у вас не возникнет проблем. Основной плюс соблюдения принципа единой ответственности и разбиение все на классы - это проще тестировать и проще заменять. Если проект активно развивается - бизнес требования меняются часто. Надо делать так чтобы каждое такое изменение требовало минимум трудозатрат в будущем.

→ Ссылка