313 lines
10 KiB
PHP
313 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is developed by evoWeb.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Evoweb\EwBase\Services;
|
|
|
|
use Doctrine\DBAL\ArrayParameterType;
|
|
use Doctrine\DBAL\ParameterType;
|
|
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
|
|
|
|
/**
|
|
* Utility class that parses sql statements with regard to types and parameters.
|
|
*/
|
|
class QueryBuilderHelper
|
|
{
|
|
public function getStatement(QueryBuilder $queryBuilder): string
|
|
{
|
|
[$sql, $parameters, $types] = $this->expandListParameters(
|
|
$queryBuilder->getSQL(),
|
|
$queryBuilder->getParameters(),
|
|
$queryBuilder->getParameterTypes(),
|
|
);
|
|
|
|
array_walk($parameters, function ($value, $key) use (&$sql, $types) {
|
|
if ($types[$key] == ParameterType::STRING) {
|
|
$value = '\'' . $value . '\'';
|
|
} elseif ($types[$key] == ParameterType::INTEGER) {
|
|
$value = '' . $value;
|
|
} elseif ($types[$key] == ArrayParameterType::INTEGER) {
|
|
$value = implode(', ', $value);
|
|
} elseif ($types[$key] == ArrayParameterType::STRING) {
|
|
$value = $value ? '"' . implode('", "', $value) . '"' : '""';
|
|
}
|
|
if (is_int($key)) {
|
|
$sql = substr_replace($sql, (string)$value, strpos($sql, '?'), 1);
|
|
} else {
|
|
$sql = str_replace(':' . $key, $value, $sql);
|
|
}
|
|
});
|
|
|
|
return $sql;
|
|
}
|
|
|
|
/**
|
|
* Gets an array of the placeholders in a sql statements as keys and their positions in the query string.
|
|
*
|
|
* For a statement with positional parameters, returns a zero-indexed list of placeholder position.
|
|
* For a statement with named parameters, returns a map of placeholder positions to their parameter names.
|
|
*/
|
|
public function getPlaceholderPositions(string $statement, bool $isPositional = true): array
|
|
{
|
|
return $isPositional
|
|
? $this->getPositionalPlaceholderPositions($statement)
|
|
: $this->getNamedPlaceholderPositions($statement);
|
|
}
|
|
|
|
/**
|
|
* Returns a zero-indexed list of placeholder position.
|
|
*
|
|
* @return list<int>
|
|
*/
|
|
private function getPositionalPlaceholderPositions(string $statement): array
|
|
{
|
|
return $this->collectPlaceholders(
|
|
$statement,
|
|
'?',
|
|
'\?',
|
|
function (string $_, int $placeholderPosition, int $fragmentPosition, array &$carry): void {
|
|
$carry[] = $placeholderPosition + $fragmentPosition;
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns a map of placeholder positions to their parameter names.
|
|
*
|
|
* @return array<int,string>
|
|
*/
|
|
private function getNamedPlaceholderPositions(string $statement): array
|
|
{
|
|
return $this->collectPlaceholders(
|
|
$statement,
|
|
':',
|
|
'(?<!:):[a-zA-Z_][a-zA-Z0-9_]*',
|
|
function (
|
|
string $placeholder,
|
|
int $placeholderPosition,
|
|
int $fragmentPosition,
|
|
array &$carry
|
|
): void {
|
|
$carry[$placeholderPosition + $fragmentPosition] = substr($placeholder, 1);
|
|
}
|
|
);
|
|
}
|
|
|
|
private function collectPlaceholders(
|
|
string $statement,
|
|
string $match,
|
|
string $token,
|
|
callable $collector
|
|
): array {
|
|
if (!str_contains($statement, $match)) {
|
|
return [];
|
|
}
|
|
|
|
$carry = [];
|
|
|
|
foreach ($this->getUnquotedStatementFragments($statement) as $fragment) {
|
|
preg_match_all('/' . $token . '/', $fragment[0], $matches, PREG_OFFSET_CAPTURE);
|
|
foreach ($matches[0] as $placeholder) {
|
|
$collector($placeholder[0], $placeholder[1], $fragment[1], $carry);
|
|
}
|
|
}
|
|
|
|
return $carry;
|
|
}
|
|
|
|
/**
|
|
* For a positional query this method can rewrite the sql statement with regard to array parameters.
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function expandListParameters(string $query, array $params, array $types): array
|
|
{
|
|
$isPositional = is_int(key($params));
|
|
$bindIndex = -1;
|
|
$arrayPositions = [];
|
|
|
|
if ($isPositional) {
|
|
// make sure that $types has the same keys as $params
|
|
// to allow omitting parameters with unspecified types
|
|
$types += array_fill_keys(array_keys($params), null);
|
|
|
|
ksort($params);
|
|
ksort($types);
|
|
}
|
|
|
|
foreach ($types as $name => $type) {
|
|
++$bindIndex;
|
|
|
|
if ($type !== ArrayParameterType::INTEGER && $type !== ArrayParameterType::STRING) {
|
|
continue;
|
|
}
|
|
|
|
if ($isPositional) {
|
|
$name = $bindIndex;
|
|
}
|
|
|
|
$arrayPositions[$name] = false;
|
|
}
|
|
|
|
// parameter are positional and no array parameter given
|
|
if ($isPositional && !$arrayPositions) {
|
|
return [$query, $params, $types];
|
|
}
|
|
|
|
return $isPositional
|
|
? $this->preparePositionalParameters($query, $params, $types, $arrayPositions)
|
|
: $this->convertNamedParamsToPositionalParams($query, $params, $types, $arrayPositions);
|
|
}
|
|
|
|
private function preparePositionalParameters(
|
|
string $query,
|
|
array $params,
|
|
array $types,
|
|
array $arrayPositions
|
|
): array {
|
|
$paramOffset = 0;
|
|
$queryOffset = 0;
|
|
$params = array_values($params);
|
|
$types = array_values($types);
|
|
|
|
$paramPos = $this->getPositionalPlaceholderPositions($query);
|
|
|
|
foreach ($paramPos as $needle => $needlePos) {
|
|
if (!isset($arrayPositions[$needle])) {
|
|
continue;
|
|
}
|
|
|
|
$needle += $paramOffset;
|
|
$needlePos += $queryOffset;
|
|
$count = count($params[$needle]);
|
|
|
|
$params = array_merge(
|
|
array_slice($params, 0, $needle),
|
|
$params[$needle],
|
|
array_slice($params, $needle + 1)
|
|
);
|
|
|
|
$types = array_merge(
|
|
array_slice($types, 0, $needle),
|
|
$count ?
|
|
// array needles are at {@link \Doctrine\DBAL\ArrayParameterType} constants
|
|
array_fill(0, $count, $types[$needle]) :
|
|
[],
|
|
array_slice($types, $needle + 1)
|
|
);
|
|
|
|
$expandStr = $count ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
|
|
$query = substr($query, 0, $needlePos) . $expandStr . substr($query, $needlePos + 1);
|
|
|
|
$paramOffset += $count - 1; // Grows larger by number of parameters minus the replaced needle.
|
|
$queryOffset += strlen($expandStr) - 1;
|
|
}
|
|
|
|
return [$query, $params, $types];
|
|
}
|
|
|
|
private function convertNamedParamsToPositionalParams(
|
|
string $query,
|
|
array $params,
|
|
array $types,
|
|
array $arrayPositions
|
|
): array {
|
|
$queryOffset = 0;
|
|
$typesOrd = [];
|
|
$paramsOrd = [];
|
|
|
|
$paramPos = $this->getNamedPlaceholderPositions($query);
|
|
|
|
foreach ($paramPos as $pos => $paramName) {
|
|
$paramLen = strlen($paramName) + 1;
|
|
$value = $this->extractParam($paramName, $params, true);
|
|
|
|
if (!isset($arrayPositions[$paramName]) && !isset($arrayPositions[':' . $paramName])) {
|
|
$pos += $queryOffset;
|
|
$queryOffset -= $paramLen - 1;
|
|
$paramsOrd[] = $value;
|
|
$typesOrd[] = $this->extractParam($paramName, $types, false, ParameterType::STRING);
|
|
$query = substr($query, 0, $pos) . '?' . substr($query, $pos + $paramLen);
|
|
|
|
continue;
|
|
}
|
|
|
|
$count = count($value);
|
|
$expandStr = $count > 0 ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
|
|
|
|
foreach ($value as $val) {
|
|
$paramsOrd[] = $val;
|
|
$type = $this->extractParam($paramName, $types, false);
|
|
$typesOrd[] = match ($type) {
|
|
ArrayParameterType::INTEGER => ParameterType::INTEGER,
|
|
ArrayParameterType::STRING => ParameterType::STRING,
|
|
default => $type
|
|
};
|
|
}
|
|
|
|
$pos += $queryOffset;
|
|
$queryOffset += strlen($expandStr) - $paramLen;
|
|
$query = substr($query, 0, $pos) . $expandStr . substr($query, $pos + $paramLen);
|
|
}
|
|
|
|
return [$query, $paramsOrd, $typesOrd];
|
|
}
|
|
|
|
/**
|
|
* Slice the SQL statement around pairs of quotes and
|
|
* return string fragments of outside SQL of quoted literals.
|
|
* Each fragment is captured as a 2-element array:
|
|
*
|
|
* 0 => matched fragment string,
|
|
* 1 => offset of fragment in $statement
|
|
*/
|
|
private function getUnquotedStatementFragments(string $statement): array
|
|
{
|
|
$literal = "(?:'(?:\\\\)+'|'(?:[^'\\\\]|\\\\'?|'')*')"
|
|
. '|' . '(?:"(?:\\\\)+"|"(?:[^"\\\\]|\\\\"?)*")'
|
|
. '|' . '(?:`(?:\\\\)+`|`(?:[^`\\\\]|\\\\`?)*`)'
|
|
. '|' . '(?<!\b(?i:ARRAY))\[(?:[^\]])*\]';
|
|
$expression = sprintf('/((.+(?i:ARRAY)\\[.+\\])|([^\'"`\\[]+))(?:%s)?/s', $literal);
|
|
|
|
preg_match_all($expression, $statement, $fragments, PREG_OFFSET_CAPTURE);
|
|
|
|
return $fragments[1];
|
|
}
|
|
|
|
private function extractParam(
|
|
string $paramName,
|
|
array $paramsOrTypes,
|
|
bool $isParam,
|
|
mixed $defaultValue = null
|
|
): mixed {
|
|
if (array_key_exists($paramName, $paramsOrTypes)) {
|
|
return $paramsOrTypes[$paramName];
|
|
}
|
|
if (array_key_exists(':' . $paramName, $paramsOrTypes)) {
|
|
// Hash keys can be prefixed with a colon for compatibility
|
|
return $paramsOrTypes[':' . $paramName];
|
|
}
|
|
|
|
if ($defaultValue !== null) {
|
|
return $defaultValue;
|
|
}
|
|
|
|
if ($isParam) {
|
|
throw new \Exception($paramName);
|
|
}
|
|
|
|
throw new \Exception($paramName);
|
|
}
|
|
}
|