Демо-версия конвертера валют Symfony

Недавно я сделал тестовое задание на Symfony — конвертер валют с прямой и перекрестной конвертацией. Поэтому хочу поделиться результатом с сообществом как примером простого консольного приложения по правилам Symfony: DI, autowiring, service tagging, flexible configuration, вот это все. Надеюсь, это будет полезно для начинающих.

Исходный код

Приложение рассчитывает «обмен валют» по прямым курсам (например, USD -> EUR), а также через «промежуточные» валюты (например, BTC -> USD -> 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 накладывал некоторые ограничения из-за отсутствия реальных десятичных и числовых форматов, а INSERT IGNORE, точность вычисления 16,8 приходилось жестко кодировать.
Возникли проблемы с датами курсов ЕЦБ, поэтому в приложении используется последний доступный день из каждого источника.

Основные моменты

Команды

В приложении есть две консольные команды: «currency:update» — обновление курсов валют (AppCommandCurrencyUpdateCommand) и «currency:exchange» — обмен валют (AppCommandCurrencyExchangeCommand).

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

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

AppProvidersCoinDeskRatesProvider:
    tags: [ 'app.rates_provider' ]

AppProvidersEcbRatesProvider:
    tags: [ 'app.rates_provider' ]

AppServicesRatesUpdater:
    arguments:
        - !tagged_iterator app.rates_provider
Вход в полноэкранный режим Выход из полноэкранного режима
class RatesUpdater
{
    public function __construct(private readonly iterable $ratesProviders, ...)
    {
    }
...
}
Войти в полноэкранный режим Выход из полноэкранного режима

Обмен данными и проверка

Данные для обмена валют и курсов сбережений отправляются через DTO: AppDtoExchange и AppDtoRate, соответственно.
Валидация «AmountRequirements» — требования количества и «ExchangeCurrencyRequirements» — требования валюты накладываются на DTO для обмена валюты.
Кроме того, валидация применяется к сущностям AppEntityPair и AppEntityRate.

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

class AmountRequirements extends Compound
{
    protected function getConstraints(array $options): array
    {
        return [
            new AssertNotBlank(),
            new AssertType(type: 'numeric', message: 'The value {{ value }} is not a valid {{ type }}'),
            new AssertPositive(),
        ];
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Существует также более сложный валидатор существования валют AppValidatorPairCurrencyExistValidator. Он обращается к хранилищу валютных пар и проверяет базу данных на наличие SELECT COUNT(1) FROM pair WHERE base = <passed currency ticker>. Это реализовано с помощью Doctrine Query Builder.

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

Здесь все довольно просто: AppServicesRatesUpdater получает в конструкторе итератор провайдеров курсов валют и вызывает их по очереди (через __invoke, поэтому не нужно придумывать имя метода). Все провайдеры наследуют абстрактный класс AppProvidersRatesProvider и реализуют свои методы преобразования данных в DTO AppDtoRate.

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

Для тестов используется AppProvidersFakeRatesProvider с переопределенным методом fetch и парой тарифов, подключенных к нему.

Полученные в виде DTO тарифы сохраняются в базе данных в прямой и обратной форме, после чего в работу вводится триангулятор AppServicesRatesTriangulator. Он создает все возможные комбинации курсов через промежуточные валюты (так называемые кросс-курсы) и записывает их в сущность AppEntityPair.

Триангуляция основана на принципах 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>

Например

должно вывести

Вы также можете собрать и запустить приложение в 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>

Например

Должен вывести тот же результат, что и локальный запуск PHP.

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

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

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

AppTestsCommandCurrencyExchangeCommandTest — немного сложнее: проверка реальной конвертации с использованием 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
php 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 bin/phpunit
Войти в полноэкранный режим Выход из полноэкранного режима

Добро пожаловать в комментарии и запросы на исправление 🙂

Оцените статью
devanswers.ru
Добавить комментарий