Разработчик: Студия Флаг
Дата публикации API: 30.11.2021
- docker-compose.yml — для локальной разработки
- docker-compose.test.yml — для сборки образа для тестовой площадки
- docker-compose.staging.yml — для сборки образа для staging площадки
- docker-compose.prod.yml — конфиг для запуска на проде. Лежит на проде, переименованный в docker-compose.yml
- docker-compose.build-base.yml — конфиг для сборки базовых образов. Не должен использоваться разработчиками, так как нужен для создания образов общих для всех проектов веб-студии
- app
- Debian base (registry.gitlab.com/flagstudio/tairai:base)
- Node 12
- PHP: 8.1.1
- Laravel: ^8.73
- postgres
- dockerhub image (Postgres:13)
- postgrestest (используется для запусков тестов с базой postgres)
- dockerhub image (Postgres:13)
- traefik (для локальной разработки)
- dockerhub image (Traefik:2.5.6)
- redis (Используется для сессий, кэша)
- dockerhub image (redis:6.2.5-buster)
Общие
- .env — единственный конфиг не под Git'ом, поэтому в нем хранятся все настройки сайта и докера
- docker/app/www.conf — php-fpm
- docker/app/laraflag.ini — php
- docker/app/crontab — cron
Local
- docker/app/supervisord_local.conf — supervisor
- docker/app/xdebug.ini — xdebug
Prod
- docker/app/opcache.ini — opcache
- docker/app/supervisord_build.conf — supervisor
Используйте docker-compose.yml
, запускайте нужные службы, собирайте зависимости в app
. Вот несоклько полезных команд для запуска на локале:
dc up -d — запуск проекта
dc up --build -d app — пересобрать образ и перезапустить контейнер
dc exec app composer install — выполнение команд в контейнере
dc exec app bash — подключиться к контейнеру
dc -f docker-compose.build-base.yml build - собираем образ
docker push registry.gitlab.com/<REPOSITORY_NAME>/<COMPOSE_PROJECT_NAME>:base - пушим в репозиторий
- Если хук возвращает code style errors, пофиксите с помощью команды:
dc exec app composer csfix
, добавьте изменения в коммит. - Чтобы запустить ТОЛЬКО проверку на code style:
dc exec app composer csfix-validate
, команда вернет список проблемных файлов. - Если pre_commit hook содержит ошибки тестов, чиним тесты и запускаем проверку заново.
- Запустите
php artisan test
cp .env.example .env
Заполняем .env
файл. Необходимо указать корретные значения для подключения к базе данных, COMPOSE_PROJECT_NAME
должен совпадать с именем сети в файле docker-compose.yml
. После этого можно пробовать поднимать проект.
Первым шагом будет сборка базового образа. После этого пробуем поднять контейнеры проекта:
docker-compose up -d
docker-compose exec app composer i
docker-compose exec app npm i
docker-compose exec app npm run dev
docker-compose migrate --seed
docker-compose exec app php artisan key:generate
Здесь можно почитать про концепцию Porto от его создателя. А это его собственная реализация Porto на laravel. Можно развернуть и потыкать при желании. Или просто подстмотреть что-то и взять к себе в проект.
В сборке предполагается использование подхода TDD при разработке. Поэтому тесты настроены, чтобы они запускались у каждого контейнера и корабля.
В Porto приложение делится на два слоя: контейнеры и корабль. Слой корабля отвечает за логику приложения, а контейнеры(они же домены/модули) отвечают за бизнес логику. Все компоненты контейнеров должны наследоваться только от классов из коробля(Ship/Parents/*). Если вы добавляете новый пакет, от которого ван нужно наследовать свои компоненты в контейнере, то ДОЛЖНЫ быть родительские классы в слое коробля, наследующие классы пакетов, и только от них уже наследуются компоненты. В Porto ещё допускается объединение нескольких контейнеров в однин раздел (section). Это допускается если контейнеры имеют связи между собой. Например таким образом мы может объединить в один раздел интеграции приложений.
Можете посмотреть как выглядит раздел Integrations.
Так как мы делим наше приложение на отдельные контейнеры, то у нас обязательно возникнил ситуация, когда в компонентах одного контейнера нам понядобятся компоненты других контейнеров. В таких случаях нужно использовать контракты (интерфейсы).
Можно посмотреть на примере контейнера
Payment
. Ему нужен функционал из контейнераIntegrations\Robokassa
Подробнее рассмотрим из чего состоит контейнер:
Container
├── Actions
├── Configs
├── Console
├── Contracts
├── Data
| ├── Factories
| ├── Migrations
| └── Seeders
├── Domain
| ├── Collections
| ├── Commands
| ├── Сriterias
| ├── Entities
| ├── Enums
| ├── Exceptions
| ├── Factories
| ├── Models
| ├── Repositories
| ├── Tests
| └── Values
├── Events
├── Exceptions
├── Http
| ├── Composers
| ├── Controllers
| ├── Middlewares
| ├── Responders
| └── Requests
├── Jobs
├── Listeners
├── Mails
├── Nova
| ├── Actions
| ├── Filters
| └── Resources
├── Notifications
├── Providers
├── Routes
├── Tasks
├── Tests
| ├── Feature
| └── Unit
└── Transfers
├── Resources
└── Transporters
Данная структура ещё дорабатывается и подлежит обсуждению.
Подробнее
Сюда мы выносим всю логику из контроллера. Один класс - одно действие. Список actions должен полностью отображать, что может делать контейнер.
Например, если мы может получить профиль пользователя, то для этого у нас будет отдельный action.
Action в себе содержит только один метод run
, который на вход принимает класс наследник Transporter
из контроллера. Структура action должна быть предельно ленейной минимум ветвлений if
или switch
. Его содержание долно просто читаться сверху вниз. Также кроме метода run
ничего быть не должно. При необходимости часть работы action
МОЖЕТ дилегировать в task-и
или сервисы.
namespace App\Containers\User\Actions;
use ...
class UserUpdateAction extends Action
{
public function run(UserUpdateTransporter $transporter): Responder
{
$this->task(UserUpdateTask::class, $transporter);
return $this->responder(UserUpdatedResponder::class);
}
}
Action
может вызвать Task
, Responder
, SubAction
, Command
. Также он сам может быть вызван из Controller
, Command
, Listener
, Job
.
В примере для обновления пользователя используется
Task
. Сейчас ответственность за обновление/создание/удаление переходит к классамDomain\Commands
. Это сделано для реализации подхода CQS/CQRS. Почему не использовать для этих целейTask-и
? Очень просто, ответственностьTask-ов
слишком размыта. И целесообразность их спользования сейчас находится под большим вопросом.
Содержат необходимые команды для работы контейнера. Команды могут вызывать Action-ы
Содержат необходимые конфиги для работы контейнера
Подробнее
Класс в который мы просто выносим наши scope-методы из моделей. Например мы хотим найти всех мастеров, которые относятся к отпределённому салону. Для этого мы создаём класс критерий, который на вход принимает экземпляр салона.
<?php
namespace App\Containers\Master\Domain\Criterias;
use App\Ship\Parents\Criterias\Criteria;
class AdminCriteria extends Criteria
{
public function apply($query)
{
$query->where('role', 'admin');
}
}
Сейчас сущности не имеют родительских классов. Они описывают объектами которыми мы оперируем в нашем контейнере. Должны содержать набор свойств и методов для работы с сущностью. Взяты из DDD.
Появились в PHP 8.1. Может использоваться вместо ValueObject
, как и Collection
.
Репозитории используются для извлечения сущностей из хранилища (БД, файлы, кэш и т.д.).
Сейчас сиды лежат в обычном месте. Нужно добавить загрузчик сидов, чтобы их можно было по имени запускать из любого контейнера. Сейчас просто подключаются в DatabaseSeeder
.
//TODO Сейчас думаю, как это сделать наиболее простым и удобным способом, чтобы не заставлять разработчика плодить кучи объектов, которых и так уже не мало выходит. Возможно и не так страшно дабавить парочку. Одним больше, одним меньше...
Исключениями должно быть покрыто максимальное количество кода. Например, Action-ы
и Task-и
в 99% случаев должны иметь хотябы одно исключение.
Контроллер принимает Request
и вызывает Action
. Формирование правильного ответа занимается Action
. Контроллер не может вызывать компоненты ниже Action
в иерархии.
Принимает данные, которые упаковывает либо во view
, либо в виде json response
Подробнее
Обычный реквест, но с добавлением Transporter
(DTO). это позволяем достать из запроса отвалидированные данные в видео объекта и быть уверенным, что все необходимые данные (свойства класса DTO) будут нам доступны в правильном наборе. В запросе указываем в методе transporter()
namespace
нашего транспортера:
namespace App\Containers\User\Http\Requests;
use ...
class UserUpdateRequest extends Request
{
...
public function transporter(): string
{
return UserUpdateTransporter::class;
}
...
}
Затем в контроллере достаём эти данные из запроса, вызовом метода transportered()
:
namespace App\Containers\User\Http\Controllers;
use ...
class UserController extends Controller
{
...
public function update(UserUpdateRequest $request)
{
$response = $this->action(
UserUpdateAction::class,
$request->transportered(),
);
return response()->json($response);
}
...
}
И дальше в Action
мы принимаем не какой-то массив, с неясным набором полей, а конкретный экземпляр класса, где может быть точно уверены, что данные там есть:
namespace App\Containers\User\Actions;
use ...
class UserUpdateAction extends Action
{
public function run(UserUpdateTransporter $transporter)
{
//code...
}
}
Resource
, Action
и Filter
помещаются внутри контейнера поближе к домену.
Рауты делятся на два вида api
и web
, как и у Laravel. Но теперь они помещены в каждый контейнер и хранятся отдельно от других.
Подробнее
Задачи - это класс, который не содержит в себе бизнес логику. В нём хранится линь маленькое унарное действие. Они нужны для убирания дублирования из нашего кода. их использование не обязательно в Action-ах
, однако проще сразу вынести какую-то операцию в Task
и переиспользовать при необходимости, чем потом искать все места и заменять на Task-и
.
Task
может работать с моделью либо её репозиторием и не вызывать компонемны выше него по иерархии. Его могет вызывать только Action
и SubAction
namespace App\Containers\User\Tasks;
use ...
class FindUserByPhoneTask extends Task
{
private UserRepository $repository;
public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
public function run(string $phone)
{
return $this->repository->getByPhone($phone);
}
...
}
Пример кода выше является лишь ПРИМЕРОМ и НЕ рекомендуется создавать отдельный класс для простого вызова одного метода репозитория. Репозитории можно и нужно использовать сразу в
Action
, не увеличивая искуственно сложность и запутанность проекта.
Тесты для проверки функциональнсти контейнера. И не забываем ПИСАТЬ ТЕСТЫ, даже простые. Тесты должны быть у каждого контейнера. Не забываем про TDD, поэтому не стеняемся писать тест на то чего ещё нет.
DTO объекты. Используется пакет от Spatie.
Подробнее
Преобразует отвалидированные данные из запроса в объект DTO. Для его использования достаточно создать класс транспортера используя команду в консоли php artisan flag:transporter
. Затем добавить в класс закроса в методе transporter()
namespace App\Containers\User\Transfers\Transporters;
use ...
class UserUpdateTransporter extends Transporter
{
public string $name;
public string $phone;
public string $email;
public string $birthday;
#[MapFrom('offers')]
public bool $allowAds;
}
namespace App\Containers\User\Http\Requests;
use ...
class UserUpdateRequest extends Request
{
public function transporter(): string
{
return UserUpdateTransporter::class;
}
public function rules(): array
{
return [
'name' => 'required|string|min:3',
'phone' => 'nullable|numeric|regex:/\+79[0-9]{9}/',
'email' => 'nullable|email',
'birthday' => 'nullable|date',
'offers' => 'nullable|boolean',
];
}
}