Pull to refresh

Прагматичная разработка

Level of difficultyMedium
Reading time6 min
Views8.1K
Specialty-кофейни на Кипре
Specialty-кофейни на Кипре

Недавно выдалось свободное время и я сделал простой проект про specialty-кофейни на Кипре: сайт и телеграм-бот по всем канонам "большой" разработки. Люблю хороший кофе 😊

Делюсь своим процессом разработки и рекомендациями как сделать всё задуманное без потери времени.

Update

Как и планировал, добавил тесты. Сначала swagger-php генерит openapi.yaml на основе аттрибутов кода, а затем spectator проверяет ответы API на совпадение с openapi-спецификацией. Популярный L5-Swagger в данном случае избыточен, т.к. основан на том же swagger-php с добавкой Swagger UI.

Цели проекта изначально простые:

  1. карта кофеен на сайте

  2. просмотр подробностей о кофейне

  3. переход в большую карту или приложение Google Maps для маршрута, отзывов и т.д.

  4. список кофеен в боте

  5. поиск кофейни по названию в боте

  6. поиск ближайшей кофейни в боте

  7. случайная кофейня в боте

  8. всё это в минимальном и понятном стиле

Кроме того - не уходить в перфекционизм и бесконечную разработку.

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

Поэтому декомпозируем всё на мелкие задачи, выкидываем всё длиннее нескольких часов и то, что невозможно оценить.

Честно говоря, я срывался несколько раз: первый раз зацепился за идею запустить сервер Caddy без конфига из консоли примерно caddy php_fastcgi 9000, но увы, так можно запускать только обратный прокси и/или файл-сервер; потом ещё добавились хитрые редиректы; в общем, убил на это 2 дня.

Ещё день пришлось потратить на неудачно выбранную библиотеку для Telegram-бота.

В целом всё удалось, код проекта открыт, велкам в пул-реквесты. Адрес сайта - в конце статьи, чтобы меньше походило на рекламу.

Для достижения целей было решено реализовать REST API микросервис, фронтэнд-сайт (подробнее во второй части) и бэкэнд для бота (подробнее в третьей части). Деплой - на что-нибудь современное управляемое с free tier, а не VPS или shared-хостинг. Бот вообще хорошо ложится на идеологию Serverless/FaaS.

Первым шагом я зарегистрировал домен: 20 евро из собственного кармана - хорошая мотивация не потратить их впустую реализовать задуманное.

К слову про регистрацию: сложно предугадать, как сложится судьба проекта: возможно, получится выгодно продать, а, возможно, захочется избавиться от него без следов. Поэтому лучше регистрировать все внешние сервисы на отдельные аккаунты: почта, домен, хостинг, аналитика, мониторинг и т.д. Дополнительно получится использовать free tier'ы и триалы.

REST API микросервис

У меня есть хороший опыт работы с Laravel и Symfony, поэтому для быстрой реализации я выбрал знакомую и лёгкую технологию. Потом обязательно перепишу на Go. А использование свежей версии PHP 8.1 позволило написать чуть меньше кода и получить чуть выше производительность. Promoted properties, readonly и строгая типизация сильно облегчают разработку.

Для бОльшего облегчения из Laravel удалены неиспользуемые пакеты и сервисы: получился почти Lumen.

В composer.json можно отметить пакеты как "установленные" и они не будут устанавливаться на самом деле. Очень удобно для выпиливания избыточных полифилов, например, так:

"replace": {
    "symfony/polyfill-ctype": "*",
    "symfony/polyfill-iconv": "*",
    "symfony/polyfill-intl-grapheme": "*",
    "symfony/polyfill-intl-idn": "*",
    "symfony/polyfill-mbstring": "*",
    "symfony/polyfill-php72": "*",
    "symfony/polyfill-php73": "*",
    "symfony/polyfill-php80": "*",
    "symfony/polyfill-php81": "*",
    "dragonmantank/cron-expression": "*",
    "egulias/email-validator": "*",
    "league/commonmark": "*",
    "league/flysystem": "*",
    "symfony/mime": "*",
    "symfony/var-dumper": "*",
    "tijsverkoyen/css-to-inline-styles": "*"
}

Ещё можно отключить platform-check, чтобы не проверять версию PHP при каждом запросе, и ограничиться проверкой при установке пакетов. Также полезно включить classmap-authoritative, чтобы классы загружались только из созданной composer'ом карты, а не из каждого use, но это будет мешать разработке, поэтому достаточно включить при деплое.

Итоговый composer.json и config/app.php. На подобную оптимизацию ушло менее часа, поэтому ok. Но более глубокая оптимизация потребует гораздо больше времени, так что не сейчас.

Архитектура

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

Входные данные валидируются отдельными Request'ами, выходные - заворачиваются в GeoJson Resourse'ы. Один класс - одна ответственность.

На момент разработки фронтэнда был всего один endpoint /cafes со списком всех кофеен: это позволило быстро запустить API и не мокать его для других частей проекта. Во время разработки бота я добавил ещё несколько endpoint'ов.

БД

В качестве БД для начала используется SQLite - это позволило не тратить время на развёртывание классических MySQL/PostgreSQL. Более того, я уверен, что при нагрузке в 100 посещений в день и нескольких десятках или сотнях записей в таблицах, SQLite - отличное решение для микропродакшена.

Сидирование данными производится из обычного массива в database/seeders/CafeSeeder.php в процессе деплоя. В дальнейшем планирую сделать 1-2 консольные команды для редактирования данных, потому что гораздо быстрее, чем любая визуальная админка.

Поиск

API умеет в полнотекстовый поиск благодаря Scout c драйвером "collection": он позволяет искать по каждому полю модели обычным "LIKE %smth%" запросом и не требует полнотекстовых индексов в БД. Подключение заняло 15 минут, поэтому ok.

Статика

В сервисе есть немного обязательных статических файлов:

  • robots.txt с запретом индексации

  • favicon.ico, который любят многие сервисы

  • humans.txt

  • и т.д.

Тесты

В качестве отдельных Feature-тестов используются http-request'ы в PHPStorm по всем endpoint'ам с правильными и ошибочными данными. Проверка - глазами, но в CI не положишь ¯\_(ツ)_/¯

Написание нормальных тестов - первоочередная задача и я планирую использовать для этого Pest https://pestphp.com/.

Конфигурация

Laravel, в отличии от Symfony, не читает конфигрурацию из файла .env.local чтобы переопределить/дополнить конфигурацию из .env, да и вообще не рекомендует хранить .env в репозитарии. Это хороший подход, но он не очень удобен, когда параметров конфигурации много.
Можно сделать чуть иначе: записать локальные параметры в .env (и не добавлять его в репозиторий), а все боевые параметры и названия параметров-секретов - в .env.production и добавить его в репозиторий. Параметр APP_ENV=production, а также сами секреты необходимо записать средствами хостинга и/или деплоя.

В таком случае .env.local заменит (не дополнит!) конфигурацию из .env.production, а перечисление всех используемых параметров (даже без значений) в .env.production упростит понимание проекта. Бесполезный, в данном случае, .env.example удаляем.

Мониторинг

По окончании первого этапа разработки добавляем в проект Sentry: в .env.production достаточно указать пустое значение SENTRY_LARAVEL_DSN (для наглядности), а фактическое значение записать в секрет.

Деплой

Для размещения сервера используется платформа Fly.io с управляемыми microVM Firecracker. В отличии от популярного Heroku, никогда не спит, имеет хороший free tier и позволяет разместить как статику, так и любой сервер приложений. Кроме того, есть различные стратегии деплоя и отката изменений, health check'и и география размещения на выбор.

Настроить среду выполнения можно автоматически командой flyctl launch из каталога приложения - сервис сам определелит необходимые компоненты и соберёт конфиг fly.toml и Dockerfile. Либо можно написать свои конфиг и Dockerfile.

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

В относительно свежих версиях Docker реализовано кеширование слоёв, поэтому нет смысла писать все инструкции в одной команде RUN. Наоборот, лучше размещать "тонкие" слои RUN и COPY в порядке увеличения частоты изменений данных в них.

Например, дистрибутивы и пакеты ОС меняются редко, поэтому слой RUN apk add ... может быть в самом начале Dockerfile.

Пакеты composer обновляются чаще, но не так часто как исходный код проекта, поэтому слои COPY composer.* . и RUN composer install --no-autoloader --no-dev --no-interaction --no-scripts могут быть указаны в середине Dockerfile и смогут браться из кеша.

Ну а COPY --chown=www-data:www-data . ., RUN composer dump-autoload --classmap-authoritative --no-interaction и возможные другие команды, затрагивающие исходный код проекта могут быть в самом конце и будут выполняться, только если изменится сам код проекта, а не пакеты ОС или зависимости composer.

Итоговый Dockerfile.

Внутри контейнера находится классический PHP-FPM на Alpine и проксирующий сервер Caddy. Он чуть легче и проще в настройке, чем привычный Nginx и состоит из одного бинарника и одного необязательного конфига.

Конфиг

{
    admin off
    auto_https off
    log
    skip_install_trust
}

{$APP_URL_INSECURE}:8080 {
    root * /var/www/html/public
    php_fastcgi 127.0.0.1:9000
    file_server
    encode gzip
    header -Server
}

:8080 {
    respond 404
    header -Server
}

Тут "file_server" отвечает за раздачу статики.

Платформа Fly.io сама терминирует https и управляет сертификатами, поэтому приложению в контейнере достаточно обрабатывать обычный http-трафик.

Главный минус при размещении PHP-приложений - необходимость использования прокси-сервера или балансера, их совместный запуск и работа. В данном случае пришлось использовать достаточно тяжёлый supervisor, который тянет за собой Python. Но это быстрое и работающее решение, позволяющее не застревать в разработке и настройке.

Итоговый supervisord.conf

CI/CD

Тут всё просто: Github Action из одного workflow и тот же самый flyctl.

Благодарности

Спасибо @kvasilenkoза код-ревью проекта.

На этом этапе микросервис API работает, размещён в production-окружении и доступен всем пользователям. План-минимум выполнен :-)

Репозиторий API, сайт https://specialtycoffee.cy

Во второй части расскажу про создание фронтэнда и в третьей - про телеграм-бота.

Tags:
Hubs:
Total votes 11: ↑11 and ↓0+11
Comments3

Articles