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