Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
72.62% |
61 / 84 |
|
16.67% |
1 / 6 |
CRAP | |
0.00% |
0 / 1 |
DatabaseFactory | |
72.62% |
61 / 84 |
|
16.67% |
1 / 6 |
30.05 | |
0.00% |
0 / 1 |
__construct | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
create | |
91.67% |
33 / 36 |
|
0.00% |
0 / 1 |
5.01 | |||
attributesFromType | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getClass | |
72.00% |
18 / 25 |
|
0.00% |
0 / 1 |
9.40 | |||
initConnectionFlags | |
40.00% |
2 / 5 |
|
0.00% |
0 / 1 |
4.94 | |||
fieldHasBit | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | namespace Wikimedia\Rdbms; |
21 | |
22 | use InvalidArgumentException; |
23 | use Psr\Log\NullLogger; |
24 | use Throwable; |
25 | use Wikimedia\ObjectCache\HashBagOStuff; |
26 | use Wikimedia\RequestTimeout\CriticalSectionProvider; |
27 | |
28 | /** |
29 | * Constructs Database objects |
30 | * |
31 | * @since 1.39 |
32 | * @ingroup Database |
33 | */ |
34 | class DatabaseFactory { |
35 | /** @var string Agent name for query profiling */ |
36 | private $agent; |
37 | /** @var callable Deprecation logger */ |
38 | private $deprecationLogger; |
39 | /** |
40 | * @var callable|null An optional callback that returns a ScopedCallback instance, |
41 | * meant to profile the actual query execution in {@see Database::doQuery} |
42 | */ |
43 | private $profiler; |
44 | /** @var CriticalSectionProvider|null */ |
45 | private $csProvider; |
46 | /** @var bool Whether this PHP instance is for a CLI script */ |
47 | private $cliMode; |
48 | /** @var bool Log SQL queries in debug toolbar if set to true */ |
49 | private $debugSql; |
50 | |
51 | public function __construct( array $params = [] ) { |
52 | $this->agent = $params['agent'] ?? ''; |
53 | $this->deprecationLogger = $params['deprecationLogger'] ?? static function ( $msg ) { |
54 | trigger_error( $msg, E_USER_DEPRECATED ); |
55 | }; |
56 | $this->csProvider = $params['criticalSectionProvider'] ?? null; |
57 | $this->profiler = $params['profiler'] ?? null; |
58 | $this->cliMode = $params['cliMode'] ?? ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ); |
59 | $this->debugSql = $params['debugSql'] ?? false; |
60 | } |
61 | |
62 | /** |
63 | * Construct a Database subclass instance given a database type and parameters |
64 | * |
65 | * This also connects to the database immediately upon object construction |
66 | * |
67 | * @param string $type A possible DB type (sqlite, mysql, postgres,...) |
68 | * @param array $params Parameter map with keys: |
69 | * - host : The hostname of the DB server |
70 | * - user : The name of the database user the client operates under |
71 | * - password : The password for the database user |
72 | * - dbname : The name of the database to use where queries do not specify one. |
73 | * The database must exist or an error might be thrown. Setting this to the empty string |
74 | * will avoid any such errors and make the handle have no implicit database scope. This is |
75 | * useful for queries like SHOW STATUS, CREATE DATABASE, or DROP DATABASE. Note that a |
76 | * "database" in Postgres is rougly equivalent to an entire MySQL server. This the domain |
77 | * in which user names and such are defined, e.g. users are database-specific in Postgres. |
78 | * - schema : The database schema to use (if supported). A "schema" in Postgres is roughly |
79 | * equivalent to a "database" in MySQL. Note that MySQL and SQLite do not use schemas. |
80 | * - tablePrefix : Optional table prefix that is implicitly added on to all table names |
81 | * recognized in queries. This can be used in place of schemas for handle site farms. |
82 | * - flags : Optional bit field of DBO_* constants that define connection, protocol, |
83 | * buffering, and transaction behavior. It is STRONGLY adviced to leave the DBO_DEFAULT |
84 | * flag in place UNLESS this this database simply acts as a key/value store. |
85 | * - ssl : Whether to use TLS connections. |
86 | * - strictWarnings: Whether to check for warnings and throw an exception if an unacceptable |
87 | * warning is found. |
88 | * - driver: Optional name of a specific DB client driver. For MySQL, there is only the |
89 | * 'mysqli' driver; the old one 'mysql' has been removed. |
90 | * - variables: Optional map of session variables to set after connecting. This can be |
91 | * used to adjust lock timeouts or encoding modes and the like. |
92 | * - topologyRole: Optional IDatabase::ROLE_* constant for the server. |
93 | * - lbInfo: Optional map of field/values for the managing load balancer instance. |
94 | * The "master" and "replica" fields are used to flag the replication role of this |
95 | * database server and whether methods like getLag() should actually issue queries. |
96 | * - connectTimeout: Optional timeout, in seconds, for connection attempts. |
97 | * - receiveTimeout: Optional timeout, in seconds, for receiving query results. |
98 | * - logger: Optional PSR-3 logger interface instance. |
99 | * - profiler : Optional callback that takes a section name argument and returns |
100 | * a ScopedCallback instance that ends the profile section in its destructor. |
101 | * These will be called in query(), using a simplified version of the SQL that |
102 | * also includes the agent as a SQL comment. |
103 | * - trxProfiler: Optional TransactionProfiler instance. |
104 | * - errorLogger: Optional callback that takes an Exception and logs it. |
105 | * - deprecationLogger: Optional callback that takes a string and logs it. |
106 | * - cliMode: Whether to consider the execution context that of a CLI script. |
107 | * - agent: Optional name used to identify the end-user in query profiling/logging. |
108 | * - serverName: Optional human-readable server name |
109 | * - srvCache: Optional BagOStuff instance to an APC-style cache. |
110 | * - nonNativeInsertSelectBatchSize: Optional batch size for non-native INSERT SELECT. |
111 | * @param int $connect One of the class constants (NEW_CONNECTED, NEW_UNCONNECTED) [optional] |
112 | * @return Database|null If the database driver or extension cannot be found |
113 | * @throws InvalidArgumentException If the database driver or extension cannot be found |
114 | */ |
115 | public function create( $type, $params = [], $connect = Database::NEW_CONNECTED ) { |
116 | $class = $this->getClass( $type, $params['driver'] ?? null ); |
117 | |
118 | if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) { |
119 | $params += [ |
120 | // Default configuration |
121 | 'host' => null, |
122 | 'user' => null, |
123 | 'password' => null, |
124 | 'dbname' => null, |
125 | 'schema' => null, |
126 | 'tablePrefix' => '', |
127 | 'variables' => [], |
128 | 'lbInfo' => [], |
129 | 'serverName' => null, |
130 | 'topologyRole' => null, |
131 | // Objects and callbacks |
132 | 'srvCache' => $params['srvCache'] ?? new HashBagOStuff(), |
133 | 'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(), |
134 | 'logger' => $params['logger'] ?? new NullLogger(), |
135 | 'errorLogger' => $params['errorLogger'] ?? static function ( Throwable $e ) { |
136 | trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING ); |
137 | }, |
138 | ]; |
139 | |
140 | $params['flags'] ??= 0; |
141 | if ( $this->debugSql ) { |
142 | $params['flags'] |= DBO_DEBUG; |
143 | } |
144 | |
145 | $overrides = [ |
146 | 'flags' => $this->initConnectionFlags( $params['flags'] ), |
147 | 'cliMode' => $this->cliMode, |
148 | 'agent' => $this->agent, |
149 | 'profiler' => $this->profiler, |
150 | 'deprecationLogger' => $this->deprecationLogger, |
151 | 'criticalSectionProvider' => $this->csProvider, |
152 | ]; |
153 | |
154 | /** @var Database $conn */ |
155 | $conn = new $class( array_merge( $params, $overrides ) ); |
156 | if ( $connect === Database::NEW_CONNECTED ) { |
157 | $conn->initConnection(); |
158 | } |
159 | } else { |
160 | $conn = null; |
161 | } |
162 | |
163 | return $conn; |
164 | } |
165 | |
166 | /** |
167 | * @param string $dbType A possible DB type (sqlite, mysql, postgres,...) |
168 | * @param string|null $driver Optional name of a specific DB client driver |
169 | * @return array Map of (Database::ATTR_* constant => value) for all such constants |
170 | * @throws DBUnexpectedError |
171 | */ |
172 | public function attributesFromType( $dbType, $driver = null ) { |
173 | static $defaults = [ |
174 | Database::ATTR_DB_IS_FILE => false, |
175 | Database::ATTR_DB_LEVEL_LOCKING => false, |
176 | Database::ATTR_SCHEMAS_AS_TABLE_GROUPS => false |
177 | ]; |
178 | |
179 | $class = $this->getClass( $dbType, $driver ); |
180 | if ( class_exists( $class ) ) { |
181 | return call_user_func( [ $class, 'getAttributes' ] ) + $defaults; |
182 | } else { |
183 | throw new DBUnexpectedError( null, "$dbType is not a supported database type." ); |
184 | } |
185 | } |
186 | |
187 | /** |
188 | * @param string $dbType A possible DB type (sqlite, mysql, postgres,...) |
189 | * @param string|null $driver Optional name of a specific DB client driver |
190 | * @return string Database subclass name to use |
191 | * @throws InvalidArgumentException |
192 | */ |
193 | protected function getClass( $dbType, $driver = null ) { |
194 | // For database types with built-in support, the below maps type to IDatabase |
195 | // implementations. For types with multiple driver implementations (PHP extensions), |
196 | // an array can be used, keyed by extension name. In case of an array, the |
197 | // optional 'driver' parameter can be used to force a specific driver. Otherwise, |
198 | // we auto-detect the first available driver. For types without built-in support, |
199 | // a class named "Database<Type>" is used, eg. DatabaseFoo for type 'foo'. |
200 | static $builtinTypes = [ |
201 | 'mysql' => [ 'mysqli' => DatabaseMySQL::class ], |
202 | 'sqlite' => DatabaseSqlite::class, |
203 | 'postgres' => DatabasePostgres::class, |
204 | ]; |
205 | |
206 | $dbType = strtolower( $dbType ); |
207 | |
208 | if ( !isset( $builtinTypes[$dbType] ) ) { |
209 | // Not a built in type, assume standard naming scheme |
210 | return 'Database' . ucfirst( $dbType ); |
211 | } |
212 | |
213 | $class = false; |
214 | $possibleDrivers = $builtinTypes[$dbType]; |
215 | if ( is_string( $possibleDrivers ) ) { |
216 | $class = $possibleDrivers; |
217 | } elseif ( (string)$driver !== '' ) { |
218 | if ( !isset( $possibleDrivers[$driver] ) ) { |
219 | throw new InvalidArgumentException( __METHOD__ . |
220 | " type '$dbType' does not support driver '{$driver}'" ); |
221 | } |
222 | |
223 | $class = $possibleDrivers[$driver]; |
224 | } else { |
225 | foreach ( $possibleDrivers as $posDriver => $possibleClass ) { |
226 | if ( extension_loaded( $posDriver ) ) { |
227 | $class = $possibleClass; |
228 | break; |
229 | } |
230 | } |
231 | } |
232 | |
233 | if ( $class === false ) { |
234 | throw new InvalidArgumentException( __METHOD__ . |
235 | " no viable database extension found for type '$dbType'" ); |
236 | } |
237 | |
238 | return $class; |
239 | } |
240 | |
241 | /** |
242 | * @see IDatabase::DBO_DEFAULT |
243 | * @param int $flags Bit field of IDatabase::DBO_* constants from configuration |
244 | * @return int Bit field of IDatabase::DBO_* constants to use with Database::factory() |
245 | */ |
246 | private function initConnectionFlags( int $flags ) { |
247 | if ( self::fieldHasBit( $flags, IDatabase::DBO_DEFAULT ) ) { |
248 | // Server is configured to participate in transaction rounds in non-CLI mode |
249 | if ( $this->cliMode ) { |
250 | $flags &= ~IDatabase::DBO_TRX; |
251 | } else { |
252 | $flags |= IDatabase::DBO_TRX; |
253 | } |
254 | } |
255 | return $flags; |
256 | } |
257 | |
258 | /** |
259 | * @param int $flags A bitfield of flags |
260 | * @param int $bit Bit flag constant |
261 | * @return bool Whether the bit field has the specified bit flag set |
262 | */ |
263 | private function fieldHasBit( int $flags, int $bit ) { |
264 | return ( ( $flags & $bit ) === $bit ); |
265 | } |
266 | } |