Code Monkey home page Code Monkey logo

funpay-test's Introduction

Тестовое задание 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 находятся примеры (тесты). Тесты обязательно должны быть успешными (в противном случае код рассматриваться не будет).

Код должен работать с PHP 8.3.

Решение

Самый простой вариант: использовать замены строк:

// preg_match_all('/(\?d|\?f|\?a|\?#|\?)/i', $query, $matches)
// или
// str_replace(...)
// и т. п.

Но тогда мы будем заменять плейсхолдеры даже внутри литеральных строк. По ТЗ не совсем понятно, разрешены ли строковые литералы внутри запросов; принимаем более сложный сценарий, что разрешены. Соответственно, нам нужно разбирать и строки, и типизированные параметры.

Для решения задачи парсинга я сделал простенький конечный автомат. Комментарии внутри запроса по ТЗ запрещены, так что их не обрабатываю. Но такой подход хорош тем, что его несложно расширить до комментариев да и любых других нужных лексических единиц внутри запроса.

Основа решения — в файле QueryParser.php

По сути, нам нужно два переключающихся состояния:

  • токены/лексемы (перечисление QueryParserState.php);
  • условные блоки (достаточно обычной логической переменной). По ТЗ вложенных условных блоков нет, поэтому обходимся без стека вложенных блоков.

Ещё в ТЗ не совсем однозначно написано про случай, когда в параметре находится ассоциативный массив (имя => значение). Я принял, что это вариант в формате UPDATE-запроса: идентификатор = значение, идентификатор = значение, ...

Также я добавил несколько дополнительных тестов:

  • на управляющие конструкции внутри строковых литералов,
  • на открытие и закрытие условных блоков сразу после ?,
  • на ошибки парсинга (незакрытые блоки, вложенные блоки, и т. п.).

Код написан на современном PHP 8.3 (enum’ы, readonly, и т. д.) без фреймворков. Где это важно, я добавил подсказки типов в докблоках, например, чтобы уточнить типы массивов:

/**
 * Либо список произвольных элементов, либо ассоциативный массив: [строка => произвольное значение]
 * @param list<mixed>|array<string,mixed> $values
 */

Примечания

  • Код конечного автомата немного сумбурный и в нескольких местах излишен для тестового задания, потому что я хотел обработать разные корнер-кейсы (типа незакрытых блоков или двойного открытия/закрытия), а также намеренно не разбивал на более мелкие функции, не размазывал состояние по классу, написал внутри одной главной функции, и в разных участках автомата оставил немного копипаста, чтобы можно было прочитать не прыгая по исходнику.

  • Не использовал „lookahead“ (просмотр символов вперёд) в конечном автомате, чтобы был более „лабораторный“/чистый вариант автомата. Однако с просмотром даже на один символ вперёд код разбора параметров можно хорошо упростить, ведь у нас все управляющие токены максимум по два символа.

  • Тесты по-хорошему бы переписать на Pest, потому что в заданном шаблоне их код вообще не ахти (в Идее сплошные ворнинги по делу), но я так понимаю, задача намеренно поставлена сделать всё без фреймворков, так что решено оставить.

  • В задании сказано, что в случае значения null вставлять в запрос NULL в верхнем регистре, и так в SQL выглядит красивее, согласен; однако в тестовых запросах null написан в нижнем регистре, поэтому я сделал тоже в нижнем.

  • Стиль кодирования: PSR-12T (PSR-12 со СмартТабами).

Запуск

Самый простой способ запустить решение — выполнить программу в уже подготовленном Докер-контейнере.

Программа сама поймёт, запущена она из-под Докера или в локальном окружении, и попытается подключиться к БД:

  • в Докере — к БД в контейнере по внутреннему хосту database;
  • в локальном окружении — к БД по локальному хосту 127.0.0.1.

В Докере

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

git clone https://github.com/maximal/funpay-test
cd funpay-test
docker compose run app

Эта команда прогонит миграции и тесты. База данных открывает свой порт 3306 из контейнера наружу, так что в принципе она будет доступна и по хосту 127.0.0.1.

Ожидаемый вывод:

$ 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
git clone https://github.com/maximal/funpay-test
cd funpay-test
php test.php

Ожидаемый вывод:

$ php test.php
Running tests...
Running additional tests...
Running parse error tests...
Tests OK

Контакты

funpay-test's People

Contributors

maximal avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.