Пятница. Говнокод. Парсинг плейсхолдеров.

Фанат

oncle terrible
Команда форума
Решил я тут прогнуться под любителей именованных пейсхолдеров.
Соответственно, сделать запросы совместимыми с ПДО
Соответственно, ввести дефолтный плейсхолдер, без указания типа, обрабатывается, как строковый.
Соответственно, поддерживать ключи как с двоеточиями, так и без.

В этой связи накатал какой-то ужас.
Рабочий урезанный вариант класса можно взять здесь https://github.com/colshrapnel/safemysqli.dev/blob/master/safemysqli.class.php

Ну и сам код на посмотреть.

PHP:
	function prepare($query, $args = array() )
	{
		$pattern = '~(\?[nsiuap]?|[nsiuap]?:[a-zA-Z_][a-zA-Z0-9_]*)~u';
		$array   = preg_split($pattern, $query , null, PREG_SPLIT_DELIM_CAPTURE);
		$out     = '';

		if (key($args) === 0)
		{
			$mode = 'numeric';

			$anum  = count($args);
			$pnum  = floor(count($array) / 2);
			if ( $pnum != $anum )
			{
				$this->error("Number of args ($anum) doesn't match number of placeholders ($pnum) in [$query]");
			}

		} else {

			$mode = 'named';
		}

		foreach ($array as $i => $part)
		{
			if ( ($i % 2) == 0 )
			{
				$out .= $part;
				continue;
			}

			if ($part[0] == '?')
			{
				if ($mode == 'named')
				{
					$this->error("Cannot mix named and positional placeholders");
				}
				$value = array_shift($args);
				$type  = trim($part,"?");

			} else {
    
				if ($mode == 'numeric')
				{
					$this->error("Cannot mix named and positional placeholders");
				}

				list($type, $key) = explode(":", $part);

				if (isset($args[$key]))
				{
					$value = $args[$key];
					
				} elseif (isset($args[":$key"])) {

					$value = $args[":$key"];

				} else {
					
					$this->error("No key found for the named placeholder [$key] in the data array");
				}
			}

			switch ($type)
			{
				case '':
				case 's':
					$part = $this->escapeString($value);
					break;
				case 'n':
					$part = $this->escapeIdent($value);
					break;
				case 'i':
					$part = $this->escapeInt($value);
					break;
				case 'a':
					$part = $this->createIN($value);
					break;
				case 'u':
					$part = $this->createSET($value);
					break;
				case 'p':
					$part = $value;
					break;
			}
			$out .= $part;
		}
		return $out;
	}
PHP:
include 'safemysqli.class.php';
$db = new SafeMySQLi();

$sql = "SELECT :one FROM n:two WHERE f = i:tre OR f = i:tre OR f = :four\n";
$args['one'] = 'hello';
$args['two'] = 'table';
$args['tre'] = 5;
$args[':four'] = 666;
echo $db->prepare($sql, $args);

$sql = "SELECT ?s FROM ?n WHERE f = ?i OR f = ?\n";
$args = array('hello', 'table', 5,666);
echo $db->prepare($sql, $args);

echo $db->prepare("SELECT c FROM t \n");

SELECT 'hello' FROM `table` WHERE f = 5 OR f = 5 OR f = '666'
SELECT 'hello' FROM `table` WHERE f = 5 OR f = '666'
SELECT c FROM t

уду рад критическим замечаниям.
 

WMix

герр M:)ller
Партнер клуба
PHP:
$sql = "SELECT ?s FROM ?n WHERE f = ?i OR f = ?\n";
$args = array('hello', 'database.table', 5,666); // не забыл?
 

Vladson

Сильнобухер
Так как у меня ещё пятница (не протрезвел я) пофлужу....

Даже такие "простые" вещи (замечу слово простые в кавычках, т.е как простые числа, вроде простые а попробуй их найди все) и получается что никто не знает как сделать максимально правильно. Все только знают только как делать не надо. Однако, почему-то до сих пор программирование считается больше "наукой" чем "искусством". В науке должны быть строгие формулы по которым надо было бы писать код, но их нет. Там было бы два варианта, ответ есть или ответа нет. Уж думаю если бы формулы эти были, уж кто нибудь с этого форума за всё время их существования их нашёл бы. Есть только правила как писать нельзя (у каждого поэта/художника/музыканта есть такие правила) а как писать надо даже опытные люди не всегда могут понять.

Программирование требует именно творчества, а от нас требуют гарантии и сроки. Вот как так можно жить...
 

ksnk

прохожий
А вот почему нельзя смешивать именованные и неименованные параметры? Если объявить, что имя у именованного параметра обязано начинаться с буквы, то все параметры с ключами is_int будут позиционированными.

Чтобы в ногу себе стрелять получалось пореже - можно сначала указывать позиционированные, а потом только именованные.

Иначе слишком сильный перекос, либо переписывай все на имена, либо плюй на новые фичи
 

ksnk

прохожий
Ну был у меня , к примеру запрос
PHP:
$x=$db->select('select * from xxx where one=? anf two=?',$one,$two);
Внезапно оказалось, что нужно пристроить к нему маловразумительное условие по большому количеству критериев.

PHP:
$where=''; // с массивом - элегантнее, но и так видно
if( isset($request['brand'])){
  $where.=' and brand=s:brand' ; args['brand']=$request['brand'];
}
...
$x=$db->select('select * from xxx where one=? anf two=?'.$where, array_merge(array($one,$two),$args));
Построение таких критериев - довольно убедительный довод в пользу именованных параметров, однако старый запрос при этом придется переписывать.

Хотя довод слабоват. Все равно запрос переписывать...
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
.hell0w0rd вероятно, чтобы избежать ограничений нативных плейсхолдеров
 

fixxxer

К.О.
Партнер клуба
сделай-ка мне in($array) нативными плейсхолдерами, например

конечно, можно, где это в принципе возможно, генерировать плейсхолдеры, а где нет - подставлять строки, но зачем это усложнение на пустом месте?
 

Фанат

oncle terrible
Команда форума
А почему ты не хочешь предложить использовать нативные плейсхолдеры?)
https://github.com/gajus/pdo/blob/master/pdo.class.php#L17
Это очень интересный вопрос, и я постараюсь на него подробно ответить.

Если коротко - то да, так как grigori сказал.
Но тут, скорее, не ограничения как нативных как таковых, а их несовместимость с не-нативными и очень малая востребованность.

С одной стороны - хочу. У меня даже третий параметр был у функции, который я пока выпилил - как раз режим нативности препаредов (по умолчанию - офф).
И планировал я использовать нативные в двух вариантах -

1. Только для исполнения запроса, однократно. Внутри хелпер-функции у меня есть и запрос, и данные - я могу исполнять как хочу. то есть, нет проблем строки и числа транслировать в нативные и заекзекьютить. Нужно для получения информации о типах. По какой-то причине mysqli выдает тебе инты и нуллы(!) только для prepared запроса. Возможно, станут в будущем выдавать и для обычной квери, и надобность отпадёт. Надобность, впрочем, тоже так себе.
2. Как-то ухитриться сделать раздельные prepare и execute, поставив при этом искусственное ограничение на используемые типы - только нативные. Но сама по себе фишка "один препейр - много екзекьютов" нужна в тысячной доле процента случаев (в пхпе) - т.е. практически не востребована.

То есть, с одной стороны, мы имеем очень спорную востребованность.
С другой - несовместимость с одной из ключевых фич safeMysql, о которой как раз выше напомнил ksnk- парсинг произвольного куска запроса. При котором значение однозначно подставляется в запрос.
Если делать поддержку нативных, подставляя плейсхолдер в отпаршенный запрос, то придется куда-то класть подставляемое значение... а куда? Если поможешь придумать - я буду очень благодарен.

Плюс очевидная несовместимость с идентификаторным плейсхолдером. Непонятно, как с ним быть опять же. Идеи принимаются.

Отдельное спасибо за ссылку. Чувак сделал то, что я собирался тут месяц назад, но ниасилил. Попробую подтолкнуть его в сторону расширенного набора пейсхолдеров.
Если сделает, то я буду всем давать на него ссылку на стаковерфлое. А ты его откуда знаешь?
 

Фанат

oncle terrible
Команда форума
Построение таких критериев - довольно убедительный довод в пользу именованных параметров, однако старый запрос при этом придется переписывать.
Хотя довод слабоват. Все равно запрос переписывать...
Фишка-то safeMysql как раз в том, что она сразу парсит такие куски, безо всяких $args.
И я это нахожу очень большим достоинством.
уже подствленные данные в конечном запросе будут всяко нагляднее именованного плейсхолдера :)

И - да, мне тоже кажется, что довод слабоват.
В любом случае, тут все делается в режиме совместимости с ПДОшечкой - она тоже не разрешает мешать.
Я думаю, это из-за того, что именованный - в тории - должен обслуживать несколько плейсхолдеров - на это упирают почти все требователи. А в таком вариенте арифметика с позиционированием вообще превратится в адъ :)
 

Фанат

oncle terrible
Команда форума
сделай-ка мне in($array) нативными плейсхолдерами, например

конечно, можно, где это в принципе возможно, генерировать плейсхолдеры, а где нет - подставлять строки, но зачем это усложнение на пустом месте?
Всё правильно. Использование массива по определению предполагает его вариативность. Тупо количество элементов может гулять.
То есть - для однократного исполнения если только. А смысл?
 

hell0w0rd

Продвинутый новичок
Фанат
Та ни от куда) Переодически заглядываю в https://github.com/languages/PHP, как-то раз был в топе по звездам, заглянул и запомнил)
На счет нескольких экзекьютов - можно сделать функцию multiQuery, например
Если делать поддержку нативных, подставляя плейсхолдер в отпаршенный запрос, то придется куда-то класть подставляемое значение... а куда?
вот это я не особо понял, в чем проблема?
fixxxer
Ну обертка же пишется не от хорошей жизни, а чтобы сделать мир чуточку лучше? Разворачивать массив. А вообще кто следит за баг-трекерами, разработчикам pdo-расширения такой вопрос задавали?
PS Фанат, мне кажется сильно поможет написать тесты на то, что хочешь в итоге видеть:)
 

Фанат

oncle terrible
Команда форума
вот это я не особо понял, в чем проблема?
Ну вот смртри, тут примеры:
http://phpfaq.ru/examples#where
http://phpfaq.ru/examples#multiparse
В текущем варианте данные подставляются прямо на месте
А если их копить отдельно, то куда? В отдельную глобальную переменную? Пременную класса? Ни то ни другое мне не нравится.
 

hell0w0rd

Продвинутый новичок
Ну вот смртри, тут примеры:
http://phpfaq.ru/examples#where
http://phpfaq.ru/examples#multiparse
В текущем варианте данные подставляются прямо на месте
А если их копить отдельно, то куда? В отдельную глобальную переменную? Пременную класса? Ни то ни другое мне не нравится.
Тогда единственный выход, который я вижу - возвращать объект, если очень хочется - с методом __toString для тех, кто хочет готовый запрос посмотреть, ну и сделать метод concatQuery, который будет соединять эти запросы
PHP:
class QueryPart
{
    protected $raw;
    protected $data;
    protected $parsed;

    public function __construct($query, $data)
    {
        $this->raw = $query;
        $this->data = $data;
    }

    /**
     * @param QueryPart|string|array $query1
     * @param QueryPart|string|array $query2
     * @return $this
     */
    public function concat($query1, $query2 = null)
    {
        foreach (func_get_args() as $query) {
            if ($query instanceof QueryPart) {
                $this->raw .= $query->raw;
                $this->addData($query->data);
            } elseif (is_string($query)) {
                $this->raw .= $query;
            } elseif (is_array($query)) {
                $this->addData($query);
            }
        }

        return $this;
    }

    public function addData(array $data)
    {
        $this->data = array_merge($this->data, $data);

        return $this;
    }
}
Типо такого, ну и обработку сюда засунуть бы хорошо
 

fixxxer

К.О.
Партнер клуба

hell0w0rd

Продвинутый новичок
А смысл?

PHP:
$stmt = $db->prepare("select * from t where i in ?a");
$stmt->execute(array(1));
$stmt->execute(array(1,2));
- так сделать не получится. А если мы не можем вынести отдельно prepare, весь смысл теряется.
Если хочется именно так - то либо делать декоратора, либо делать multiQuery, либо прекратить поддерживать mysqli и юзать pdo, где как раньше заметили через одно место, но можно подменять классы
В доктрине кстати декораторами обошлись: https://github.com/doctrine/dbal/blob/master/lib/Doctrine/DBAL/Driver/Mysqli/MysqliStatement.php
 
Сверху