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