Pull to refresh

Организация кода в Laravel. Личный опыт

Level of difficultyMedium
Reading time6 min
Views21K

Hola Amigos! На связи Евгений Шмулевский, PHP-разработчик в Amiga. Начал заниматься программированием с 2001 года, привет Basic и Express/Turbo Pascal. Веб-разработкой — с 2011 года, а профессионально в вебе с 2013 года. Работал продолжительное время с Битрикс, а с 2018 начал осваивать Laravel. 

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

Давайте посмотрим, какие есть подходы к организации кода.

Организация кода в контроллерах

Когда я начал изучать Laravel (до этого еще знакомился с Zend/Yii/Phalcon), то писал всю бизнес логику в контроллерах. Так сделал пару своих пет-проектов (своя LMS, и CRM для HR) и особенно плохого в этом ничего не видел.

Действительно, организация кода в контроллерах является наиболее быстрым решением, но имеет ряд недостатков:

  • сложность переиспользования кода;

  • вносит некоторую запутанность в код, т.к. у контроллера появляется множество ответственностей, помимо его основной роли; 

  • читать и отлаживать код состоящий из большого количества строк достаточно проблематично (напомню, что вся бизнес логика в контроллерах);

  • из первых 2-х пунктов вытекает третий с проблемами масштабирования.

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

Использование команд (actions)

Далее познакомился с подходом с использованием команд. Команда — выполнение некоторого ОДНОГО действия. В таком случае бизнес-логика выносится в отдельный класс-команду.

В некоторых проектах сталкивался с тем, что весь код был вынесен из контроллера в команду просто через copy-paste. На входе у такого action объект Request. Считаю, что при использовании команд разработчику следует отвязаться от Request, либо через параметры, либо через DTO (о DTO напишу чуть ниже). Это позволяет использовать action вне контроллера и не зависеть от Request.

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

  • AuthAction

  • RegisterAction

  • StoreCommentAction

Ниже пример использования Action в упрощенном варианте (далее посмотрим, как сделать лучше):

  1. Controller обращается к action передавая DTO на вход.

  2. Action обрабатывает данные и возвращает ResultDTO, либо ничего.

  3. Грубо говоря, мы весь код из контроллера вынесли в action и поделили на методы.

Использование сервисов (services)

Дальнейшее изучение вопроса навело меня на Сервисы. С понятием «сервиса» я познакомился, изучая курс от Povilas Korop «How to Structure Laravel Projects».

Сервис, в отличии от action, может иметь несколько методов и является более крупным строительным блоком. Маттиас Нобак в книге «Объекты. Стильное ООП» приводит такое определение: сервисы — объекты, которые либо выполняют задачу, либо предоставляют информацию. Также автор отмечает, что сервисы не хранят свое состояние, т.к. для повторного использования сервиса придется заново его инициализировать, сбрасывая состояние.

Также сервисы могут реализовывать интерфейсы и внедряться через интерфейс. Сервис также как и команда должен иметь единственную ответственность.

Ниже схема использования сервисов:

  1. Controller обращается к Service/Repository передавая DTO на вход (либо просто параметры, если их количество менее 3).

  2. Service обрабатывает данные и возвращает ResultDTO, либо ничего.

  3. Service может внутри себя использовать другие Services (расчеты и сохранение данных) и Repository (выборка).

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

Использование паттерна репозиторий

С репозиториями также познакомился на курсе от Povilas Korop. Паттерн репозиторий служит цели отделить логику работы с БД от бизнес-логики приложения. Лично для себя выделяю основной плюс в переиспользовании методов выборки. Примерами методов репозитория могут быть такие названия методов как:

  • getById()

  • getSellers

  • getUserList()

  • etc

Также репозиторий может использоваться для create/update/delete операций. Я предпочитаю в репозиторий выносить только все операции выборки, а для операций изменяющих БД использую сервисы.

Плюсы паттерна:

  • достаточно просто переиспользовать код;

  • простота миграции на другие БД (на практике мной не встречалось), т.к. у нас есть дополнительный слой абстракции.

Использование DTO

Еще один кирпичик это DTO, те специальный тип объекта предназначенный для передачи данных между другими объектами. Чаще всего DTO имеют набор публичных полей и методы для своего создания.

В чем плюсы использования DTO:

  • type-hint;

  • инкапсуляция;

  • разделение слоев приложения;

  • модификация данных.

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

  • UserDTO::fromRequest($request)

  • UserDTO::fromArray($data

Начиная с php 8.0, можно задавать свойства прямо в конструкторе, определяя область видимости. Также для облегчения создания DTO есть пакет от spatie.

Складываем все вместе

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

  1. Controller обращается к action передавая DTO на вход.

  2. Action обрабатывает данные и возвращает ResultDTO, либо ничего.

  3. Action может внутри себя использовать Services (расчеты и сохранение данных) и Repository (выборка), о них подробнее ниже.

  4. Может быть вариант не использовать Repository непосредственно внутри Action, а вызывать только из Service, но считаю это излишним усложнением.

Организация структуры проекта

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

  • app

    • Http

      • Controllers

      • Middlewares

      • Requests

    • Jobs

    • Models

    • Polices

    • ServiceProviders

    • Rules

    • итд

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

В идеальном случае, модуль — это независимая часть бизнес-логики. При модульной организации кода структура у нас получается примерно следующей:

 app

  • Modules

    • Admin

    • Order

      • Models

      • Services

      • Repositories

      • Requests

      • Controllers

      • итд

    • User

    • SMS

    • Security

    • итд

  • ServiceProviders

  • итд

Код разбивается, исходя из логики принадлежности к Домену. Есть также упрощенный вариант, когда Controllers не выносится в папку с модулями, а остается в изначальной директории app/Http/Controllers.

Плюсами данного подхода является:

  • Проще переиспользовать код на других проектах. При необходимости переносится модуль целиком и подключается на новом проекте.

  • У кода меньше зависимостей, т.к. модули имеют минимум связей.

  • Разные модули могут разрабатывать разные разработчики.

  • Проще в поддержке и масштабировании.

Минусы:

  • Основным минусом является увеличение количества директорий.

  • Возможно допустить ошибку при разделении кода на модули. И в будущем придется рефакторить данный код.

Мой способ организации кода

  1. Классы находятся в папке app и разбиты по модулям. Каждый модуль относится к определенной сущности либо бизнес-процессу.

  2. Для выборок данных использую паттерн репозиторий.

  3. Бизнес-логика и операции создания/изменения моделей выношу в сервис-классы. Сервис классы не хранят свое состояние, что позволяет их переиспользовать без повторной инициализации.

  4. Для того чтобы не зависеть от Request в сервисы передаю либо одиночные параметры, либо DTO. Это позволяет переиспользовать код вне контроллеров (например, команда создания нового пользователя и т.д.).

  5. Стараюсь, чтобы модели оставались максимально тонкими. В основном содержат в себе связи (relations).

  6. Контроллеры остаются на своих местах, но создаю папку согласно наименованию модуля.

  7. Для создания объектов на лету использую статическую фабрику (static factory).

  8. Если используем сервис, и он выполняет единственное действие.

  9. Как именую методы:

  • getSomething — получение информации. Важный момент — отсутствие side эффектов.

  • isSomething — метод для проверки, должен возвращать.  

Вот основная логика организации проекта. Для примера создал на github репозиторий с примером простого проекта (авторизация + выборка сущностей) организованный по модулям. Его можно использовать в качестве шаблона для новых проектов. Надеюсь, будет полезно!

Tags:
Hubs:
Total votes 18: ↑16 and ↓2+19
Comments29

Articles