Недавно я сделал тестовое задание на 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
Добро пожаловать в комментарии и запросы на исправление 🙂