Демо Symfony конвертер валют

от автора

Здравствуйте.
Недавно довелось делать тестовое задание на Symfony — конвертер валют с прямой и кросс-конвертацией. Получилось весьма неплохо, поэтому хочу поделиться с сообществом примером простого консольного приложения по всем канонам Symfony: DI, autowiring, тегирование сервисов, гибкая конфигурация, вот это вот всё. Надеюсь, это будет полезно начинающим «симфонистам».

Код приложения https://github.com/vladimirmartsul/symfony-exchange-demo

Приложение считает «обмен валюты» по прямым курсам (например, USD -> EUR), а также через «промежуточные» валюты (например, BTC -> EUR). Также есть фейковые курсы для тестов.

Курсы берутся с сайтов ecb.europa.eu (основные мировые валюты по отношению к EUR) и coindesk.com (BTC к USD). Триангуляция основана на принципах отсюда http://www.dpxo.net/articles/fx_rate_triangulation_sql.html. Для хранения данных используется БД SQLite.

Использовать приложение можно через локальный PHP или в Docker.
Требования к PHP: версия 8.1, модули bcmath, ctype, iconv, intl, pdo_sqlite, simplexml, sqlite3.

На момент выполнения задания у меня было мало опыта с Symfony, в основном, я работал с Laravel, поэтому могут быть некоторые недоработки. Кроме того, использование SQLite наложило свои ограничения (из-за отсутствия настоящих decimal и numeric форматов и INSERT IGNORE пришлось «зашить» точность вычисления 16,8). Ещё был сбой с датами курсов ЕЦБ, из-за чего пришлось пожертвовать проверкой совпадения дат курсов, в приложении используется последний доступый день из каждого источника.

Основные моменты реализации

Команды

В приложении две консольные команды: «currency:update» — обновление курсов валют из указанных источников (\App\Command\CurrencyExchangeCommand) и «currency:exchange» — непосредственно обмен (\App\Command\CurrencyUpdateCommand).

Команды принимают параметры, валидируют данные, передают их в сервисы, ловят исключения и красиво выводят результат в консоль с соответствующими exit status.

Все сервисы и провайдеры передаются своим потребителям через внедрение в конструкторы. Провайдеры курсов помечены тегом «app.rates_provider» в config/services.yaml и по этому тегу передаюся через итератор в \App\Services\RatesUpdater. Очень удобно, на мой взгляд.

App\Providers\CoinDeskRatesProvider:     tags: [ 'app.rates_provider' ]  App\Providers\EcbRatesProvider:     tags: [ 'app.rates_provider' ]  App\Services\RatesUpdater:     arguments:         - !tagged_iterator app.rates_provider
class RatesUpdater {     public function __construct(private readonly iterable $ratesProviders, ...)     {     } ... }

Обмен данными и валидация

Данные для обмена валют и сохранения курсов передаются через DTO: \App\Dto\Exchange и \App\Dto\Rate соотетственно. На DTO для обмена валют наложена валидация «AmountRequirements» — требования к количеству и «ExchangeCurrencyRequirements» — требования к валюте.
Кроме того, валидация наложена на сущности \App\Entity\Pair и \App\Entity\Rate.

Все валидаторы — кастомные, чтобы не засорять потребителей лишними деталями, а также для переиспользованияю. Валидаторы описаны в классах src/Validator/. Большинство из них — составные (Compound) из простейших правил. Например, требования к количеству — «Не пустая строка«, «Тип Numeric» и «Положительное значение«.

class AmountRequirements extends Compound {     protected function getConstraints(array $options): array     {         return [             new Assert\NotBlank(),             new Assert\Type(type: 'numeric', message: 'The value {{ value }} is not a valid {{ type }}'),             new Assert\Positive(),         ];     } }

Есть и более сложный валидатор существования валюты \App\Validator\PairCurrencyExistValidator. Он обращается к репозитарию валютных пар и проверяет в БД SELECT COUNT(1) FROM pair WHERE base = <переданный тикер валюты>. Реализовано через Doctrine Query Builder.

Обновление курсов валют

Тут всё достаточно просто: \App\Services\RatesUpdater получает в конструкторе итератор провайдеров курсов валют и по-очереди вызывает их (через __invoke, чтобы не придумывать название метода). Провайдеры, в свою очередь, наследуют абстрактный класс \App\Providers\RatesProvider и реализуют собственные методы трансформации данных в DTO \App\Dto\Rate.

Абстрактный провайдер «ходит» за курсами по указанному в конфигурации и .env адресу, который внедрён в конструктор вместе с названием базовой валюты. После получения курсов, провайдер парсит их из Json или XML в простой массив и передаёт их в трансформер конкретного провайдера. Парсеры реализованы в src/Parsers/.

Для тестов используется \App\Providers\FakeRatesProvider с переопределённым методом fetch и парой зашитых в него курсов.

Полученные в виде DTO курсы сохраняются в БД в прямом и обратном виде, после чего в работу включается триангулятор \App\Services\RatesTriangulator. Он создаёт все возможные сочетания курсов через промежуточные валюты (т.н. кросс-курсы) и записывает их в сущности \App\Entity\Pair.
Триангуляция основана на принципах http://www.dpxo.net/articles/fx_rate_triangulation_sql.html и в дальнейшем из такой отдельной таблицы с валютными парами гораздо проще получить интересующую пару для конвертации, нежели считать курсы при каждой конвертации.

Если что-то пошло не так, то провайдеры или триангулятор кидают исключения.

Использование приложения

При наличии локально установленного PHP необходимо клонировать репозитарий, установить пакеты, создать БД, выполнить миграции и обновить курсы валют

git clone https://github.com/vladimirmartsul/symfony-exchange-demo.git cd symfony-exchange-demo composer install --no-dev --no-interaction php bin/console doctrine:database:create php bin/console doctrine:migrations:migrate --no-interaction php bin/console currency:update

Рассчитать конвертацию
php bin/console currency:exchange <amount> <from> <to>

Например
php bin/console currency:exchange 2 EUR BTC
должно вывести примерно
[OK] 2 EUR is 0.00005254 BTC

Также можно собрать и запустить приложение в Docker

git clone https://github.com/vladimirmartsul/symfony-exchange-demo.git cd symfony-exchange-demo docker compose up --build

При сборке загрузятся курсы валют.

Рассчитать конвертацию
docker compose run symfony-exchange-demo currency:exchange <amount> <from> <to>

Например
docker compose run symfony-exchange-demo currency:exchange 2 EUR BTC

Результат должен быть таким же как при запуске с локальным PHP.

Тестирование

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

\App\Tests\Command\CurrencyUpdateCommandTest — простая проверка наличия сообщений об успешной загрузке, триангуляции и обновлении курсов.

\App\Tests\Command\CurrencyExchangeCommandTest — чуть сложнее: проверка реальной конвертации при помощи dataProvider’а с несколькими парами валют и ожидаемым результатом. При каждом запуске теста производится обновление курсов валют.

Запустить тесты можно локально, доустановив dev-пакеты

cd symfony-exchange-demo echo APP_ENV=test > .env.local composer install --no-interaction php bin/console doctrine:database:create php bin/console doctrine:migrations:migrate --no-interaction vendor/bin/phpunit

или аналогично в Docker

cd symfony-exchange-demo echo APP_ENV=test > .env.local docker compose run symfony-exchange-demo composer install --no-interaction docker compose run symfony-exchange-demo doctrine:database:create docker compose run symfony-exchange-demo doctrine:migrations:migrate --no-interaction docker compose run symfony-exchange-demo vendor/bin/phpunit

Велкам в комментарии или пул-реквесты 🙂


ссылка на оригинал статьи https://habr.com/ru/post/671956/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *