Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/maximal/funpay-test
Senior PHP developer vacancy test for FunPay
https://github.com/maximal/funpay-test
Last synced: about 2 months ago
JSON representation
Senior PHP developer vacancy test for FunPay
- Host: GitHub
- URL: https://github.com/maximal/funpay-test
- Owner: maximal
- Created: 2024-04-17T05:51:59.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2024-04-17T06:24:01.000Z (9 months ago)
- Last Synced: 2024-04-17T09:56:09.927Z (9 months ago)
- Language: PHP
- Size: 12.7 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Тестовое задание FunPay
## Задание
Написать функцию формирования SQL-запросов (MySQL) из шаблона и значений параметров.Места вставки значений в шаблон помечаются вопросительным знаком, после которого может следовать спецификатор преобразования. Спецификаторы:
* `?d` — конвертация в целое число;
* `?f` — конвертация в число с плавающей точкой;
* `?a` — массив значений;
* `?#` — идентификатор или массив идентификаторов.Если спецификатор не указан, то используется тип переданного значения, но допускаются только типы `string`, `int`, `float`, `bool` (приводится к `0` или `1`) и `null`.
Параметры `?`, `?d`, `?f` могут принимать значения `null` (в этом случае в шаблон вставляется `NULL`).
Строки и идентификаторы автоматически экранируются.Массив (параметр `?a`) преобразуется либо в список значений через запятую (список), либо в пары идентификатор и значение через запятую (ассоциативный массив).
Каждое значение из массива форматируется в зависимости от его типа (идентично универсальному параметру без спецификатора).Также необходимо реализовать условные блоки, помечаемые фигурными скобками.
Если внутри условного блока есть хотя бы один параметр со специальным значением, то блок не попадает в сформированный запрос.
Специальное значение должно возвращаться методом `skip()`. Нужно выбрать подходящее значение на своё усмотрение.
Условные блоки **не могут** быть вложенными.При ошибках в шаблонах или значениях выбрасывать исключения.
Для упрощения задачи предполагается, что в передаваемом шаблоне не будут использоваться комментарии SQL.
В файле Database.php находится заготовка класса с заглушками в виде исключений. Нужно реализовать методы `buildQuery()` и `skip()`.
В файле [DatabaseTest.php](src/DatabaseTest.php) находятся примеры (тесты). Тесты обязательно должны быть успешными (в противном случае код рассматриваться не будет).Код должен работать с PHP 8.3.
## Решение
Самый простой вариант: использовать замены строк:
```php
// preg_match_all('/(\?d|\?f|\?a|\?#|\?)/i', $query, $matches)
// или
// str_replace(...)
// и т. п.
```
Но тогда мы будем заменять плейсхолдеры даже внутри литеральных строк.
По ТЗ не совсем понятно, разрешены ли строковые литералы внутри запросов;
принимаем более сложный сценарий, что разрешены.
Соответственно, нам нужно разбирать и строки, и типизированные параметры.Для решения задачи парсинга я сделал простенький конечный автомат.
Комментарии внутри запроса по ТЗ запрещены, так что их не обрабатываю.
Но такой подход хорош тем, что его несложно расширить до комментариев
да и любых других нужных лексических единиц внутри запроса.Основа решения — в файле [QueryParser.php](src/QueryParser.php)
По сути, нам нужно два переключающихся состояния:
* токены/лексемы (перечисление [QueryParserState.php](src/QueryParserState.php));
* условные блоки (достаточно обычной логической переменной).
По ТЗ вложенных условных блоков нет, поэтому обходимся без стека вложенных блоков.Ещё в ТЗ не совсем однозначно написано про случай,
когда в параметре находится ассоциативный массив (`имя => значение`).
Я принял, что это вариант в формате `UPDATE`-запроса:
`идентификатор = значение, идентификатор = значение, ...`Также я добавил несколько дополнительных тестов:
* на управляющие конструкции внутри строковых литералов,
* на открытие и закрытие условных блоков сразу после `?`,
* на ошибки парсинга (незакрытые блоки, вложенные блоки, и т. п.).Код написан на современном PHP 8.3 (`enum`’ы, `readonly`, и т. д.) без фреймворков.
Где это важно, я добавил подсказки типов в докблоках, например, чтобы уточнить типы массивов:
```php
/**
* Либо список произвольных элементов, либо ассоциативный массив: [строка => произвольное значение]
* @param list|array $values
*/
```### Примечания
* Код конечного автомата немного сумбурный и в нескольких местах излишен для тестового задания,
потому что я хотел обработать разные корнер-кейсы
(типа незакрытых блоков или двойного открытия/закрытия),
а также намеренно не разбивал на более мелкие функции, не размазывал состояние по классу,
написал внутри одной главной функции, и в разных участках автомата оставил немного копипаста,
чтобы можно было прочитать не прыгая по исходнику.* Не использовал „lookahead“ (просмотр символов вперёд) в конечном автомате,
чтобы был более „лабораторный“/чистый вариант автомата. Однако с просмотром даже
на один символ вперёд код разбора параметров можно хорошо упростить,
ведь у нас все управляющие токены максимум по два символа.* Тесты по-хорошему бы переписать на [Pest](https://pestphp.com/),
потому что в заданном шаблоне их код вообще не ахти (в Идее сплошные ворнинги по делу),
но я так понимаю, задача намеренно поставлена сделать всё без фреймворков,
так что решено оставить.* В задании сказано, что в случае значения `null` вставлять в запрос `NULL` в верхнем регистре,
и так в SQL выглядит красивее, согласен; однако в тестовых запросах `null` написан
в нижнем регистре, поэтому я сделал тоже в нижнем.* Стиль кодирования: PSR-12T ([PSR-12](https://www.php-fig.org/psr/psr-12/)
со [СмартТабами](http://www.emacswiki.org/emacs/SmartTabs)).## Запуск
Самый простой способ запустить решение — выполнить программу в уже подготовленном Докер-контейнере.Программа сама поймёт, запущена она из-под Докера или в локальном окружении,
и попытается подключиться к БД:
* в Докере — к БД в контейнере по внутреннему хосту `database`;
* в локальном окружении — к БД по локальному хосту `127.0.0.1`.### В Докере
Склонировать репозиторий, запустить приложение в Докере.
```shell
git clone https://github.com/maximal/funpay-test
cd funpay-test
docker compose run app
```
Эта команда прогонит миграции и тесты. База данных открывает свой порт `3306`
из контейнера наружу, так что в принципе она будет доступна и по хосту `127.0.0.1`.Ожидаемый вывод:
```plain
$ docker compose run app
# ...
# ... сборка контейнера ...
# ...
Running database seeder...
We’re In Docker
Database credentials: database, root, password, database, 3306
Database connection failed: Connection refused
Retrying in 5 seconds...
`users` table dropped
`users` table created
`users` table seeded
`users` table records count: 9
Database seeding OK
Running tests...
Running additional tests...
Running parse error tests...
Tests OK
```### Без Докера
Склонировать репозиторий, проверить зависимости, запустить приложение в локальном окружении.Понадобится PHP 8.3, расширение `mysqli` (`sudo apt install php-mysqli`),
и подготовленная база данных MariaDB/MySQL:
* host: `127.0.0.1`
* username: `root`
* password: `password`
* database: `database`
* table: `users````shell
git clone https://github.com/maximal/funpay-test
cd funpay-test
php test.php
```Ожидаемый вывод:
```plain
$ php test.php
Running tests...
Running additional tests...
Running parse error tests...
Tests OK
```## Контакты
* Телеграм: https://t.me/maximal
* Гитхаб: https://github.com/maximal
* Почта: [email protected]