vendor/contao/core-bundle/src/Resources/contao/library/Contao/Database/Statement.php line 237

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of Contao.
  4. *
  5. * (c) Leo Feyer
  6. *
  7. * @license LGPL-3.0-or-later
  8. */
  9. namespace Contao\Database;
  10. use Contao\Database;
  11. use Doctrine\DBAL\Connection;
  12. use Doctrine\DBAL\Driver\Result as DoctrineResult;
  13. use Doctrine\DBAL\Exception\DriverException;
  14. use Doctrine\DBAL\ParameterType;
  15. /**
  16. * Create and execute queries
  17. *
  18. * The class creates the database queries replacing the wildcards and escaping
  19. * the values. It then executes the query and returns a result object.
  20. *
  21. * Usage:
  22. *
  23. * $db = Database::getInstance();
  24. * $stmt = $db->prepare("SELECT * FROM tl_member WHERE city=?");
  25. * $stmt->limit(10);
  26. * $res = $stmt->execute('London');
  27. *
  28. * @property string $query The query string
  29. * @property string $error The last error message
  30. * @property integer $affectedRows The number of affected rows
  31. * @property integer $insertId The last insert ID
  32. */
  33. class Statement
  34. {
  35. /**
  36. * Connection ID
  37. * @var Connection
  38. */
  39. protected $resConnection;
  40. /**
  41. * Database statement
  42. * @var DoctrineResult
  43. */
  44. protected $statement;
  45. /**
  46. * Query string
  47. * @var string
  48. */
  49. protected $strQuery;
  50. /**
  51. * @var array
  52. */
  53. private $arrSetParams = array();
  54. /**
  55. * @var array
  56. */
  57. private $arrLastUsedParams = array();
  58. /**
  59. * Validate the connection resource and store the query string
  60. *
  61. * @param Connection $resConnection The connection resource
  62. */
  63. public function __construct(Connection $resConnection)
  64. {
  65. $this->resConnection = $resConnection;
  66. }
  67. /**
  68. * Return an object property
  69. *
  70. * @param string $strKey The property name
  71. *
  72. * @return mixed|null The property value or null
  73. */
  74. public function __get($strKey)
  75. {
  76. switch ($strKey)
  77. {
  78. case 'query':
  79. return $this->strQuery;
  80. case 'affectedRows':
  81. return $this->statement->rowCount();
  82. case 'insertId':
  83. return $this->resConnection->lastInsertId();
  84. }
  85. return null;
  86. }
  87. /**
  88. * Prepare a query string so the following functions can handle it
  89. *
  90. * @param string $strQuery The query string
  91. *
  92. * @return Statement The statement object
  93. *
  94. * @throws \Exception If $strQuery is empty
  95. */
  96. public function prepare($strQuery)
  97. {
  98. if (!$strQuery)
  99. {
  100. throw new \Exception('Empty query string');
  101. }
  102. $this->strQuery = trim($strQuery);
  103. $this->arrLastUsedParams = array();
  104. return $this;
  105. }
  106. /**
  107. * Autogenerate the SET/VALUES subpart of a query from an associative array
  108. *
  109. * Usage:
  110. *
  111. * $set = array(
  112. * 'firstname' => 'Leo',
  113. * 'lastname' => 'Feyer'
  114. * );
  115. * $stmt->prepare("UPDATE tl_member %s")->set($set);
  116. *
  117. * @param array $arrParams The associative array
  118. *
  119. * @return Statement The statement object
  120. */
  121. public function set($arrParams)
  122. {
  123. if (substr_count($this->strQuery, '%s') !== 1 || !\in_array(strtoupper(substr($this->strQuery, 0, 6)), array('INSERT', 'UPDATE'), true))
  124. {
  125. trigger_deprecation('contao/core-bundle', '4.13', 'Using "%s()" is only supported for INSERT and UPDATE queries with the "%%s" placeholder. This will throw an exception in Contao 5.0.', __METHOD__);
  126. return $this;
  127. }
  128. $this->arrSetParams = array_values($arrParams);
  129. $arrParamNames = array_map(
  130. static function ($strName)
  131. {
  132. if (!preg_match('/^(?:[A-Za-z0-9_$]+|`[^`]+`)$/', $strName))
  133. {
  134. throw new \RuntimeException(sprintf('Invalid column name "%s" in %s()', $strName, __METHOD__));
  135. }
  136. return Database::quoteIdentifier($strName);
  137. },
  138. array_keys($arrParams)
  139. );
  140. // INSERT
  141. if (strncasecmp($this->strQuery, 'INSERT', 6) === 0)
  142. {
  143. $strQuery = sprintf(
  144. '(%s) VALUES (%s)',
  145. implode(', ', $arrParamNames),
  146. implode(', ', array_fill(0, \count($arrParams), '?'))
  147. );
  148. }
  149. // UPDATE
  150. else
  151. {
  152. if (!$arrParamNames)
  153. {
  154. throw new \InvalidArgumentException('Set array must not be empty for UPDATE queries');
  155. }
  156. $strQuery = 'SET ' . implode('=?, ', $arrParamNames) . '=?';
  157. }
  158. $this->strQuery = str_replace('%s', $strQuery, $this->strQuery);
  159. return $this;
  160. }
  161. /**
  162. * Handle limit and offset
  163. *
  164. * @param integer $intRows The maximum number of rows
  165. * @param integer $intOffset The number of rows to skip
  166. *
  167. * @return Statement The statement object
  168. */
  169. public function limit($intRows, $intOffset=0)
  170. {
  171. if ($intRows <= 0)
  172. {
  173. // 2^64-1 is the maximum of unsigned BIGINT, the biggest limit possible
  174. $intRows = '18446744073709551615';
  175. }
  176. if ($intOffset < 0)
  177. {
  178. $intOffset = 0;
  179. }
  180. if (strncasecmp($this->strQuery, 'SELECT', 6) === 0)
  181. {
  182. $this->strQuery .= ' LIMIT ' . $intOffset . ',' . $intRows;
  183. }
  184. else
  185. {
  186. $this->strQuery .= ' LIMIT ' . $intRows;
  187. }
  188. return $this;
  189. }
  190. /**
  191. * Execute the query and return the result object
  192. *
  193. * @return Result The result object
  194. */
  195. public function execute()
  196. {
  197. $arrParams = \func_get_args();
  198. if (\count($arrParams) === 1 && \is_array($arrParams[0]))
  199. {
  200. trigger_deprecation('contao/core-bundle', '4.13', 'Using "%s()" with an array parameter has been deprecated and will no longer work in Contao 5.0. Use argument unpacking via ... instead."', __METHOD__);
  201. $arrParams = array_values($arrParams[0]);
  202. }
  203. return $this->query('', array_merge($this->arrSetParams, $arrParams));
  204. }
  205. /**
  206. * Directly send a query string to the database
  207. *
  208. * @param string $strQuery The query string
  209. *
  210. * @return Result|Statement The result object or the statement object if there is no result set
  211. *
  212. * @throws \Exception If the query string is empty
  213. */
  214. public function query($strQuery='', array $arrParams = array(), array $arrTypes = array())
  215. {
  216. if (!empty($strQuery))
  217. {
  218. $this->strQuery = trim($strQuery);
  219. }
  220. // Make sure there is a query string
  221. if (!$this->strQuery)
  222. {
  223. throw new \Exception('Empty query string');
  224. }
  225. foreach ($arrParams as $key => $varParam)
  226. {
  227. // Automatically set type to boolean when no type is defined,
  228. // otherwise "false" will be converted to an empty string.
  229. if (null === ($arrTypes[$key] ?? null))
  230. {
  231. $arrTypes[$key] = \is_bool($varParam) ? ParameterType::BOOLEAN : ParameterType::STRING;
  232. }
  233. if (\is_string($varParam) || \is_bool($varParam) || \is_float($varParam) || \is_int($varParam) || $varParam === null)
  234. {
  235. continue;
  236. }
  237. $arrParams[$key] = serialize($varParam);
  238. }
  239. $this->arrLastUsedParams = $arrParams;
  240. // Execute the query
  241. // TODO: remove the try/catch block in Contao 5.0
  242. try
  243. {
  244. $this->statement = $this->resConnection->executeQuery($this->strQuery, $arrParams, $arrTypes);
  245. }
  246. catch (DriverException|\ArgumentCountError $exception)
  247. {
  248. // SQLSTATE[HY000]: This command is not supported in the prepared statement protocol
  249. if ($exception->getCode() === 1295)
  250. {
  251. $this->resConnection->executeStatement($this->strQuery, $arrParams, $arrTypes);
  252. trigger_deprecation('contao/core-bundle', '4.13', 'Using "%s()" for statements (instead of queries) has been deprecated and will no longer work in Contao 5.0. Use "%s::executeStatement()" instead.', __METHOD__, Connection::class);
  253. return $this;
  254. }
  255. if (!$arrParams)
  256. {
  257. throw $exception;
  258. }
  259. $intTokenCount = substr_count(preg_replace("/('[^']*')/", '', $this->strQuery), '?');
  260. if (\count($arrParams) <= $intTokenCount)
  261. {
  262. throw $exception;
  263. }
  264. // If we get here, there are more parameters than tokens, so we slice the array and try to execute the query again
  265. $this->statement = $this->resConnection->executeQuery($this->strQuery, \array_slice($arrParams, 0, $intTokenCount), $arrTypes);
  266. // Only trigger the deprecation if the parameter count was the reason for the exception and the previous call did not throw
  267. if ($this->arrLastUsedParams === array(null))
  268. {
  269. trigger_deprecation('contao/core-bundle', '4.13', 'Using "%s::execute(null)" has been deprecated and will no longer work in Contao 5.0. Omit the NULL parameters instead.', __CLASS__);
  270. }
  271. else
  272. {
  273. trigger_deprecation('contao/core-bundle', '4.13', 'Passing more parameters than "?" tokens has been deprecated and will no longer work in Contao 5.0. Use the correct number of parameters instead.');
  274. }
  275. }
  276. // No result set available
  277. if ($this->statement->columnCount() < 1)
  278. {
  279. return $this;
  280. }
  281. // Instantiate a result object
  282. return new Result($this->statement, $this->strQuery);
  283. }
  284. /**
  285. * Replace the wildcards in the query string
  286. *
  287. * @param array $arrValues The values array
  288. *
  289. * @throws \Exception If $arrValues has too few values to replace the wildcards in the query string
  290. *
  291. * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5.0.
  292. */
  293. protected function replaceWildcards($arrValues)
  294. {
  295. trigger_deprecation('contao/core-bundle', '4.13', 'Using "%s()" has been deprecated and will no longer work in Contao 5.0.', __METHOD__);
  296. $arrValues = $this->escapeParams($arrValues);
  297. $this->strQuery = preg_replace('/(?<!%)%([^bcdufosxX%])/', '%%$1', $this->strQuery);
  298. // Replace wildcards
  299. if (!$this->strQuery = @vsprintf($this->strQuery, $arrValues))
  300. {
  301. throw new \Exception('Too few arguments to build the query string');
  302. }
  303. }
  304. /**
  305. * Escape the values and serialize objects and arrays
  306. *
  307. * @param array $arrValues The values array
  308. *
  309. * @return array The array with the escaped values
  310. *
  311. * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5.0.
  312. */
  313. protected function escapeParams($arrValues)
  314. {
  315. trigger_deprecation('contao/core-bundle', '4.13', 'Using "%s()" has been deprecated and will no longer work in Contao 5.0.', __METHOD__);
  316. foreach ($arrValues as $k=>$v)
  317. {
  318. switch (\gettype($v))
  319. {
  320. case 'string':
  321. $arrValues[$k] = $this->resConnection->quote($v);
  322. break;
  323. case 'boolean':
  324. $arrValues[$k] = ($v === true) ? 1 : 0;
  325. break;
  326. case 'object':
  327. case 'array':
  328. $arrValues[$k] = $this->resConnection->quote(serialize($v));
  329. break;
  330. default:
  331. $arrValues[$k] = $v ?? 'NULL';
  332. break;
  333. }
  334. }
  335. return $arrValues;
  336. }
  337. /**
  338. * Explain the current query
  339. *
  340. * @return string The explanation string
  341. *
  342. * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5.0.
  343. */
  344. public function explain()
  345. {
  346. trigger_deprecation('contao/core-bundle', '4.13', 'Using "%s()" has been deprecated and will no longer work in Contao 5.0.', __METHOD__);
  347. return $this->resConnection->fetchAssociative('EXPLAIN ' . $this->strQuery, $this->arrLastUsedParams);
  348. }
  349. /**
  350. * Bypass the cache and always execute the query
  351. *
  352. * @return Result The result object
  353. *
  354. * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
  355. * Use Statement::execute() instead.
  356. */
  357. public function executeUncached()
  358. {
  359. trigger_deprecation('contao/core-bundle', '4.0', 'Using "Contao\Statement::executeUncached()" has been deprecated and will no longer work in Contao 5.0. Use "Contao\Statement::execute()" instead.');
  360. return \call_user_func_array(array($this, 'execute'), \func_get_args());
  361. }
  362. /**
  363. * Always execute the query and add or replace an existing cache entry
  364. *
  365. * @return Result The result object
  366. *
  367. * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
  368. * Use Statement::execute() instead.
  369. */
  370. public function executeCached()
  371. {
  372. trigger_deprecation('contao/core-bundle', '4.0', 'Using "Contao\Statement::executeCached()" has been deprecated and will no longer work in Contao 5.0. Use "Contao\Statement::execute()" instead.');
  373. return \call_user_func_array(array($this, 'execute'), \func_get_args());
  374. }
  375. }
  376. class_alias(Statement::class, 'Database\Statement');