Completed
Push — master ( 2db1fb...5408f9 )
by Nazar
04:38
created

DB::time()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 2
Bugs 2 Features 0
Metric Value
cc 3
eloc 7
c 2
b 2
f 0
nc 4
nop 0
dl 0
loc 13
ccs 7
cts 7
cp 1
crap 3
rs 9.4285
1
<?php
2
/**
3
 * @package   CleverStyle Framework
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2011-2016, Nazar Mokrynskyi
6
 * @license   MIT License, see license.txt
7
 */
8
namespace cs;
9
10
/**
11
 * @method static $this instance($check = false)
12
 */
13
class DB {
14
	use
15
		Singleton;
16
	const CONNECTIONS_MASTER        = 0;
17
	const CONNECTIONS_MIRROR        = 1;
18
	const CONNECTIONS_SUCCESSFUL    = 2;
19
	const CONNECTIONS_FAILED        = 3;
20
	const MASTER_MIRROR             = -1;
21
	const MIRROR_MODE_MASTER_MASTER = 0;
22
	const MIRROR_MODE_MASTER_SLAVE  = 1;
23
	/**
24
	 * @var array[]
25
	 */
26
	protected $connections = [
27
		self::CONNECTIONS_MASTER     => [],
28
		self::CONNECTIONS_MIRROR     => [],
29
		self::CONNECTIONS_SUCCESSFUL => [],
30
		self::CONNECTIONS_FAILED     => [],
31
	];
32
	/**
33
	 * Get list of connections of specified type
34
	 *
35
	 * @param int $type One of constants `self::CONNECTIONS_*`
36
	 *
37
	 * @return array For `self::CONNECTIONS_MASTER` and `self::CONNECTIONS_MIRRORS` array of successful connections with corresponding objects as values of
38
	 *               array, otherwise array where keys are database ids and values are strings with information about database
39
	 */
40 2
	public function get_connections_list ($type) {
41 2
		return $this->connections[$type];
42
	}
43
	/**
44
	 * Total number of executed queries
45
	 *
46
	 * @return int
47
	 */
48 4
	public function queries () {
49 4
		$queries = 0;
50 4
		foreach ($this->connections[self::CONNECTIONS_MASTER] as $c) {
51 3
			$queries += $c->queries()['num'];
52
		}
53 4
		foreach ($this->connections[self::CONNECTIONS_MIRROR] as $c) {
54 1
			$queries += $c->queries()['num'];
55
		}
56 4
		return $queries;
57
	}
58
	/**
59
	 * Total time spent on all queries and connections
60
	 *
61
	 * @return float
62
	 */
63 4
	public function time () {
64 4
		$time = 0;
65
		/**
66
		 * @var DB\_Abstract $c
67
		 */
68 4
		foreach ($this->connections[self::CONNECTIONS_MASTER] as $c) {
69 3
			$time += $c->connecting_time() + $c->time();
70
		}
71 4
		foreach ($this->connections[self::CONNECTIONS_MIRROR] as $c) {
72 1
			$time += $c->connecting_time() + $c->time();
73
		}
74 4
		return $time;
75
	}
76
	/**
77
	 * Get database instance for read queries
78
	 *
79
	 * @param int $database_id
80
	 *
81
	 * @return DB\_Abstract|False_class Returns instance of False_class on failure
82
	 *
83
	 * @throws ExitException
84
	 */
85 38
	public function db ($database_id) {
86 38
		return $this->connect($database_id, true);
87
	}
88
	/**
89
	 * Get database instance for write queries
90
	 *
91
	 * @param int $database_id
92
	 *
93
	 * @return DB\_Abstract|False_class Returns instance of False_class on failure
94
	 *
95
	 * @throws ExitException
96
	 */
97 44
	public function db_prime ($database_id) {
98 44
		return $this->connect($database_id, false);
99
	}
100
	/**
101
	 * @param int  $database_id
102
	 * @param bool $read_query
103
	 *
104
	 * @return DB\_Abstract|False_class
105
	 *
106
	 * @throws ExitException
107
	 */
108 48
	protected function connect ($database_id, $read_query) {
109 48
		$database_id = (int)$database_id;
110
		/**
111
		 * If connection found in list of failed connections - return instance of False_class
112
		 */
113 48
		if (isset($this->connections[self::CONNECTIONS_FAILED][$database_id])) {
114 2
			return False_class::instance();
115
		}
116
		/**
117
		 * If connection to DB mirror already established
118
		 */
119
		if (
120 48
			$read_query &&
121 48
			isset($this->connections[self::CONNECTIONS_MIRROR][$database_id])
122
		) {
123 2
			return $this->connections[self::CONNECTIONS_MIRROR][$database_id];
124
		}
125
		/**
126
		 * If connection already established
127
		 */
128 48
		if (isset($this->connections[self::CONNECTIONS_MASTER][$database_id])) {
129 42
			return $this->connections[self::CONNECTIONS_MASTER][$database_id];
130
		}
131 48
		$Core = Core::instance();
132
		/**
133
		 * Try to connect second time if first time fails, probably, to another mirror
134
		 */
135 48
		for ($trial = 0; $trial < 2; ++$trial) {
136 48
			list($database_settings, $is_mirror) = $this->get_db_connection_settings($database_id, $read_query);
137
			/**
138
			 * Establish new connection
139
			 *
140
			 * @var DB\_Abstract $connection
141
			 */
142 48
			$engine_class    = "cs\\DB\\$database_settings[type]";
143 48
			$connection      = new $engine_class(
144 48
				$database_settings['name'],
145 48
				$database_settings['user'],
146 48
				$database_settings['password'],
147 48
				$database_settings['host'],
148 48
				$database_settings['prefix']
149
			);
150 48
			$connection_name = ($database_id == 0 ? "Core DB ($Core->db_type)" : $database_id)."/$database_settings[host]/$database_settings[type]";
151 48
			unset($engine_class, $database_settings);
152
			/**
153
			 * If successfully - add connection to the list of success connections and return instance of DB engine object
154
			 */
155 48
			if (is_object($connection) && $connection->connected()) {
156 48
				$this->connections[self::CONNECTIONS_SUCCESSFUL][] = $connection_name;
157 48
				if ($is_mirror) {
158 2
					$this->connections[self::CONNECTIONS_MIRROR][$database_id] = $connection;
159
				} else {
160 48
					$this->connections[self::CONNECTIONS_MASTER][$database_id] = $connection;
161
				}
162 48
				return $connection;
163
			}
164
		}
165
		/**
166
		 * If failed - add connection to the list of failed connections and log error
167
		 */
168
		/** @noinspection PhpUndefinedVariableInspection */
169 2
		$this->connections[self::CONNECTIONS_FAILED][$database_id] = $connection_name;
170 2
		trigger_error(
171 2
			$database_id == 0 ? 'Error connecting to core DB of site' : "Error connecting to DB $connection_name",
172 2
			E_USER_WARNING
173
		);
174 2
		if ($database_id == 0) {
175 2
			throw new ExitException('Error connecting to core DB of site', 500);
176
		}
177 2
		return False_class::instance();
178
	}
179
	/**
180
	 * Get database connection settings, depending on query type and system configuration settings of main db or one of mirrors might be returned
181
	 *
182
	 * @param int  $database_id
183
	 * @param bool $read_query
184
	 *
185
	 * @return array
186
	 */
187 48
	protected function get_db_connection_settings ($database_id, $read_query) {
188 48
		$Config = Config::instance();
189 48
		$Core   = Core::instance();
190
		/**
191
		 * Choose right mirror depending on system configuration
192
		 */
193 48
		$is_mirror    = false;
194 48
		$mirror_index = $this->choose_mirror($database_id, $read_query);
195 48
		if ($mirror_index === self::MASTER_MIRROR) {
196 48
			if ($database_id == 0) {
197
				$database_settings = [
198 48
					'type'     => $Core->db_type,
199 48
					'name'     => $Core->db_name,
200 48
					'user'     => $Core->db_user,
201 48
					'password' => $Core->db_password,
202 48
					'host'     => $Core->db_host,
203 48
					'prefix'   => $Core->db_prefix
204
				];
205
			} else {
206 48
				$database_settings = $Config->db[$database_id];
207
			}
208
		} else {
209 2
			$database_settings = $Config->db[$database_id]['mirrors'][$mirror_index];
210 2
			$is_mirror         = true;
211
		}
212
		return [
213 48
			$database_settings,
214 48
			$is_mirror
215
		];
216
	}
217
	/**
218
	 * Choose index of DB mirrors among available
219
	 *
220
	 * @param int  $database_id
221
	 * @param bool $read_query
222
	 *
223
	 * @return int
224
	 */
225 48
	protected function choose_mirror ($database_id, $read_query = true) {
226 48
		$Config = Config::instance(true);
227
		/**
228
		 * $Config might not be initialized yet
229
		 */
230
		if (
231 48
			!@$Config->core['db_balance'] ||
232 48
			!isset($Config->db[$database_id]['mirrors'][0])
233
		) {
234 48
			return self::MASTER_MIRROR;
235
		}
236 2
		$mirrors_count = count($Config->db[$database_id]['mirrors']);
237
		/**
238
		 * Main db should be excluded from read requests if writes to mirrors are not allowed
239
		 */
240 2
		$selected_mirror = mt_rand(
241 2
			0,
242 2
			$read_query && $Config->core['db_mirror_mode'] == self::MIRROR_MODE_MASTER_SLAVE ? $mirrors_count - 1 : $mirrors_count
243
		);
244
		/**
245
		 * Main DB assumed to be in the end of interval, that is why `$select_mirror < $mirrors_count` will correspond to one of available mirrors,
246
		 * and `$select_mirror == $mirrors_count` to master DB itself
247
		 */
248 2
		return $selected_mirror < $mirrors_count ? $selected_mirror : self::MASTER_MIRROR;
249
	}
250
}
251