Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.62% covered (warning)
72.62%
61 / 84
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
DatabaseFactory
72.62% covered (warning)
72.62%
61 / 84
16.67% covered (danger)
16.67%
1 / 6
30.05
0.00% covered (danger)
0.00%
0 / 1
 __construct
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 create
91.67% covered (success)
91.67%
33 / 36
0.00% covered (danger)
0.00%
0 / 1
5.01
 attributesFromType
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getClass
72.00% covered (warning)
72.00%
18 / 25
0.00% covered (danger)
0.00%
0 / 1
9.40
 initConnFlags
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 fieldHasBit
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
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 */
20namespace Wikimedia\Rdbms;
21
22use InvalidArgumentException;
23use Psr\Log\NullLogger;
24use Throwable;
25use Wikimedia\ObjectCache\HashBagOStuff;
26use Wikimedia\RequestTimeout\CriticalSectionProvider;
27
28/**
29 * Constructs Database objects
30 *
31 * @since 1.39
32 * @ingroup Database
33 */
34class 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                // Participate in transaction rounds if $server does not specify otherwise
147                'flags' => $this->initConnFlags( $params['flags'] ),
148                'cliMode' => $this->cliMode,
149                'agent' => $this->agent,
150                'profiler' => $this->profiler,
151                'deprecationLogger' => $this->deprecationLogger,
152                'criticalSectionProvider' => $this->csProvider,
153            ];
154
155            /** @var Database $conn */
156            $conn = new $class( array_merge( $params, $overrides ) );
157            if ( $connect === Database::NEW_CONNECTED ) {
158                $conn->initConnection();
159            }
160        } else {
161            $conn = null;
162        }
163
164        return $conn;
165    }
166
167    /**
168     * @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
169     * @param string|null $driver Optional name of a specific DB client driver
170     * @return array Map of (Database::ATTR_* constant => value) for all such constants
171     * @throws DBUnexpectedError
172     */
173    public function attributesFromType( $dbType, $driver = null ) {
174        static $defaults = [
175            Database::ATTR_DB_IS_FILE => false,
176            Database::ATTR_DB_LEVEL_LOCKING => false,
177            Database::ATTR_SCHEMAS_AS_TABLE_GROUPS => false
178        ];
179
180        $class = $this->getClass( $dbType, $driver );
181        if ( class_exists( $class ) ) {
182            return call_user_func( [ $class, 'getAttributes' ] ) + $defaults;
183        } else {
184            throw new DBUnexpectedError( null, "$dbType is not a supported database type." );
185        }
186    }
187
188    /**
189     * @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
190     * @param string|null $driver Optional name of a specific DB client driver
191     * @return string Database subclass name to use
192     * @throws InvalidArgumentException
193     */
194    protected function getClass( $dbType, $driver = null ) {
195        // For database types with built-in support, the below maps type to IDatabase
196        // implementations. For types with multiple driver implementations (PHP extensions),
197        // an array can be used, keyed by extension name. In case of an array, the
198        // optional 'driver' parameter can be used to force a specific driver. Otherwise,
199        // we auto-detect the first available driver. For types without built-in support,
200        // a class named "Database<Type>" is used, eg. DatabaseFoo for type 'foo'.
201        static $builtinTypes = [
202            'mysql' => [ 'mysqli' => DatabaseMySQL::class ],
203            'sqlite' => DatabaseSqlite::class,
204            'postgres' => DatabasePostgres::class,
205        ];
206
207        $dbType = strtolower( $dbType );
208
209        if ( !isset( $builtinTypes[$dbType] ) ) {
210            // Not a built in type, assume standard naming scheme
211            return 'Database' . ucfirst( $dbType );
212        }
213
214        $class = false;
215        $possibleDrivers = $builtinTypes[$dbType];
216        if ( is_string( $possibleDrivers ) ) {
217            $class = $possibleDrivers;
218        } elseif ( (string)$driver !== '' ) {
219            if ( !isset( $possibleDrivers[$driver] ) ) {
220                throw new InvalidArgumentException( __METHOD__ .
221                    " type '$dbType' does not support driver '{$driver}'" );
222            }
223
224            $class = $possibleDrivers[$driver];
225        } else {
226            foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
227                if ( extension_loaded( $posDriver ) ) {
228                    $class = $possibleClass;
229                    break;
230                }
231            }
232        }
233
234        if ( $class === false ) {
235            throw new InvalidArgumentException( __METHOD__ .
236                " no viable database extension found for type '$dbType'" );
237        }
238
239        return $class;
240    }
241
242    /**
243     * @see IDatabase::DBO_DEFAULT
244     * @param int $flags Bit field of IDatabase::DBO_* constants from configuration
245     * @return int Bit field of IDatabase::DBO_* constants to use with Database::factory()
246     */
247    private function initConnFlags( $flags ) {
248        if ( self::fieldHasBit( $flags, IDatabase::DBO_DEFAULT ) ) {
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}