Как стать автором
Обновить

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

Время на прочтение5 мин
Количество просмотров3.5K

Здравствуйте.
Недавно довелось делать тестовое задание на 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\CurrencyUpdateCommand) и "currency:exchange" - непосредственно обмен (\App\Command\CurrencyExchangeCommand).

Команды принимают параметры, валидируют данные, передают их в сервисы, ловят исключения и красиво выводят результат в консоль с соответствующими 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
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

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

Теги:
Хабы:
Всего голосов 7: ↑5 и ↓2+3
Комментарии3

Публикации

Истории

Работа

PHP программист
150 вакансий

Ближайшие события