PageRenderTime 51ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Doctrine/DBAL/SQLParserUtils.php

http://github.com/doctrine/dbal
PHP | 297 lines | 286 code | 3 blank | 8 comment | 0 complexity | 2e47f3a8da0a6526d18c1ebcee87223d MD5 | raw file
Possible License(s): Unlicense
  1. <?php
  2. namespace Doctrine\DBAL;
  3. use function array_fill;
  4. use function array_fill_keys;
  5. use function array_key_exists;
  6. use function array_keys;
  7. use function array_merge;
  8. use function array_slice;
  9. use function array_values;
  10. use function count;
  11. use function implode;
  12. use function is_int;
  13. use function key;
  14. use function ksort;
  15. use function preg_match_all;
  16. use function sprintf;
  17. use function strlen;
  18. use function strpos;
  19. use function substr;
  20. use const PREG_OFFSET_CAPTURE;
  21. /**
  22. * Utility class that parses sql statements with regard to types and parameters.
  23. */
  24. class SQLParserUtils
  25. {
  26. /**#@+
  27. *
  28. * @deprecated Will be removed as internal implementation details.
  29. */
  30. public const POSITIONAL_TOKEN = '\?';
  31. public const NAMED_TOKEN = '(?<!:):[a-zA-Z_][a-zA-Z0-9_]*';
  32. // Quote characters within string literals can be preceded by a backslash.
  33. public const ESCAPED_SINGLE_QUOTED_TEXT = "(?:'(?:\\\\)+'|'(?:[^'\\\\]|\\\\'?|'')*')";
  34. public const ESCAPED_DOUBLE_QUOTED_TEXT = '(?:"(?:\\\\)+"|"(?:[^"\\\\]|\\\\"?)*")';
  35. public const ESCAPED_BACKTICK_QUOTED_TEXT = '(?:`(?:\\\\)+`|`(?:[^`\\\\]|\\\\`?)*`)';
  36. /**#@-*/
  37. private const ESCAPED_BRACKET_QUOTED_TEXT = '(?<!\b(?i:ARRAY))\[(?:[^\]])*\]';
  38. /**
  39. * Gets an array of the placeholders in an sql statements as keys and their positions in the query string.
  40. *
  41. * For a statement with positional parameters, returns a zero-indexed list of placeholder position.
  42. * For a statement with named parameters, returns a map of placeholder positions to their parameter names.
  43. *
  44. * @deprecated Will be removed as internal implementation detail.
  45. *
  46. * @param string $statement
  47. * @param bool $isPositional
  48. *
  49. * @return int[]|string[]
  50. */
  51. public static function getPlaceholderPositions($statement, $isPositional = true)
  52. {
  53. return $isPositional
  54. ? self::getPositionalPlaceholderPositions($statement)
  55. : self::getNamedPlaceholderPositions($statement);
  56. }
  57. /**
  58. * Returns a zero-indexed list of placeholder position.
  59. *
  60. * @return int[]
  61. */
  62. private static function getPositionalPlaceholderPositions(string $statement) : array
  63. {
  64. return self::collectPlaceholders(
  65. $statement,
  66. '?',
  67. self::POSITIONAL_TOKEN,
  68. static function (string $_, int $placeholderPosition, int $fragmentPosition, array &$carry) : void {
  69. $carry[] = $placeholderPosition + $fragmentPosition;
  70. }
  71. );
  72. }
  73. /**
  74. * Returns a map of placeholder positions to their parameter names.
  75. *
  76. * @return string[]
  77. */
  78. private static function getNamedPlaceholderPositions(string $statement) : array
  79. {
  80. return self::collectPlaceholders(
  81. $statement,
  82. ':',
  83. self::NAMED_TOKEN,
  84. static function (string $placeholder, int $placeholderPosition, int $fragmentPosition, array &$carry) : void {
  85. $carry[$placeholderPosition + $fragmentPosition] = substr($placeholder, 1);
  86. }
  87. );
  88. }
  89. /**
  90. * @return mixed[]
  91. */
  92. private static function collectPlaceholders(string $statement, string $match, string $token, callable $collector) : array
  93. {
  94. if (strpos($statement, $match) === false) {
  95. return [];
  96. }
  97. $carry = [];
  98. foreach (self::getUnquotedStatementFragments($statement) as $fragment) {
  99. preg_match_all('/' . $token . '/', $fragment[0], $matches, PREG_OFFSET_CAPTURE);
  100. foreach ($matches[0] as $placeholder) {
  101. $collector($placeholder[0], $placeholder[1], $fragment[1], $carry);
  102. }
  103. }
  104. return $carry;
  105. }
  106. /**
  107. * For a positional query this method can rewrite the sql statement with regard to array parameters.
  108. *
  109. * @param string $query The SQL query to execute.
  110. * @param mixed[] $params The parameters to bind to the query.
  111. * @param int[]|string[] $types The types the previous parameters are in.
  112. *
  113. * @return mixed[]
  114. *
  115. * @throws SQLParserUtilsException
  116. */
  117. public static function expandListParameters($query, $params, $types)
  118. {
  119. $isPositional = is_int(key($params));
  120. $arrayPositions = [];
  121. $bindIndex = -1;
  122. if ($isPositional) {
  123. // make sure that $types has the same keys as $params
  124. // to allow omitting parameters with unspecified types
  125. $types += array_fill_keys(array_keys($params), null);
  126. ksort($params);
  127. ksort($types);
  128. }
  129. foreach ($types as $name => $type) {
  130. ++$bindIndex;
  131. if ($type !== Connection::PARAM_INT_ARRAY && $type !== Connection::PARAM_STR_ARRAY) {
  132. continue;
  133. }
  134. if ($isPositional) {
  135. $name = $bindIndex;
  136. }
  137. $arrayPositions[$name] = false;
  138. }
  139. if (( ! $arrayPositions && $isPositional)) {
  140. return [$query, $params, $types];
  141. }
  142. if ($isPositional) {
  143. $paramOffset = 0;
  144. $queryOffset = 0;
  145. $params = array_values($params);
  146. $types = array_values($types);
  147. $paramPos = self::getPositionalPlaceholderPositions($query);
  148. foreach ($paramPos as $needle => $needlePos) {
  149. if (! isset($arrayPositions[$needle])) {
  150. continue;
  151. }
  152. $needle += $paramOffset;
  153. $needlePos += $queryOffset;
  154. $count = count($params[$needle]);
  155. $params = array_merge(
  156. array_slice($params, 0, $needle),
  157. $params[$needle],
  158. array_slice($params, $needle + 1)
  159. );
  160. $types = array_merge(
  161. array_slice($types, 0, $needle),
  162. $count ?
  163. // array needles are at {@link \Doctrine\DBAL\ParameterType} constants
  164. // + {@link Doctrine\DBAL\Connection::ARRAY_PARAM_OFFSET}
  165. array_fill(0, $count, $types[$needle] - Connection::ARRAY_PARAM_OFFSET) :
  166. [],
  167. array_slice($types, $needle + 1)
  168. );
  169. $expandStr = $count ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
  170. $query = substr($query, 0, $needlePos) . $expandStr . substr($query, $needlePos + 1);
  171. $paramOffset += $count - 1; // Grows larger by number of parameters minus the replaced needle.
  172. $queryOffset += strlen($expandStr) - 1;
  173. }
  174. return [$query, $params, $types];
  175. }
  176. $queryOffset = 0;
  177. $typesOrd = [];
  178. $paramsOrd = [];
  179. $paramPos = self::getNamedPlaceholderPositions($query);
  180. foreach ($paramPos as $pos => $paramName) {
  181. $paramLen = strlen($paramName) + 1;
  182. $value = static::extractParam($paramName, $params, true);
  183. if (! isset($arrayPositions[$paramName]) && ! isset($arrayPositions[':' . $paramName])) {
  184. $pos += $queryOffset;
  185. $queryOffset -= $paramLen - 1;
  186. $paramsOrd[] = $value;
  187. $typesOrd[] = static::extractParam($paramName, $types, false, ParameterType::STRING);
  188. $query = substr($query, 0, $pos) . '?' . substr($query, $pos + $paramLen);
  189. continue;
  190. }
  191. $count = count($value);
  192. $expandStr = $count > 0 ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
  193. foreach ($value as $val) {
  194. $paramsOrd[] = $val;
  195. $typesOrd[] = static::extractParam($paramName, $types, false) - Connection::ARRAY_PARAM_OFFSET;
  196. }
  197. $pos += $queryOffset;
  198. $queryOffset += strlen($expandStr) - $paramLen;
  199. $query = substr($query, 0, $pos) . $expandStr . substr($query, $pos + $paramLen);
  200. }
  201. return [$query, $paramsOrd, $typesOrd];
  202. }
  203. /**
  204. * Slice the SQL statement around pairs of quotes and
  205. * return string fragments of SQL outside of quoted literals.
  206. * Each fragment is captured as a 2-element array:
  207. *
  208. * 0 => matched fragment string,
  209. * 1 => offset of fragment in $statement
  210. *
  211. * @param string $statement
  212. *
  213. * @return mixed[][]
  214. */
  215. private static function getUnquotedStatementFragments($statement)
  216. {
  217. $literal = self::ESCAPED_SINGLE_QUOTED_TEXT . '|' .
  218. self::ESCAPED_DOUBLE_QUOTED_TEXT . '|' .
  219. self::ESCAPED_BACKTICK_QUOTED_TEXT . '|' .
  220. self::ESCAPED_BRACKET_QUOTED_TEXT;
  221. $expression = sprintf('/((.+(?i:ARRAY)\\[.+\\])|([^\'"`\\[]+))(?:%s)?/s', $literal);
  222. preg_match_all($expression, $statement, $fragments, PREG_OFFSET_CAPTURE);
  223. return $fragments[1];
  224. }
  225. /**
  226. * @param string $paramName The name of the parameter (without a colon in front)
  227. * @param mixed $paramsOrTypes A hash of parameters or types
  228. * @param bool $isParam
  229. * @param mixed $defaultValue An optional default value. If omitted, an exception is thrown
  230. *
  231. * @return mixed
  232. *
  233. * @throws SQLParserUtilsException
  234. */
  235. private static function extractParam($paramName, $paramsOrTypes, $isParam, $defaultValue = null)
  236. {
  237. if (array_key_exists($paramName, $paramsOrTypes)) {
  238. return $paramsOrTypes[$paramName];
  239. }
  240. // Hash keys can be prefixed with a colon for compatibility
  241. if (array_key_exists(':' . $paramName, $paramsOrTypes)) {
  242. return $paramsOrTypes[':' . $paramName];
  243. }
  244. if ($defaultValue !== null) {
  245. return $defaultValue;
  246. }
  247. if ($isParam) {
  248. throw SQLParserUtilsException::missingParam($paramName);
  249. }
  250. throw SQLParserUtilsException::missingType($paramName);
  251. }
  252. }