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 */ 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 */ private function getNamedPlaceholderPositions(string $statement): array { return $this->collectPlaceholders( $statement, ':', '(?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 = "(?:'(?:\\\\)+'|'(?:[^'\\\\]|\\\\'?|'')*')" . '|' . '(?:"(?:\\\\)+"|"(?:[^"\\\\]|\\\\"?)*")' . '|' . '(?:`(?:\\\\)+`|`(?:[^`\\\\]|\\\\`?)*`)' . '|' . '(?