$params The parameters that are used for the query, and will be updated * @param string $convert_to Either pdo or pg, will be converted to lower case for check * @return array{original:array{query:string,params:array},type:''|'named'|'numbered'|'question_mark',found:int,matches:array,params_lookup:array,query:string,params:array} * @throws \OutOfRangeException 200 */ public static function convertPlaceholderInQuery( string $query, array $params, string $convert_to = 'pg' ): array { $convert_to = strtolower($convert_to); $matches = []; $query_split = '[(=,?-]|->|->>|#>|#>>|@>|<@|\?\|\?\&|\|\||#-'; $pattern = '/' // prefix string part, must match towards // seperator for ( = , ? - [and json/jsonb in pg doc section 9.15] . '(?:\'.*?\')?\s*(?:\?\?|' . $query_split . ')\s*' // match for replace part . '(?:' // digit -> ignore . '\d+|' // other string -> ignore . '(?:\'.*?\')|' // :name named part (PDO) . '(:\w+)|' // ? question mark part (PDO) . '(?:(?:\?\?)?\s*(\?{1}))|' // $n numbered part (\PG php) . '(\$[1-9]{1}(?:[0-9]{1,})?)' // end match . ')' // single line -> add line break to matches in "." . '/s'; // matches: // 1: :named // 2: ? question mark // 3: $n numbered $found = preg_match_all($pattern, $query, $matches, PREG_UNMATCHED_AS_NULL); // if false or null set to -1 // || $found === null if ($found === false) { $found = -1; } /** @var array 1: named */ $named_matches = array_filter($matches[1]); /** @var array 2: open ? */ $qmark_matches = array_filter($matches[2]); /** @var array 3: $n matches */ $numbered_matches = array_filter($matches[3]); // count matches $count_named = count($named_matches); $count_qmark = count($qmark_matches); $count_numbered = count($numbered_matches); // throw if mixed if ( ($count_named && $count_qmark) || ($count_named && $count_numbered) || ($count_qmark && $count_numbered) ) { throw new \OutOfRangeException('Cannot have named, question mark and numbered in the same query', 200); } // rebuild $matches_return = []; $type = ''; $query_new = ''; $params_new = []; $params_lookup = []; if ($count_named && $convert_to == 'pg') { $type = 'named'; $matches_return = $named_matches; // only check for :named $pattern_replace = '/' . '((?:\'.*?\')?\s*(?:\?\?|' . $query_split . ')\s*)' . '(\d+|(?:\'.*?\')|(:\w+))' . '/s'; // 0: full // 1: pre part // 2: keep part UNLESS '3' is set // 3: replace part :named $pos = 0; $query_new = preg_replace_callback( $pattern_replace, function ($matches) use (&$pos, &$params_new, &$params_lookup, $params) { // only count up if $match[3] is not yet in lookup table if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) { $pos++; $params_lookup[$matches[3]] = '$' . $pos; $params_new[] = $params[$matches[3]] ?? throw new \RuntimeException( 'Cannot lookup ' . $matches[3] . ' in params list', 210 ); } // add the connectors back (1), and the data sets only if no replacement will be done return $matches[1] . ( empty($matches[3]) ? $matches[2] : $params_lookup[$matches[3]] ?? throw new \RuntimeException( 'Cannot lookup ' . $matches[3] . ' in params lookup list', 211 ) ); }, $query ); } elseif ($count_qmark && $convert_to == 'pg') { $type = 'question_mark'; $matches_return = $qmark_matches; // order and data stays the same $params_new = $params; // only check for ? $pattern_replace = '/' . '((?:\'.*?\')?\s*(?:\?\?|' . $query_split . ')\s*)' . '(\d+|(?:\'.*?\')|(?:(?:\?\?)?\s*(\?{1})))' . '/s'; // 0: full // 1: pre part // 2: keep part UNLESS '3' is set // 3: replace part ? $pos = 0; $query_new = preg_replace_callback( $pattern_replace, function ($matches) use (&$pos, &$params_lookup) { // only count pos up for actual replacements we will do if (!empty($matches[3])) { $pos++; $params_lookup[] = '$' . $pos; } // add the connectors back (1), and the data sets only if no replacement will be done return $matches[1] . ( empty($matches[3]) ? $matches[2] : '$' . $pos ); }, $query ); // for each ?:DTN: -> replace with $1 ... $n, any remaining :DTN: remove } elseif ($count_numbered && $convert_to == 'pdo') { // convert numbered to named $type = 'numbered'; $matches_return = $numbered_matches; // only check for $n $pattern_replace = '/' . '((?:\'.*?\')?\s*(?:\?\?|' . $query_split . ')\s*)' . '(\d+|(?:\'.*?\')|(\$[1-9]{1}(?:[0-9]{1,})?))' . '/s'; // 0: full // 1: pre part // 2: keep part UNLESS '3' is set // 3: replace part $numbered $pos = 0; $query_new = preg_replace_callback( $pattern_replace, function ($matches) use (&$pos, &$params_new, &$params_lookup, $params) { // only count up if $match[3] is not yet in lookup table if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) { $pos++; $params_lookup[$matches[3]] = ':' . $pos . '_named'; $params_new[] = $params[($pos - 1)] ?? throw new \RuntimeException( 'Cannot lookup ' . ($pos - 1) . ' in params list', 220 ); } // add the connectors back (1), and the data sets only if no replacement will be done return $matches[1] . ( empty($matches[3]) ? $matches[2] : $params_lookup[$matches[3]] ?? throw new \RuntimeException( 'Cannot lookup ' . $matches[3] . ' in params lookup list', 221 ) ); }, $query ); } // return, old query is always set return [ // original 'original' => [ 'query' => $query, 'params' => $params, ], // type found, empty if nothing was done 'type' => $type, // int: found, not found; -1: problem (set from false) 'found' => (int)$found, 'matches' => $matches_return, // old to new lookup check 'params_lookup' => $params_lookup, // new 'query' => $query_new ?? '', 'params' => $params_new, ]; } } // __END__