DbConnection.php 7.44 KB
Newer Older
Scott committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
<?php
/*
	Question2Answer by Gideon Greenspan and contributors
	http://www.question2answer.org/

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	More about this license: http://www.question2answer.org/license.php
*/

namespace Q2A\Database;

21 22
use PDO;
use PDOException;
pupi1985 committed
23
use PDOStatement;
Scott committed
24
use Q2A\Database\Exceptions\SelectSpecException;
25

26 27 28
/**
 * Core database class. Handles all SQL queries within Q2A.
 */
Scott committed
29 30
class DbConnection
{
pupi1985 committed
31
	/** @var PDO */
Scott committed
32
	protected $pdo;
pupi1985 committed
33 34

	/** @var array */
Scott committed
35
	protected $config;
pupi1985 committed
36 37

	/** @var bool */
Scott committed
38
	protected $allowConnect = false;
pupi1985 committed
39 40

	/** @var string */
Scott committed
41
	protected $failHandler;
pupi1985 committed
42 43

	/** @var int */
44
	protected $updateCountsSuspended = 0;
Scott committed
45 46 47 48 49 50 51 52 53 54 55 56 57

	public function __construct()
	{
		$this->config = array(
			'driver' => 'mysql',
			'host' => QA_FINAL_MYSQL_HOSTNAME,
			'username' => QA_FINAL_MYSQL_USERNAME,
			'password' => QA_FINAL_MYSQL_PASSWORD,
			'database' => QA_FINAL_MYSQL_DATABASE,
		);

		if (defined('QA_FINAL_WORDPRESS_INTEGRATE_PATH')) {
			// Wordpress allows setting port inside DB_HOST constant, like 127.0.0.1:3306
58
			$hostAndPort = explode(':', $this->config['host']);
Scott committed
59 60 61 62 63 64 65 66 67
			if (count($hostAndPort) >= 2) {
				$this->config['host'] = $hostAndPort[0];
				$this->config['port'] = $hostAndPort[1];
			}
		} elseif (defined('QA_FINAL_MYSQL_PORT')) {
			$this->config['port'] = QA_FINAL_MYSQL_PORT;
		}
	}

68 69 70 71 72 73 74 75 76
	/**
	 * Obtain the raw PDO object.
	 * @return PDO
	 */
	public function getPDO()
	{
		return $this->pdo;
	}

77 78 79
	/**
	 * Indicates to the Q2A database layer that database connections are permitted from this point forwards (before
	 * this point, some plugins may not have had a chance to override some database access functions).
80
	 * @return void
81
	 */
Scott committed
82 83 84 85 86
	public function allowConnect()
	{
		$this->allowConnect = true;
	}

87 88 89 90 91 92 93 94 95
	/**
	 * Indicates whether Q2A is connected to the database.
	 * @return boolean
	 */
	public function isConnected()
	{
		return $this->pdo !== null;
	}

96
	/**
97 98
	 * Connect to the Q2A database, optionally install the $failHandler (and call it if necessary).
	 * Uses PDO as of Q2A 1.9.
pupi1985 committed
99
	 * @param string $failHandler
100 101
	 * @return mixed|void
	 */
Scott committed
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
	public function connect($failHandler = null)
	{
		if (!$this->allowConnect) {
			qa_fatal_error('It appears that a plugin is trying to access the database, but this is not allowed until Q2A initialization is complete.');
			return;
		}
		if ($failHandler !== null) {
			// set this even if connection already opened
			$this->failHandler = $failHandler;
		}
		if ($this->pdo) {
			return;
		}

		$dsn = sprintf('%s:host=%s;dbname=%s;charset=utf8', $this->config['driver'], $this->config['host'], $this->config['database']);
		if (isset($this->config['port'])) {
			$dsn .= ';port=' . $this->config['port'];
		}

Scott committed
121 122 123 124
		$options = array(
			PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
			PDO::ATTR_EMULATE_PREPARES => true, // required for queries like LOCK TABLES (also, slightly faster)
		);
Scott committed
125
		if (QA_PERSISTENT_CONN_DB) {
126
			$options[PDO::ATTR_PERSISTENT] = true;
Scott committed
127 128 129
		}

		try {
130 131
			$this->pdo = new PDO($dsn, $this->config['username'], $this->config['password'], $options);
		} catch (PDOException $ex) {
Scott committed
132 133 134 135 136 137
			$this->failError('connect', $ex->getCode(), $ex->getMessage());
		}

		qa_report_process_stage('db_connected');
	}

138
	/**
139
	 * Disconnect from the Q2A database. This is not strictly required, but we do it in case there is a long Q2A shutdown process.
140 141 142 143 144 145 146
	 * @return void
	 */
	public function disconnect()
	{
		$this->pdo = null;
	}

147 148
	/**
	 * If a DB error occurs, call the installed fail handler (if any) otherwise report error and exit immediately.
pupi1985 committed
149
	 * @param string $type
150 151 152 153 154
	 * @param int $errno
	 * @param string $error
	 * @param string $query
	 * @return mixed
	 */
Scott committed
155 156 157 158 159
	public function failError($type, $errno = null, $error = null, $query = null)
	{
		@error_log('PHP Question2Answer MySQL ' . $type . ' error ' . $errno . ': ' . $error . (isset($query) ? (' - Query: ' . $query) : ''));

		if (function_exists($this->failHandler)) {
Scott committed
160 161
			$failFunc = $this->failHandler;
			$failFunc($type, $errno, $error, $query);
Scott committed
162 163 164
		} else {
			echo sprintf(
				'<hr><div style="color: red">Database %s<p>%s</p><code>%s</code></div>',
165 166 167
				htmlspecialchars($type . ' error ' . $errno),
				nl2br(htmlspecialchars($error)),
				nl2br(htmlspecialchars($query))
Scott committed
168 169 170 171 172
			);
			qa_exit('error');
		}
	}

173 174
	/**
	 * Prepare and execute a SQL query, handling any failures. In debugging mode, track the queries and resources used.
175
	 * @throws SelectSpecException
176 177 178
	 * @param string $query
	 * @param array $params
	 * @return DbResult
179
	 */
180
	public function query($query, $params = [])
Scott committed
181
	{
182 183 184 185
		if (!$this->isConnected()) {
			$this->connect();
		}

186 187 188 189
		$helper = new DbQueryHelper;
		$query = $helper->applyTableSub($query);
		// handle WHERE..IN and INSERT..VALUES queries
		list($query, $params) = $helper->expandParameters($query, $params);
190

191 192 193 194
		if (substr_count($query, '?') != count($params)) {
			throw new SelectSpecException('The number of parameters and placeholders do not match');
		}

Scott committed
195 196 197 198 199 200 201 202 203
		try {
			if (QA_DEBUG_PERFORMANCE) {
				global $qa_usage;

				// time the query
				$oldtime = array_sum(explode(' ', microtime()));
				$stmt = $this->execute($query, $params);
				$usedtime = array_sum(explode(' ', microtime())) - $oldtime;

204
				$qa_usage->logDatabaseQuery($query, $usedtime, $stmt->rowCount(), $stmt->columnCount());
Scott committed
205 206 207
			} else {
				$stmt = $this->execute($query, $params);
			}
208

209
			return new DbResult($stmt);
210
		} catch (PDOException $ex) {
Scott committed
211 212 213 214
			$this->failError('query', $ex->getCode(), $ex->getMessage(), $query);
		}
	}

215 216 217
	/**
	 * Lower-level function to prepare and execute a SQL query. Automatically retries if there is a MySQL deadlock
	 * error.
218 219
	 * @param string $query
	 * @param array $params
220 221
	 * @return PDOStatement
	 */
222
	protected function execute($query, $params = array())
Scott committed
223 224
	{
		$stmt = $this->pdo->prepare($query);
Scott committed
225 226
		// PDO quotes parameters by default, which breaks LIMIT clauses, so we bind parameters manually
		foreach (array_values($params) as $i => $param) {
227 228 229 230 231 232
			if (filter_var($param, FILTER_VALIDATE_INT) !== false) {
				$dataType = PDO::PARAM_INT;
				$param = (int) $param;
			} else {
				$dataType = PDO::PARAM_STR;
			}
Scott committed
233
			$stmt->bindValue($i + 1, $param, $dataType);
Scott committed
234
		}
Scott committed
235 236

		for ($attempt = 0; $attempt < 100; $attempt++) {
Scott committed
237
			$success = $stmt->execute();
Scott committed
238 239 240 241 242 243 244 245 246 247 248

			if ($success === true || $stmt->errorCode() !== '1213') {
				break;
			}

			// deal with InnoDB deadlock errors by waiting 0.01s then retrying
			usleep(10000);
		}

		return $stmt;
	}
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276

	/**
	 * Return the value of the auto-increment column for the last inserted row.
	 * @return string
	 */
	public function lastInsertId()
	{
		return $this->pdo->lastInsertId();
	}

	/**
	 * Suspend or reinstate the updating of counts (of many different types) in the database, to save time when making
	 * a lot of changes. A counter is kept to allow multiple calls.
	 * @param bool $suspend
	 */
	public function suspendUpdateCounts($suspend = true)
	{
		$this->updateCountsSuspended += ($suspend ? 1 : -1);
	}

	/**
	 * Returns whether counts should currently be updated (i.e. if count updating has not been suspended).
	 * @return bool
	 */
	public function shouldUpdateCounts()
	{
		return $this->updateCountsSuspended <= 0;
	}
Scott committed
277
}