Connection.php 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189
  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\Cache\Cache;
  18. use Cake\Core\App;
  19. use Cake\Core\Retry\CommandRetry;
  20. use Cake\Database\Exception\MissingConnectionException;
  21. use Cake\Database\Exception\MissingDriverException;
  22. use Cake\Database\Exception\MissingExtensionException;
  23. use Cake\Database\Exception\NestedTransactionRollbackException;
  24. use Cake\Database\Log\LoggedQuery;
  25. use Cake\Database\Log\LoggingStatement;
  26. use Cake\Database\Log\QueryLogger;
  27. use Cake\Database\Query\DeleteQuery;
  28. use Cake\Database\Query\InsertQuery;
  29. use Cake\Database\Query\SelectQuery;
  30. use Cake\Database\Query\UpdateQuery;
  31. use Cake\Database\Retry\ReconnectStrategy;
  32. use Cake\Database\Schema\CachedCollection;
  33. use Cake\Database\Schema\Collection as SchemaCollection;
  34. use Cake\Database\Schema\CollectionInterface as SchemaCollectionInterface;
  35. use Cake\Datasource\ConnectionInterface;
  36. use Cake\Log\Engine\BaseLog;
  37. use Cake\Log\Log;
  38. use Psr\Log\LoggerInterface;
  39. use Psr\SimpleCache\CacheInterface;
  40. use RuntimeException;
  41. use Throwable;
  42. use function Cake\Core\deprecationWarning;
  43. /**
  44. * Represents a connection with a database server.
  45. */
  46. class Connection implements ConnectionInterface
  47. {
  48. use TypeConverterTrait;
  49. /**
  50. * Contains the configuration params for this connection.
  51. *
  52. * @var array<string, mixed>
  53. */
  54. protected $_config;
  55. /**
  56. * @var \Cake\Database\DriverInterface
  57. */
  58. protected DriverInterface $readDriver;
  59. /**
  60. * @var \Cake\Database\DriverInterface
  61. */
  62. protected DriverInterface $writeDriver;
  63. /**
  64. * Contains how many nested transactions have been started.
  65. *
  66. * @var int
  67. */
  68. protected $_transactionLevel = 0;
  69. /**
  70. * Whether a transaction is active in this connection.
  71. *
  72. * @var bool
  73. */
  74. protected $_transactionStarted = false;
  75. /**
  76. * Whether this connection can and should use savepoints for nested
  77. * transactions.
  78. *
  79. * @var bool
  80. */
  81. protected $_useSavePoints = false;
  82. /**
  83. * Whether to log queries generated during this connection.
  84. *
  85. * @var bool
  86. */
  87. protected $_logQueries = false;
  88. /**
  89. * Logger object instance.
  90. *
  91. * @var \Psr\Log\LoggerInterface|null
  92. */
  93. protected $_logger;
  94. /**
  95. * Cacher object instance.
  96. *
  97. * @var \Psr\SimpleCache\CacheInterface|null
  98. */
  99. protected $cacher;
  100. /**
  101. * The schema collection object
  102. *
  103. * @var \Cake\Database\Schema\CollectionInterface|null
  104. */
  105. protected $_schemaCollection;
  106. /**
  107. * NestedTransactionRollbackException object instance, will be stored if
  108. * the rollback method is called in some nested transaction.
  109. *
  110. * @var \Cake\Database\Exception\NestedTransactionRollbackException|null
  111. */
  112. protected $nestedTransactionRollbackException;
  113. /**
  114. * Constructor.
  115. *
  116. * ### Available options:
  117. *
  118. * - `driver` Sort name or FCQN for driver.
  119. * - `log` Boolean indicating whether to use query logging.
  120. * - `name` Connection name.
  121. * - `cacheMetaData` Boolean indicating whether metadata (datasource schemas) should be cached.
  122. * If set to a string it will be used as the name of cache config to use.
  123. * - `cacheKeyPrefix` Custom prefix to use when generation cache keys. Defaults to connection name.
  124. *
  125. * @param array<string, mixed> $config Configuration array.
  126. */
  127. public function __construct(array $config)
  128. {
  129. $this->_config = $config;
  130. [self::ROLE_READ => $this->readDriver, self::ROLE_WRITE => $this->writeDriver] = $this->createDrivers($config);
  131. if (!empty($config['log'])) {
  132. $this->enableQueryLogging((bool)$config['log']);
  133. }
  134. }
  135. /**
  136. * Creates read and write drivers.
  137. *
  138. * @param array $config Connection config
  139. * @return array<string, \Cake\Database\DriverInterface>
  140. * @psalm-return array{read: \Cake\Database\DriverInterface, write: \Cake\Database\DriverInterface}
  141. */
  142. protected function createDrivers(array $config): array
  143. {
  144. $driver = $config['driver'] ?? '';
  145. if (!is_string($driver)) {
  146. /** @var \Cake\Database\DriverInterface $driver */
  147. if (!$driver->enabled()) {
  148. throw new MissingExtensionException(['driver' => get_class($driver), 'name' => $this->configName()]);
  149. }
  150. // Legacy support for setting instance instead of driver class
  151. return [self::ROLE_READ => $driver, self::ROLE_WRITE => $driver];
  152. }
  153. /** @var class-string<\Cake\Database\DriverInterface>|null $driverClass */
  154. $driverClass = App::className($driver, 'Database/Driver');
  155. if ($driverClass === null) {
  156. throw new MissingDriverException(['driver' => $driver, 'connection' => $this->configName()]);
  157. }
  158. $sharedConfig = array_diff_key($config, array_flip([
  159. 'name',
  160. 'driver',
  161. 'log',
  162. 'cacheMetaData',
  163. 'cacheKeyPrefix',
  164. ]));
  165. $writeConfig = $config['write'] ?? [] + $sharedConfig;
  166. $readConfig = $config['read'] ?? [] + $sharedConfig;
  167. if ($readConfig == $writeConfig) {
  168. $readDriver = $writeDriver = new $driverClass(['_role' => self::ROLE_WRITE] + $writeConfig);
  169. } else {
  170. $readDriver = new $driverClass(['_role' => self::ROLE_READ] + $readConfig);
  171. $writeDriver = new $driverClass(['_role' => self::ROLE_WRITE] + $writeConfig);
  172. }
  173. if (!$writeDriver->enabled()) {
  174. throw new MissingExtensionException(['driver' => get_class($writeDriver), 'name' => $this->configName()]);
  175. }
  176. return [self::ROLE_READ => $readDriver, self::ROLE_WRITE => $writeDriver];
  177. }
  178. /**
  179. * Destructor
  180. *
  181. * Disconnects the driver to release the connection.
  182. */
  183. public function __destruct()
  184. {
  185. if ($this->_transactionStarted && class_exists(Log::class)) {
  186. Log::warning('The connection is going to be closed but there is an active transaction.');
  187. }
  188. }
  189. /**
  190. * @inheritDoc
  191. */
  192. public function config(): array
  193. {
  194. return $this->_config;
  195. }
  196. /**
  197. * @inheritDoc
  198. */
  199. public function configName(): string
  200. {
  201. return $this->_config['name'] ?? '';
  202. }
  203. /**
  204. * Returns the connection role: read or write.
  205. *
  206. * @return string
  207. */
  208. public function role(): string
  209. {
  210. return preg_match('/:read$/', $this->configName()) === 1 ? static::ROLE_READ : static::ROLE_WRITE;
  211. }
  212. /**
  213. * Sets the driver instance. If a string is passed it will be treated
  214. * as a class name and will be instantiated.
  215. *
  216. * @param \Cake\Database\DriverInterface|string $driver The driver instance to use.
  217. * @param array<string, mixed> $config Config for a new driver.
  218. * @throws \Cake\Database\Exception\MissingDriverException When a driver class is missing.
  219. * @throws \Cake\Database\Exception\MissingExtensionException When a driver's PHP extension is missing.
  220. * @return $this
  221. * @deprecated 4.4.0 Setting the driver is deprecated. Use the connection config instead.
  222. */
  223. public function setDriver($driver, $config = [])
  224. {
  225. deprecationWarning('Setting the driver is deprecated. Use the connection config instead.');
  226. $driver = $this->createDriver($driver, $config);
  227. $this->readDriver = $this->writeDriver = $driver;
  228. return $this;
  229. }
  230. /**
  231. * Creates driver from name, class name or instance.
  232. *
  233. * @param \Cake\Database\DriverInterface|string $name Driver name, class name or instance.
  234. * @param array $config Driver config if $name is not an instance.
  235. * @return \Cake\Database\DriverInterface
  236. * @throws \Cake\Database\Exception\MissingDriverException When a driver class is missing.
  237. * @throws \Cake\Database\Exception\MissingExtensionException When a driver's PHP extension is missing.
  238. */
  239. protected function createDriver($name, array $config): DriverInterface
  240. {
  241. $driver = $name;
  242. if (is_string($driver)) {
  243. /** @psalm-var class-string<\Cake\Database\DriverInterface>|null $className */
  244. $className = App::className($driver, 'Database/Driver');
  245. if ($className === null) {
  246. throw new MissingDriverException(['driver' => $driver, 'connection' => $this->configName()]);
  247. }
  248. $driver = new $className(['_role' => self::ROLE_WRITE] + $config);
  249. }
  250. if (!$driver->enabled()) {
  251. throw new MissingExtensionException(['driver' => get_class($driver), 'name' => $this->configName()]);
  252. }
  253. return $driver;
  254. }
  255. /**
  256. * Get the retry wrapper object that is allows recovery from server disconnects
  257. * while performing certain database actions, such as executing a query.
  258. *
  259. * @return \Cake\Core\Retry\CommandRetry The retry wrapper
  260. */
  261. public function getDisconnectRetry(): CommandRetry
  262. {
  263. return new CommandRetry(new ReconnectStrategy($this));
  264. }
  265. /**
  266. * Gets the driver instance.
  267. *
  268. * @param string $role Connection role ('read' or 'write')
  269. * @return \Cake\Database\DriverInterface
  270. */
  271. public function getDriver(string $role = self::ROLE_WRITE): DriverInterface
  272. {
  273. assert($role === self::ROLE_READ || $role === self::ROLE_WRITE);
  274. return $role === self::ROLE_READ ? $this->readDriver : $this->writeDriver;
  275. }
  276. /**
  277. * Connects to the configured database.
  278. *
  279. * @throws \Cake\Database\Exception\MissingConnectionException If database connection could not be established.
  280. * @return bool true, if the connection was already established or the attempt was successful.
  281. */
  282. public function connect(): bool
  283. {
  284. deprecationWarning('If you cannot use automatic connection management, use $connection->getDriver()->connect() instead.');
  285. $connected = true;
  286. foreach ([self::ROLE_READ, self::ROLE_WRITE] as $role) {
  287. try {
  288. $connected = $connected && $this->getDriver($role)->connect();
  289. } catch (MissingConnectionException $e) {
  290. throw $e;
  291. } catch (Throwable $e) {
  292. throw new MissingConnectionException(
  293. [
  294. 'driver' => App::shortName(get_class($this->getDriver($role)), 'Database/Driver'),
  295. 'reason' => $e->getMessage(),
  296. ],
  297. null,
  298. $e
  299. );
  300. }
  301. }
  302. return $connected;
  303. }
  304. /**
  305. * Disconnects from database server.
  306. *
  307. * @return void
  308. * @deprecated 4.5.0 Use getDriver()->disconnect() instead.
  309. */
  310. public function disconnect(): void
  311. {
  312. deprecationWarning('If you cannot use automatic connection management, use $connection->getDriver()->disconnect() instead.');
  313. $this->getDriver(self::ROLE_READ)->disconnect();
  314. $this->getDriver(self::ROLE_WRITE)->disconnect();
  315. }
  316. /**
  317. * Returns whether connection to database server was already established.
  318. *
  319. * @return bool
  320. * @deprecated 4.5.0 Use getDriver()->isConnected() instead.
  321. */
  322. public function isConnected(): bool
  323. {
  324. deprecationWarning('Use $connection->getDriver()->isConnected() instead.');
  325. return $this->getDriver(self::ROLE_READ)->isConnected() && $this->getDriver(self::ROLE_WRITE)->isConnected();
  326. }
  327. /**
  328. * Prepares a SQL statement to be executed.
  329. *
  330. * @param \Cake\Database\Query|string $query The SQL to convert into a prepared statement.
  331. * @return \Cake\Database\StatementInterface
  332. * @deprecated 4.5.0 Use getDriver()->prepare() instead.
  333. */
  334. public function prepare($query): StatementInterface
  335. {
  336. $role = $query instanceof Query ? $query->getConnectionRole() : self::ROLE_WRITE;
  337. return $this->getDisconnectRetry()->run(function () use ($query, $role) {
  338. $statement = $this->getDriver($role)->prepare($query);
  339. if ($this->_logQueries) {
  340. $statement = $this->_newLogger($statement);
  341. }
  342. return $statement;
  343. });
  344. }
  345. /**
  346. * Executes a query using $params for interpolating values and $types as a hint for each
  347. * those params.
  348. *
  349. * @param string $sql SQL to be executed and interpolated with $params
  350. * @param array $params list or associative array of params to be interpolated in $sql as values
  351. * @param array $types list or associative array of types to be used for casting values in query
  352. * @return \Cake\Database\StatementInterface executed statement
  353. */
  354. public function execute(string $sql, array $params = [], array $types = []): StatementInterface
  355. {
  356. return $this->getDisconnectRetry()->run(function () use ($sql, $params, $types) {
  357. $statement = $this->prepare($sql);
  358. if (!empty($params)) {
  359. $statement->bind($params, $types);
  360. }
  361. $statement->execute();
  362. return $statement;
  363. });
  364. }
  365. /**
  366. * Compiles a Query object into a SQL string according to the dialect for this
  367. * connection's driver
  368. *
  369. * @param \Cake\Database\Query $query The query to be compiled
  370. * @param \Cake\Database\ValueBinder $binder Value binder
  371. * @return string
  372. */
  373. public function compileQuery(Query $query, ValueBinder $binder): string
  374. {
  375. deprecationWarning('Use getDriver()->compileQuery() instead.');
  376. return $this->getDriver($query->getConnectionRole())->compileQuery($query, $binder)[1];
  377. }
  378. /**
  379. * Executes the provided query after compiling it for the specific driver
  380. * dialect and returns the executed Statement object.
  381. *
  382. * @param \Cake\Database\Query $query The query to be executed
  383. * @return \Cake\Database\StatementInterface executed statement
  384. */
  385. public function run(Query $query): StatementInterface
  386. {
  387. return $this->getDisconnectRetry()->run(function () use ($query) {
  388. $statement = $this->prepare($query);
  389. $query->getValueBinder()->attachTo($statement);
  390. $statement->execute();
  391. return $statement;
  392. });
  393. }
  394. /**
  395. * Create a new SelectQuery instance for this connection.
  396. *
  397. * @param \Cake\Database\ExpressionInterface|callable|array|string $fields fields to be added to the list.
  398. * @param array|string $table The table or list of tables to query.
  399. * @param array<string, string> $types Associative array containing the types to be used for casting.
  400. * @return \Cake\Database\Query\SelectQuery
  401. */
  402. public function selectQuery(
  403. $fields = [],
  404. $table = [],
  405. array $types = []
  406. ): SelectQuery {
  407. $query = new SelectQuery($this);
  408. if ($table) {
  409. $query->from($table);
  410. }
  411. if ($fields) {
  412. $query->select($fields, false);
  413. }
  414. $query->setDefaultTypes($types);
  415. return $query;
  416. }
  417. /**
  418. * Executes a SQL statement and returns the Statement object as result.
  419. *
  420. * @param string $sql The SQL query to execute.
  421. * @return \Cake\Database\StatementInterface
  422. */
  423. public function query(string $sql): StatementInterface
  424. {
  425. deprecationWarning('Use either `selectQuery`, `insertQuery`, `deleteQuery`, `updateQuery` instead.');
  426. return $this->getDisconnectRetry()->run(function () use ($sql) {
  427. $statement = $this->prepare($sql);
  428. $statement->execute();
  429. return $statement;
  430. });
  431. }
  432. /**
  433. * Create a new Query instance for this connection.
  434. *
  435. * @return \Cake\Database\Query
  436. */
  437. public function newQuery(): Query
  438. {
  439. deprecationWarning(
  440. 'As of 4.5.0, using newQuery() is deprecated. Instead, use `insertQuery()`, ' .
  441. '`deleteQuery()`, `selectQuery()` or `updateQuery()`. The query objects ' .
  442. 'returned by these methods will emit deprecations that will become fatal errors in 5.0.' .
  443. 'See https://book.cakephp.org/4/en/appendices/4-5-migration-guide.html for more information.'
  444. );
  445. return new Query($this);
  446. }
  447. /**
  448. * Sets a Schema\Collection object for this connection.
  449. *
  450. * @param \Cake\Database\Schema\CollectionInterface $collection The schema collection object
  451. * @return $this
  452. */
  453. public function setSchemaCollection(SchemaCollectionInterface $collection)
  454. {
  455. $this->_schemaCollection = $collection;
  456. return $this;
  457. }
  458. /**
  459. * Gets a Schema\Collection object for this connection.
  460. *
  461. * @return \Cake\Database\Schema\CollectionInterface
  462. */
  463. public function getSchemaCollection(): SchemaCollectionInterface
  464. {
  465. if ($this->_schemaCollection !== null) {
  466. return $this->_schemaCollection;
  467. }
  468. if (!empty($this->_config['cacheMetadata'])) {
  469. return $this->_schemaCollection = new CachedCollection(
  470. new SchemaCollection($this),
  471. empty($this->_config['cacheKeyPrefix']) ? $this->configName() : $this->_config['cacheKeyPrefix'],
  472. $this->getCacher()
  473. );
  474. }
  475. return $this->_schemaCollection = new SchemaCollection($this);
  476. }
  477. /**
  478. * Executes an INSERT query on the specified table.
  479. *
  480. * @param string $table the table to insert values in
  481. * @param array $values values to be inserted
  482. * @param array<int|string, string> $types Array containing the types to be used for casting
  483. * @return \Cake\Database\StatementInterface
  484. */
  485. public function insert(string $table, array $values, array $types = []): StatementInterface
  486. {
  487. return $this->getDisconnectRetry()->run(function () use ($table, $values, $types) {
  488. return $this->insertQuery($table, $values, $types)->execute();
  489. });
  490. }
  491. /**
  492. * Create a new InsertQuery instance for this connection.
  493. *
  494. * @param string|null $table The table to insert rows into.
  495. * @param array $values Associative array of column => value to be inserted.
  496. * @param array<int|string, string> $types Associative array containing the types to be used for casting.
  497. * @return \Cake\Database\Query\InsertQuery
  498. */
  499. public function insertQuery(?string $table = null, array $values = [], array $types = []): InsertQuery
  500. {
  501. $query = new InsertQuery($this);
  502. if ($table) {
  503. $query->into($table);
  504. }
  505. if ($values) {
  506. $columns = array_keys($values);
  507. $query->insert($columns, $types)
  508. ->values($values);
  509. }
  510. return $query;
  511. }
  512. /**
  513. * Executes an UPDATE statement on the specified table.
  514. *
  515. * @param string $table the table to update rows from
  516. * @param array $values values to be updated
  517. * @param array $conditions conditions to be set for update statement
  518. * @param array<string> $types list of associative array containing the types to be used for casting
  519. * @return \Cake\Database\StatementInterface
  520. */
  521. public function update(string $table, array $values, array $conditions = [], array $types = []): StatementInterface
  522. {
  523. return $this->getDisconnectRetry()->run(function () use ($table, $values, $conditions, $types) {
  524. return $this->updateQuery($table, $values, $conditions, $types)->execute();
  525. });
  526. }
  527. /**
  528. * Create a new UpdateQuery instance for this connection.
  529. *
  530. * @param \Cake\Database\ExpressionInterface|string|null $table The table to update rows of.
  531. * @param array $values Values to be updated.
  532. * @param array $conditions Conditions to be set for the update statement.
  533. * @param array<string, string> $types Associative array containing the types to be used for casting.
  534. * @return \Cake\Database\Query\UpdateQuery
  535. */
  536. public function updateQuery(
  537. $table = null,
  538. array $values = [],
  539. array $conditions = [],
  540. array $types = []
  541. ): UpdateQuery {
  542. $query = new UpdateQuery($this);
  543. if ($table) {
  544. $query->update($table);
  545. }
  546. if ($values) {
  547. $query->set($values, $types);
  548. }
  549. if ($conditions) {
  550. $query->where($conditions, $types);
  551. }
  552. return $query;
  553. }
  554. /**
  555. * Executes a DELETE statement on the specified table.
  556. *
  557. * @param string $table the table to delete rows from
  558. * @param array $conditions conditions to be set for delete statement
  559. * @param array<string> $types list of associative array containing the types to be used for casting
  560. * @return \Cake\Database\StatementInterface
  561. */
  562. public function delete(string $table, array $conditions = [], array $types = []): StatementInterface
  563. {
  564. return $this->getDisconnectRetry()->run(function () use ($table, $conditions, $types) {
  565. return $this->deleteQuery($table, $conditions, $types)->execute();
  566. });
  567. }
  568. /**
  569. * Create a new DeleteQuery instance for this connection.
  570. *
  571. * @param string|null $table The table to delete rows from.
  572. * @param array $conditions Conditions to be set for the delete statement.
  573. * @param array<string, string> $types Associative array containing the types to be used for casting.
  574. * @return \Cake\Database\Query\DeleteQuery
  575. */
  576. public function deleteQuery(?string $table = null, array $conditions = [], array $types = []): DeleteQuery
  577. {
  578. $query = new DeleteQuery($this);
  579. if ($table) {
  580. $query->from($table);
  581. }
  582. if ($conditions) {
  583. $query->where($conditions, $types);
  584. }
  585. return $query;
  586. }
  587. /**
  588. * Starts a new transaction.
  589. *
  590. * @return void
  591. */
  592. public function begin(): void
  593. {
  594. if (!$this->_transactionStarted) {
  595. if ($this->_logQueries) {
  596. $this->log('BEGIN');
  597. }
  598. $this->getDisconnectRetry()->run(function (): void {
  599. $this->getDriver()->beginTransaction();
  600. });
  601. $this->_transactionLevel = 0;
  602. $this->_transactionStarted = true;
  603. $this->nestedTransactionRollbackException = null;
  604. return;
  605. }
  606. $this->_transactionLevel++;
  607. if ($this->isSavePointsEnabled()) {
  608. $this->createSavePoint((string)$this->_transactionLevel);
  609. }
  610. }
  611. /**
  612. * Commits current transaction.
  613. *
  614. * @return bool true on success, false otherwise
  615. */
  616. public function commit(): bool
  617. {
  618. if (!$this->_transactionStarted) {
  619. return false;
  620. }
  621. if ($this->_transactionLevel === 0) {
  622. if ($this->wasNestedTransactionRolledback()) {
  623. /** @var \Cake\Database\Exception\NestedTransactionRollbackException $e */
  624. $e = $this->nestedTransactionRollbackException;
  625. $this->nestedTransactionRollbackException = null;
  626. throw $e;
  627. }
  628. $this->_transactionStarted = false;
  629. $this->nestedTransactionRollbackException = null;
  630. if ($this->_logQueries) {
  631. $this->log('COMMIT');
  632. }
  633. return $this->getDriver()->commitTransaction();
  634. }
  635. if ($this->isSavePointsEnabled()) {
  636. $this->releaseSavePoint((string)$this->_transactionLevel);
  637. }
  638. $this->_transactionLevel--;
  639. return true;
  640. }
  641. /**
  642. * Rollback current transaction.
  643. *
  644. * @param bool|null $toBeginning Whether the transaction should be rolled back to the
  645. * beginning of it. Defaults to false if using savepoints, or true if not.
  646. * @return bool
  647. */
  648. public function rollback(?bool $toBeginning = null): bool
  649. {
  650. if (!$this->_transactionStarted) {
  651. return false;
  652. }
  653. $useSavePoint = $this->isSavePointsEnabled();
  654. if ($toBeginning === null) {
  655. $toBeginning = !$useSavePoint;
  656. }
  657. if ($this->_transactionLevel === 0 || $toBeginning) {
  658. $this->_transactionLevel = 0;
  659. $this->_transactionStarted = false;
  660. $this->nestedTransactionRollbackException = null;
  661. if ($this->_logQueries) {
  662. $this->log('ROLLBACK');
  663. }
  664. $this->getDriver()->rollbackTransaction();
  665. return true;
  666. }
  667. $savePoint = $this->_transactionLevel--;
  668. if ($useSavePoint) {
  669. $this->rollbackSavepoint($savePoint);
  670. } elseif ($this->nestedTransactionRollbackException === null) {
  671. $this->nestedTransactionRollbackException = new NestedTransactionRollbackException();
  672. }
  673. return true;
  674. }
  675. /**
  676. * Enables/disables the usage of savepoints, enables only if driver the allows it.
  677. *
  678. * If you are trying to enable this feature, make sure you check
  679. * `isSavePointsEnabled()` to verify that savepoints were enabled successfully.
  680. *
  681. * @param bool $enable Whether save points should be used.
  682. * @return $this
  683. */
  684. public function enableSavePoints(bool $enable = true)
  685. {
  686. if ($enable === false) {
  687. $this->_useSavePoints = false;
  688. } else {
  689. $this->_useSavePoints = $this->getDriver()->supports(DriverInterface::FEATURE_SAVEPOINT);
  690. }
  691. return $this;
  692. }
  693. /**
  694. * Disables the usage of savepoints.
  695. *
  696. * @return $this
  697. */
  698. public function disableSavePoints()
  699. {
  700. $this->_useSavePoints = false;
  701. return $this;
  702. }
  703. /**
  704. * Returns whether this connection is using savepoints for nested transactions
  705. *
  706. * @return bool true if enabled, false otherwise
  707. */
  708. public function isSavePointsEnabled(): bool
  709. {
  710. return $this->_useSavePoints;
  711. }
  712. /**
  713. * Creates a new save point for nested transactions.
  714. *
  715. * @param string|int $name Save point name or id
  716. * @return void
  717. */
  718. public function createSavePoint($name): void
  719. {
  720. $this->execute($this->getDriver()->savePointSQL($name))->closeCursor();
  721. }
  722. /**
  723. * Releases a save point by its name.
  724. *
  725. * @param string|int $name Save point name or id
  726. * @return void
  727. */
  728. public function releaseSavePoint($name): void
  729. {
  730. $sql = $this->getDriver()->releaseSavePointSQL($name);
  731. if ($sql) {
  732. $this->execute($sql)->closeCursor();
  733. }
  734. }
  735. /**
  736. * Rollback a save point by its name.
  737. *
  738. * @param string|int $name Save point name or id
  739. * @return void
  740. */
  741. public function rollbackSavepoint($name): void
  742. {
  743. $this->execute($this->getDriver()->rollbackSavePointSQL($name))->closeCursor();
  744. }
  745. /**
  746. * Run driver specific SQL to disable foreign key checks.
  747. *
  748. * @return void
  749. */
  750. public function disableForeignKeys(): void
  751. {
  752. $this->getDisconnectRetry()->run(function (): void {
  753. $this->execute($this->getDriver()->disableForeignKeySQL())->closeCursor();
  754. });
  755. }
  756. /**
  757. * Run driver specific SQL to enable foreign key checks.
  758. *
  759. * @return void
  760. */
  761. public function enableForeignKeys(): void
  762. {
  763. $this->getDisconnectRetry()->run(function (): void {
  764. $this->execute($this->getDriver()->enableForeignKeySQL())->closeCursor();
  765. });
  766. }
  767. /**
  768. * Returns whether the driver supports adding or dropping constraints
  769. * to already created tables.
  770. *
  771. * @return bool true if driver supports dynamic constraints
  772. * @deprecated 4.3.0 Fixtures no longer dynamically drop and create constraints.
  773. */
  774. public function supportsDynamicConstraints(): bool
  775. {
  776. return $this->getDriver()->supportsDynamicConstraints();
  777. }
  778. /**
  779. * @inheritDoc
  780. */
  781. public function transactional(callable $callback)
  782. {
  783. $this->begin();
  784. try {
  785. $result = $callback($this);
  786. } catch (Throwable $e) {
  787. $this->rollback(false);
  788. throw $e;
  789. }
  790. if ($result === false) {
  791. $this->rollback(false);
  792. return false;
  793. }
  794. try {
  795. $this->commit();
  796. } catch (NestedTransactionRollbackException $e) {
  797. $this->rollback(false);
  798. throw $e;
  799. }
  800. return $result;
  801. }
  802. /**
  803. * Returns whether some nested transaction has been already rolled back.
  804. *
  805. * @return bool
  806. */
  807. protected function wasNestedTransactionRolledback(): bool
  808. {
  809. return $this->nestedTransactionRollbackException instanceof NestedTransactionRollbackException;
  810. }
  811. /**
  812. * @inheritDoc
  813. */
  814. public function disableConstraints(callable $callback)
  815. {
  816. return $this->getDisconnectRetry()->run(function () use ($callback) {
  817. $this->disableForeignKeys();
  818. try {
  819. $result = $callback($this);
  820. } finally {
  821. $this->enableForeignKeys();
  822. }
  823. return $result;
  824. });
  825. }
  826. /**
  827. * Checks if a transaction is running.
  828. *
  829. * @return bool True if a transaction is running else false.
  830. */
  831. public function inTransaction(): bool
  832. {
  833. return $this->_transactionStarted;
  834. }
  835. /**
  836. * Quotes value to be used safely in database query.
  837. *
  838. * This uses `PDO::quote()` and requires `supportsQuoting()` to work.
  839. *
  840. * @param mixed $value The value to quote.
  841. * @param \Cake\Database\TypeInterface|string|int $type Type to be used for determining kind of quoting to perform
  842. * @return string Quoted value
  843. * @deprecated 4.5.0 Use getDriver()->quote() instead.
  844. */
  845. public function quote($value, $type = 'string'): string
  846. {
  847. deprecationWarning('Use getDriver()->quote() instead.');
  848. [$value, $type] = $this->cast($value, $type);
  849. return $this->getDriver()->quote($value, $type);
  850. }
  851. /**
  852. * Checks if using `quote()` is supported.
  853. *
  854. * This is not required to use `quoteIdentifier()`.
  855. *
  856. * @return bool
  857. * @deprecated 4.5.0 Use getDriver()->supportsQuoting() instead.
  858. */
  859. public function supportsQuoting(): bool
  860. {
  861. deprecationWarning('Use getDriver()->supportsQuoting() instead.');
  862. return $this->getDriver()->supports(DriverInterface::FEATURE_QUOTE);
  863. }
  864. /**
  865. * Quotes a database identifier (a column name, table name, etc..) to
  866. * be used safely in queries without the risk of using reserved words.
  867. *
  868. * This does not require `supportsQuoting()` to work.
  869. *
  870. * @param string $identifier The identifier to quote.
  871. * @return string
  872. * @deprecated 4.5.0 Use getDriver()->quoteIdentifier() instead.
  873. */
  874. public function quoteIdentifier(string $identifier): string
  875. {
  876. deprecationWarning('Use getDriver()->quoteIdentifier() instead.');
  877. return $this->getDriver()->quoteIdentifier($identifier);
  878. }
  879. /**
  880. * Enables or disables metadata caching for this connection
  881. *
  882. * Changing this setting will not modify existing schema collections objects.
  883. *
  884. * @param string|bool $cache Either boolean false to disable metadata caching, or
  885. * true to use `_cake_model_` or the name of the cache config to use.
  886. * @return void
  887. */
  888. public function cacheMetadata($cache): void
  889. {
  890. $this->_schemaCollection = null;
  891. $this->_config['cacheMetadata'] = $cache;
  892. if (is_string($cache)) {
  893. $this->cacher = null;
  894. }
  895. }
  896. /**
  897. * @inheritDoc
  898. */
  899. public function setCacher(CacheInterface $cacher)
  900. {
  901. $this->cacher = $cacher;
  902. return $this;
  903. }
  904. /**
  905. * @inheritDoc
  906. */
  907. public function getCacher(): CacheInterface
  908. {
  909. if ($this->cacher !== null) {
  910. return $this->cacher;
  911. }
  912. $configName = $this->_config['cacheMetadata'] ?? '_cake_model_';
  913. if (!is_string($configName)) {
  914. $configName = '_cake_model_';
  915. }
  916. if (!class_exists(Cache::class)) {
  917. throw new RuntimeException(
  918. 'To use caching you must either set a cacher using Connection::setCacher()' .
  919. ' or require the cakephp/cache package in your composer config.'
  920. );
  921. }
  922. return $this->cacher = Cache::pool($configName);
  923. }
  924. /**
  925. * Enable/disable query logging
  926. *
  927. * @param bool $enable Enable/disable query logging
  928. * @return $this
  929. * @deprecated 4.5.0 Connection logging is moving to the driver in 5.x
  930. */
  931. public function enableQueryLogging(bool $enable = true)
  932. {
  933. $this->_logQueries = $enable;
  934. return $this;
  935. }
  936. /**
  937. * Disable query logging
  938. *
  939. * @return $this
  940. * @deprecated 4.5.0 Connection logging is moving to the driver in 5.x
  941. */
  942. public function disableQueryLogging()
  943. {
  944. $this->_logQueries = false;
  945. return $this;
  946. }
  947. /**
  948. * Check if query logging is enabled.
  949. *
  950. * @return bool
  951. * @deprecated 4.5.0 Connection logging is moving to the driver in 5.x
  952. */
  953. public function isQueryLoggingEnabled(): bool
  954. {
  955. return $this->_logQueries;
  956. }
  957. /**
  958. * Sets a logger
  959. *
  960. * @param \Psr\Log\LoggerInterface $logger Logger object
  961. * @return $this
  962. * @psalm-suppress ImplementedReturnTypeMismatch
  963. */
  964. public function setLogger(LoggerInterface $logger)
  965. {
  966. $this->_logger = $logger;
  967. return $this;
  968. }
  969. /**
  970. * Gets the logger object
  971. *
  972. * @return \Psr\Log\LoggerInterface logger instance
  973. */
  974. public function getLogger(): LoggerInterface
  975. {
  976. if ($this->_logger !== null) {
  977. return $this->_logger;
  978. }
  979. if (!class_exists(BaseLog::class)) {
  980. throw new RuntimeException(
  981. 'For logging you must either set a logger using Connection::setLogger()' .
  982. ' or require the cakephp/log package in your composer config.'
  983. );
  984. }
  985. return $this->_logger = new QueryLogger(['connection' => $this->configName()]);
  986. }
  987. /**
  988. * Logs a Query string using the configured logger object.
  989. *
  990. * @param string $sql string to be logged
  991. * @return void
  992. */
  993. public function log(string $sql): void
  994. {
  995. $query = new LoggedQuery();
  996. $query->query = $sql;
  997. $this->getLogger()->debug((string)$query, ['query' => $query]);
  998. }
  999. /**
  1000. * Returns a new statement object that will log the activity
  1001. * for the passed original statement instance.
  1002. *
  1003. * @param \Cake\Database\StatementInterface $statement the instance to be decorated
  1004. * @return \Cake\Database\Log\LoggingStatement
  1005. */
  1006. protected function _newLogger(StatementInterface $statement): LoggingStatement
  1007. {
  1008. $log = new LoggingStatement($statement, $this->getDriver());
  1009. $log->setLogger($this->getLogger());
  1010. return $log;
  1011. }
  1012. /**
  1013. * Returns an array that can be used to describe the internal state of this
  1014. * object.
  1015. *
  1016. * @return array<string, mixed>
  1017. */
  1018. public function __debugInfo(): array
  1019. {
  1020. $secrets = [
  1021. 'password' => '*****',
  1022. 'username' => '*****',
  1023. 'host' => '*****',
  1024. 'database' => '*****',
  1025. 'port' => '*****',
  1026. ];
  1027. $replace = array_intersect_key($secrets, $this->_config);
  1028. $config = $replace + $this->_config;
  1029. if (isset($config['read'])) {
  1030. /** @psalm-suppress PossiblyInvalidArgument */
  1031. $config['read'] = array_intersect_key($secrets, $config['read']) + $config['read'];
  1032. }
  1033. if (isset($config['write'])) {
  1034. /** @psalm-suppress PossiblyInvalidArgument */
  1035. $config['write'] = array_intersect_key($secrets, $config['write']) + $config['write'];
  1036. }
  1037. return [
  1038. 'config' => $config,
  1039. 'readDriver' => $this->readDriver,
  1040. 'writeDriver' => $this->writeDriver,
  1041. 'transactionLevel' => $this->_transactionLevel,
  1042. 'transactionStarted' => $this->_transactionStarted,
  1043. 'useSavePoints' => $this->_useSavePoints,
  1044. 'logQueries' => $this->_logQueries,
  1045. 'logger' => $this->_logger,
  1046. ];
  1047. }
  1048. }