Driver.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Database;
  17. use Cake\Core\App;
  18. use Cake\Core\Exception\CakeException;
  19. use Cake\Core\Retry\CommandRetry;
  20. use Cake\Database\Exception\MissingConnectionException;
  21. use Cake\Database\Log\LoggedQuery;
  22. use Cake\Database\Log\QueryLogger;
  23. use Cake\Database\Retry\ErrorCodeWaitStrategy;
  24. use Cake\Database\Schema\SchemaDialect;
  25. use Cake\Database\Schema\TableSchema;
  26. use Cake\Database\Schema\TableSchemaInterface;
  27. use Cake\Database\Statement\Statement;
  28. use Closure;
  29. use InvalidArgumentException;
  30. use PDO;
  31. use PDOException;
  32. use Psr\Log\LoggerAwareTrait;
  33. use Psr\Log\LoggerInterface;
  34. use Stringable;
  35. /**
  36. * Represents a database driver containing all specificities for
  37. * a database engine including its SQL dialect.
  38. */
  39. abstract class Driver implements DriverInterface
  40. {
  41. use LoggerAwareTrait;
  42. /**
  43. * @var int|null Maximum alias length or null if no limit
  44. */
  45. protected const MAX_ALIAS_LENGTH = null;
  46. /**
  47. * @var array<int> DB-specific error codes that allow connect retry
  48. */
  49. protected const RETRY_ERROR_CODES = [];
  50. /**
  51. * @var class-string<\Cake\Database\Statement\Statement>
  52. */
  53. protected const STATEMENT_CLASS = Statement::class;
  54. /**
  55. * Instance of PDO.
  56. *
  57. * @var \PDO|null
  58. */
  59. protected ?PDO $pdo = null;
  60. /**
  61. * Configuration data.
  62. *
  63. * @var array<string, mixed>
  64. */
  65. protected array $_config = [];
  66. /**
  67. * Base configuration that is merged into the user
  68. * supplied configuration data.
  69. *
  70. * @var array<string, mixed>
  71. */
  72. protected array $_baseConfig = [];
  73. /**
  74. * Indicates whether the driver is doing automatic identifier quoting
  75. * for all queries
  76. *
  77. * @var bool
  78. */
  79. protected bool $_autoQuoting = false;
  80. /**
  81. * The server version
  82. *
  83. * @var string|null
  84. */
  85. protected ?string $_version = null;
  86. /**
  87. * The last number of connection retry attempts.
  88. *
  89. * @var int
  90. */
  91. protected int $connectRetries = 0;
  92. /**
  93. * The schema dialect for this driver
  94. *
  95. * @var \Cake\Database\Schema\SchemaDialect
  96. */
  97. protected SchemaDialect $_schemaDialect;
  98. /**
  99. * Constructor
  100. *
  101. * @param array<string, mixed> $config The configuration for the driver.
  102. * @throws \InvalidArgumentException
  103. */
  104. public function __construct(array $config = [])
  105. {
  106. if (empty($config['username']) && !empty($config['login'])) {
  107. throw new InvalidArgumentException(
  108. 'Please pass "username" instead of "login" for connecting to the database'
  109. );
  110. }
  111. $config += $this->_baseConfig + ['log' => false];
  112. $this->_config = $config;
  113. if (!empty($config['quoteIdentifiers'])) {
  114. $this->enableAutoQuoting();
  115. }
  116. if ($config['log'] !== false) {
  117. $this->logger = $this->createLogger($config['log'] === true ? null : $config['log']);
  118. }
  119. }
  120. /**
  121. * Establishes a connection to the database server
  122. *
  123. * @param string $dsn A Driver-specific PDO-DSN
  124. * @param array<string, mixed> $config configuration to be used for creating connection
  125. * @return \PDO
  126. */
  127. protected function createPdo(string $dsn, array $config): PDO
  128. {
  129. $action = function () use ($dsn, $config): PDO {
  130. return new PDO(
  131. $dsn,
  132. $config['username'] ?: null,
  133. $config['password'] ?: null,
  134. $config['flags']
  135. );
  136. };
  137. $retry = new CommandRetry(new ErrorCodeWaitStrategy(static::RETRY_ERROR_CODES, 5), 4);
  138. try {
  139. return $retry->run($action);
  140. } catch (PDOException $e) {
  141. throw new MissingConnectionException(
  142. [
  143. 'driver' => App::shortName(static::class, 'Database/Driver'),
  144. 'reason' => $e->getMessage(),
  145. ],
  146. null,
  147. $e
  148. );
  149. } finally {
  150. $this->connectRetries = $retry->getRetries();
  151. }
  152. }
  153. /**
  154. * @inheritDoc
  155. */
  156. abstract public function connect(): void;
  157. /**
  158. * @inheritDoc
  159. */
  160. public function disconnect(): void
  161. {
  162. $this->pdo = null;
  163. $this->_version = null;
  164. }
  165. /**
  166. * Returns connected server version.
  167. *
  168. * @return string
  169. */
  170. public function version(): string
  171. {
  172. if ($this->_version === null) {
  173. $this->_version = (string)$this->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
  174. }
  175. return $this->_version;
  176. }
  177. /**
  178. * Get the PDO connection instance.
  179. *
  180. * @return \PDO
  181. */
  182. protected function getPdo(): PDO
  183. {
  184. if ($this->pdo === null) {
  185. $this->connect();
  186. }
  187. /** @var \PDO */
  188. return $this->pdo;
  189. }
  190. /**
  191. * Execute the SQL query using the internal PDO instance.
  192. *
  193. * @param string $sql SQL query.
  194. * @return int|false
  195. */
  196. public function exec(string $sql): int|false
  197. {
  198. return $this->getPdo()->exec($sql);
  199. }
  200. /**
  201. * @inheritDoc
  202. */
  203. abstract public function enabled(): bool;
  204. /**
  205. * @inheritDoc
  206. */
  207. public function execute(string $sql, array $params = [], array $types = []): StatementInterface
  208. {
  209. $statement = $this->prepare($sql);
  210. if (!empty($params)) {
  211. $statement->bind($params, $types);
  212. }
  213. $this->executeStatement($statement);
  214. return $statement;
  215. }
  216. /**
  217. * @inheritDoc
  218. */
  219. public function run(Query $query): StatementInterface
  220. {
  221. $statement = $this->prepare($query);
  222. $query->getValueBinder()->attachTo($statement);
  223. $this->executeStatement($statement);
  224. return $statement;
  225. }
  226. /**
  227. * Execute the statement and log the query string.
  228. *
  229. * @param \Cake\Database\StatementInterface $statement Statement to execute.
  230. * @param array|null $params List of values to be bound to query.
  231. * @return void
  232. */
  233. protected function executeStatement(StatementInterface $statement, ?array $params = null): void
  234. {
  235. if ($this->logger === null) {
  236. $statement->execute($params);
  237. return;
  238. }
  239. $exception = null;
  240. $took = 0.0;
  241. try {
  242. $start = microtime(true);
  243. $statement->execute($params);
  244. $took = (float)number_format((microtime(true) - $start) * 1000, 1);
  245. } catch (PDOException $e) {
  246. $exception = $e;
  247. }
  248. $logContext = [
  249. 'driver' => $this,
  250. 'error' => $exception,
  251. 'params' => $params ?? $statement->getBoundParams(),
  252. ];
  253. if (!$exception) {
  254. $logContext['numRows'] = $statement->rowCount();
  255. $logContext['took'] = $took;
  256. }
  257. $this->log($statement->queryString(), $logContext);
  258. if ($exception) {
  259. throw $exception;
  260. }
  261. }
  262. /**
  263. * @inheritDoc
  264. */
  265. public function prepare(Query|string $query): StatementInterface
  266. {
  267. $statement = $this->getPdo()->prepare($query instanceof Query ? $query->sql() : $query);
  268. $typeMap = null;
  269. if ($query instanceof Query && $query->isResultsCastingEnabled() && $query->type() === Query::TYPE_SELECT) {
  270. $typeMap = $query->getSelectTypeMap();
  271. }
  272. /** @var \Cake\Database\StatementInterface */
  273. return new (static::STATEMENT_CLASS)($statement, $this, $typeMap);
  274. }
  275. /**
  276. * @inheritDoc
  277. */
  278. public function beginTransaction(): bool
  279. {
  280. if ($this->getPdo()->inTransaction()) {
  281. return true;
  282. }
  283. $this->log('BEGIN');
  284. return $this->getPdo()->beginTransaction();
  285. }
  286. /**
  287. * @inheritDoc
  288. */
  289. public function commitTransaction(): bool
  290. {
  291. if (!$this->getPdo()->inTransaction()) {
  292. return false;
  293. }
  294. $this->log('COMMIT');
  295. return $this->getPdo()->commit();
  296. }
  297. /**
  298. * @inheritDoc
  299. */
  300. public function rollbackTransaction(): bool
  301. {
  302. if (!$this->getPdo()->inTransaction()) {
  303. return false;
  304. }
  305. $this->log('ROLLBACK');
  306. return $this->getPdo()->rollBack();
  307. }
  308. /**
  309. * Returns whether a transaction is active for connection.
  310. *
  311. * @return bool
  312. */
  313. public function inTransaction(): bool
  314. {
  315. return $this->getPdo()->inTransaction();
  316. }
  317. /**
  318. * @inheritDoc
  319. */
  320. public function quote($value, $type = PDO::PARAM_STR): string
  321. {
  322. return $this->getPdo()->quote((string)$value, $type);
  323. }
  324. /**
  325. * @inheritDoc
  326. */
  327. abstract public function queryTranslator(string $type): Closure;
  328. /**
  329. * @inheritDoc
  330. */
  331. abstract public function schemaDialect(): SchemaDialect;
  332. /**
  333. * @inheritDoc
  334. */
  335. abstract public function quoteIdentifier(string $identifier): string;
  336. /**
  337. * @inheritDoc
  338. */
  339. public function schemaValue($value): string
  340. {
  341. if ($value === null) {
  342. return 'NULL';
  343. }
  344. if ($value === false) {
  345. return 'FALSE';
  346. }
  347. if ($value === true) {
  348. return 'TRUE';
  349. }
  350. if (is_float($value)) {
  351. return str_replace(',', '.', (string)$value);
  352. }
  353. /** @psalm-suppress InvalidArgument */
  354. if (
  355. (
  356. is_int($value) ||
  357. $value === '0'
  358. ) ||
  359. (
  360. is_numeric($value) &&
  361. !str_contains($value, ',') &&
  362. substr($value, 0, 1) !== '0' &&
  363. !str_contains($value, 'e')
  364. )
  365. ) {
  366. return (string)$value;
  367. }
  368. return $this->getPdo()->quote((string)$value, PDO::PARAM_STR);
  369. }
  370. /**
  371. * @inheritDoc
  372. */
  373. public function schema(): string
  374. {
  375. return $this->_config['schema'];
  376. }
  377. /**
  378. * @inheritDoc
  379. */
  380. public function lastInsertId(?string $table = null): string
  381. {
  382. return $this->getPdo()->lastInsertId($table);
  383. }
  384. /**
  385. * @inheritDoc
  386. */
  387. public function isConnected(): bool
  388. {
  389. if (isset($this->pdo)) {
  390. try {
  391. $connected = (bool)$this->pdo->query('SELECT 1');
  392. } catch (PDOException $e) {
  393. $connected = false;
  394. }
  395. } else {
  396. $connected = false;
  397. }
  398. return $connected;
  399. }
  400. /**
  401. * @inheritDoc
  402. */
  403. public function enableAutoQuoting(bool $enable = true)
  404. {
  405. $this->_autoQuoting = $enable;
  406. return $this;
  407. }
  408. /**
  409. * @inheritDoc
  410. */
  411. public function disableAutoQuoting()
  412. {
  413. $this->_autoQuoting = false;
  414. return $this;
  415. }
  416. /**
  417. * @inheritDoc
  418. */
  419. public function isAutoQuotingEnabled(): bool
  420. {
  421. return $this->_autoQuoting;
  422. }
  423. /**
  424. * Returns whether the driver supports the feature.
  425. *
  426. * Defaults to true for FEATURE_QUOTE and FEATURE_SAVEPOINT.
  427. *
  428. * @param string $feature Driver feature name
  429. * @return bool
  430. */
  431. public function supports(string $feature): bool
  432. {
  433. switch ($feature) {
  434. case static::FEATURE_DISABLE_CONSTRAINT_WITHOUT_TRANSACTION:
  435. case static::FEATURE_QUOTE:
  436. case static::FEATURE_SAVEPOINT:
  437. return true;
  438. }
  439. return false;
  440. }
  441. /**
  442. * @inheritDoc
  443. */
  444. public function compileQuery(Query $query, ValueBinder $binder): array
  445. {
  446. $processor = $this->newCompiler();
  447. $translator = $this->queryTranslator($query->type());
  448. $query = $translator($query);
  449. return [$query, $processor->compile($query, $binder)];
  450. }
  451. /**
  452. * @inheritDoc
  453. */
  454. public function newCompiler(): QueryCompiler
  455. {
  456. return new QueryCompiler();
  457. }
  458. /**
  459. * @inheritDoc
  460. */
  461. public function newTableSchema(string $table, array $columns = []): TableSchemaInterface
  462. {
  463. /** @var class-string<\Cake\Database\Schema\TableSchemaInterface> $className */
  464. $className = $this->_config['tableSchema'] ?? TableSchema::class;
  465. return new $className($table, $columns);
  466. }
  467. /**
  468. * Returns the maximum alias length allowed.
  469. * This can be different from the maximum identifier length for columns.
  470. *
  471. * @return int|null Maximum alias length or null if no limit
  472. */
  473. public function getMaxAliasLength(): ?int
  474. {
  475. return static::MAX_ALIAS_LENGTH;
  476. }
  477. /**
  478. * @inheritDoc
  479. */
  480. public function setLogger(LoggerInterface $logger): void
  481. {
  482. $this->logger = $logger;
  483. }
  484. /**
  485. * Create logger instance.
  486. *
  487. * @param string|null $className Logger's class name
  488. * @return \Psr\Log\LoggerInterface
  489. */
  490. protected function createLogger(?string $className): LoggerInterface
  491. {
  492. if ($className === null) {
  493. $className = QueryLogger::class;
  494. }
  495. /** @var class-string<\Psr\Log\LoggerInterface>|null $className */
  496. $className = App::className($className, 'Cake/Log', 'Log');
  497. if ($className === null) {
  498. throw new CakeException(
  499. 'For logging you must either set the `log` config to a FQCN which implemnts Psr\Log\LoggerInterface' .
  500. ' or require the cakephp/log package in your composer config.'
  501. );
  502. }
  503. return new $className();
  504. }
  505. /**
  506. * @inheritDoc
  507. */
  508. public function log(Stringable|string $message, array $context = []): bool
  509. {
  510. if ($this->logger === null) {
  511. return false;
  512. }
  513. $context['query'] = $message;
  514. $loggedQuery = new LoggedQuery();
  515. $loggedQuery->setContext($context);
  516. $this->logger->debug((string)$loggedQuery, ['query' => $loggedQuery]);
  517. return true;
  518. }
  519. /**
  520. * Destructor
  521. */
  522. public function __destruct()
  523. {
  524. $this->pdo = null;
  525. }
  526. /**
  527. * Returns an array that can be used to describe the internal state of this
  528. * object.
  529. *
  530. * @return array<string, mixed>
  531. */
  532. public function __debugInfo(): array
  533. {
  534. return [
  535. 'connected' => $this->pdo !== null,
  536. ];
  537. }
  538. }