1<?php
2
3namespace Doctrine\DBAL;
4
5use Doctrine\Common\EventManager;
6use Doctrine\DBAL\Driver\DrizzlePDOMySql\Driver as DrizzlePDOMySQLDriver;
7use Doctrine\DBAL\Driver\IBMDB2\DB2Driver;
8use Doctrine\DBAL\Driver\Mysqli\Driver as MySQLiDriver;
9use Doctrine\DBAL\Driver\OCI8\Driver as OCI8Driver;
10use Doctrine\DBAL\Driver\PDOMySql\Driver as PDOMySQLDriver;
11use Doctrine\DBAL\Driver\PDOOracle\Driver as PDOOCIDriver;
12use Doctrine\DBAL\Driver\PDOPgSql\Driver as PDOPgSQLDriver;
13use Doctrine\DBAL\Driver\PDOSqlite\Driver as PDOSQLiteDriver;
14use Doctrine\DBAL\Driver\PDOSqlsrv\Driver as PDOSQLSrvDriver;
15use Doctrine\DBAL\Driver\SQLAnywhere\Driver as SQLAnywhereDriver;
16use Doctrine\DBAL\Driver\SQLSrv\Driver as SQLSrvDriver;
17use PDO;
18
19use function array_keys;
20use function array_map;
21use function array_merge;
22use function assert;
23use function class_implements;
24use function in_array;
25use function is_string;
26use function is_subclass_of;
27use function parse_str;
28use function parse_url;
29use function preg_replace;
30use function str_replace;
31use function strpos;
32use function substr;
33
34/**
35 * Factory for creating Doctrine\DBAL\Connection instances.
36 */
37final class DriverManager
38{
39    /**
40     * List of supported drivers and their mappings to the driver classes.
41     *
42     * To add your own driver use the 'driverClass' parameter to
43     * {@link DriverManager::getConnection()}.
44     *
45     * @var string[]
46     */
47    private static $_driverMap = [
48        'pdo_mysql'          => PDOMySQLDriver::class,
49        'pdo_sqlite'         => PDOSQLiteDriver::class,
50        'pdo_pgsql'          => PDOPgSQLDriver::class,
51        'pdo_oci'            => PDOOCIDriver::class,
52        'oci8'               => OCI8Driver::class,
53        'ibm_db2'            => DB2Driver::class,
54        'pdo_sqlsrv'         => PDOSQLSrvDriver::class,
55        'mysqli'             => MySQLiDriver::class,
56        'drizzle_pdo_mysql'  => DrizzlePDOMySQLDriver::class,
57        'sqlanywhere'        => SQLAnywhereDriver::class,
58        'sqlsrv'             => SQLSrvDriver::class,
59    ];
60
61    /**
62     * List of URL schemes from a database URL and their mappings to driver.
63     *
64     * @var string[]
65     */
66    private static $driverSchemeAliases = [
67        'db2'        => 'ibm_db2',
68        'mssql'      => 'pdo_sqlsrv',
69        'mysql'      => 'pdo_mysql',
70        'mysql2'     => 'pdo_mysql', // Amazon RDS, for some weird reason
71        'postgres'   => 'pdo_pgsql',
72        'postgresql' => 'pdo_pgsql',
73        'pgsql'      => 'pdo_pgsql',
74        'sqlite'     => 'pdo_sqlite',
75        'sqlite3'    => 'pdo_sqlite',
76    ];
77
78    /**
79     * Private constructor. This class cannot be instantiated.
80     *
81     * @codeCoverageIgnore
82     */
83    private function __construct()
84    {
85    }
86
87    /**
88     * Creates a connection object based on the specified parameters.
89     * This method returns a Doctrine\DBAL\Connection which wraps the underlying
90     * driver connection.
91     *
92     * $params must contain at least one of the following.
93     *
94     * Either 'driver' with one of the array keys of {@link $_driverMap},
95     * OR 'driverClass' that contains the full class name (with namespace) of the
96     * driver class to instantiate.
97     *
98     * Other (optional) parameters:
99     *
100     * <b>user (string)</b>:
101     * The username to use when connecting.
102     *
103     * <b>password (string)</b>:
104     * The password to use when connecting.
105     *
106     * <b>driverOptions (array)</b>:
107     * Any additional driver-specific options for the driver. These are just passed
108     * through to the driver.
109     *
110     * <b>pdo</b>:
111     * You can pass an existing PDO instance through this parameter. The PDO
112     * instance will be wrapped in a Doctrine\DBAL\Connection.
113     *
114     * <b>wrapperClass</b>:
115     * You may specify a custom wrapper class through the 'wrapperClass'
116     * parameter but this class MUST inherit from Doctrine\DBAL\Connection.
117     *
118     * <b>driverClass</b>:
119     * The driver class to use.
120     *
121     * @param  array{wrapperClass?: class-string<T>} $params
122     * @param Configuration|null                    $config       The configuration to use.
123     * @param EventManager|null                     $eventManager The event manager to use.
124     *
125     * @throws DBALException
126     *
127     * @phpstan-param mixed[] $params
128     * @psalm-return ($params is array{wrapperClass:mixed} ? T : Connection)
129     * @template T of Connection
130     */
131    public static function getConnection(
132        array $params,
133        ?Configuration $config = null,
134        ?EventManager $eventManager = null
135    ): Connection {
136        // create default config and event manager, if not set
137        if (! $config) {
138            $config = new Configuration();
139        }
140
141        if (! $eventManager) {
142            $eventManager = new EventManager();
143        }
144
145        $params = self::parseDatabaseUrl($params);
146
147        // URL support for MasterSlaveConnection
148        if (isset($params['master'])) {
149            $params['master'] = self::parseDatabaseUrl($params['master']);
150        }
151
152        if (isset($params['slaves'])) {
153            foreach ($params['slaves'] as $key => $slaveParams) {
154                $params['slaves'][$key] = self::parseDatabaseUrl($slaveParams);
155            }
156        }
157
158        // URL support for PoolingShardConnection
159        if (isset($params['global'])) {
160            $params['global'] = self::parseDatabaseUrl($params['global']);
161        }
162
163        if (isset($params['shards'])) {
164            foreach ($params['shards'] as $key => $shardParams) {
165                $params['shards'][$key] = self::parseDatabaseUrl($shardParams);
166            }
167        }
168
169        // check for existing pdo object
170        if (isset($params['pdo']) && ! $params['pdo'] instanceof PDO) {
171            throw DBALException::invalidPdoInstance();
172        }
173
174        if (isset($params['pdo'])) {
175            $params['pdo']->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
176            $params['driver'] = 'pdo_' . $params['pdo']->getAttribute(PDO::ATTR_DRIVER_NAME);
177        } else {
178            self::_checkParams($params);
179        }
180
181        $className = $params['driverClass'] ?? self::$_driverMap[$params['driver']];
182
183        $driver = new $className();
184
185        $wrapperClass = Connection::class;
186        if (isset($params['wrapperClass'])) {
187            if (! is_subclass_of($params['wrapperClass'], $wrapperClass)) {
188                throw DBALException::invalidWrapperClass($params['wrapperClass']);
189            }
190
191            $wrapperClass = $params['wrapperClass'];
192        }
193
194        return new $wrapperClass($params, $driver, $config, $eventManager);
195    }
196
197    /**
198     * Returns the list of supported drivers.
199     *
200     * @return string[]
201     */
202    public static function getAvailableDrivers(): array
203    {
204        return array_keys(self::$_driverMap);
205    }
206
207    /**
208     * Checks the list of parameters.
209     *
210     * @param mixed[] $params The list of parameters.
211     *
212     * @throws DBALException
213     */
214    private static function _checkParams(array $params): void
215    {
216        // check existence of mandatory parameters
217
218        // driver
219        if (! isset($params['driver']) && ! isset($params['driverClass'])) {
220            throw DBALException::driverRequired();
221        }
222
223        // check validity of parameters
224
225        // driver
226        if (isset($params['driver']) && ! isset(self::$_driverMap[$params['driver']])) {
227            throw DBALException::unknownDriver($params['driver'], array_keys(self::$_driverMap));
228        }
229
230        if (
231            isset($params['driverClass'])
232            && ! in_array(Driver::class, class_implements($params['driverClass'], true))
233        ) {
234            throw DBALException::invalidDriverClass($params['driverClass']);
235        }
236    }
237
238    /**
239     * Normalizes the given connection URL path.
240     *
241     * @return string The normalized connection URL path
242     */
243    private static function normalizeDatabaseUrlPath(string $urlPath): string
244    {
245        // Trim leading slash from URL path.
246        return substr($urlPath, 1);
247    }
248
249    /**
250     * Extracts parts from a database URL, if present, and returns an
251     * updated list of parameters.
252     *
253     * @param mixed[] $params The list of parameters.
254     *
255     * @return mixed[] A modified list of parameters with info from a database
256     *                 URL extracted into indidivual parameter parts.
257     *
258     * @throws DBALException
259     */
260    private static function parseDatabaseUrl(array $params): array
261    {
262        if (! isset($params['url'])) {
263            return $params;
264        }
265
266        // (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid
267        $url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $params['url']);
268        assert(is_string($url));
269
270        $url = parse_url($url);
271
272        if ($url === false) {
273            throw new DBALException('Malformed parameter "url".');
274        }
275
276        $url = array_map('rawurldecode', $url);
277
278        // If we have a connection URL, we have to unset the default PDO instance connection parameter (if any)
279        // as we cannot merge connection details from the URL into the PDO instance (URL takes precedence).
280        unset($params['pdo']);
281
282        $params = self::parseDatabaseUrlScheme($url, $params);
283
284        if (isset($url['host'])) {
285            $params['host'] = $url['host'];
286        }
287
288        if (isset($url['port'])) {
289            $params['port'] = $url['port'];
290        }
291
292        if (isset($url['user'])) {
293            $params['user'] = $url['user'];
294        }
295
296        if (isset($url['pass'])) {
297            $params['password'] = $url['pass'];
298        }
299
300        $params = self::parseDatabaseUrlPath($url, $params);
301        $params = self::parseDatabaseUrlQuery($url, $params);
302
303        return $params;
304    }
305
306    /**
307     * Parses the given connection URL and resolves the given connection parameters.
308     *
309     * Assumes that the connection URL scheme is already parsed and resolved into the given connection parameters
310     * via {@link parseDatabaseUrlScheme}.
311     *
312     * @see parseDatabaseUrlScheme
313     *
314     * @param mixed[] $url    The URL parts to evaluate.
315     * @param mixed[] $params The connection parameters to resolve.
316     *
317     * @return mixed[] The resolved connection parameters.
318     */
319    private static function parseDatabaseUrlPath(array $url, array $params): array
320    {
321        if (! isset($url['path'])) {
322            return $params;
323        }
324
325        $url['path'] = self::normalizeDatabaseUrlPath($url['path']);
326
327        // If we do not have a known DBAL driver, we do not know any connection URL path semantics to evaluate
328        // and therefore treat the path as regular DBAL connection URL path.
329        if (! isset($params['driver'])) {
330            return self::parseRegularDatabaseUrlPath($url, $params);
331        }
332
333        if (strpos($params['driver'], 'sqlite') !== false) {
334            return self::parseSqliteDatabaseUrlPath($url, $params);
335        }
336
337        return self::parseRegularDatabaseUrlPath($url, $params);
338    }
339
340    /**
341     * Parses the query part of the given connection URL and resolves the given connection parameters.
342     *
343     * @param mixed[] $url    The connection URL parts to evaluate.
344     * @param mixed[] $params The connection parameters to resolve.
345     *
346     * @return mixed[] The resolved connection parameters.
347     */
348    private static function parseDatabaseUrlQuery(array $url, array $params): array
349    {
350        if (! isset($url['query'])) {
351            return $params;
352        }
353
354        $query = [];
355
356        parse_str($url['query'], $query); // simply ingest query as extra params, e.g. charset or sslmode
357
358        return array_merge($params, $query); // parse_str wipes existing array elements
359    }
360
361    /**
362     * Parses the given regular connection URL and resolves the given connection parameters.
363     *
364     * Assumes that the "path" URL part is already normalized via {@link normalizeDatabaseUrlPath}.
365     *
366     * @see normalizeDatabaseUrlPath
367     *
368     * @param mixed[] $url    The regular connection URL parts to evaluate.
369     * @param mixed[] $params The connection parameters to resolve.
370     *
371     * @return mixed[] The resolved connection parameters.
372     */
373    private static function parseRegularDatabaseUrlPath(array $url, array $params): array
374    {
375        $params['dbname'] = $url['path'];
376
377        return $params;
378    }
379
380    /**
381     * Parses the given SQLite connection URL and resolves the given connection parameters.
382     *
383     * Assumes that the "path" URL part is already normalized via {@link normalizeDatabaseUrlPath}.
384     *
385     * @see normalizeDatabaseUrlPath
386     *
387     * @param mixed[] $url    The SQLite connection URL parts to evaluate.
388     * @param mixed[] $params The connection parameters to resolve.
389     *
390     * @return mixed[] The resolved connection parameters.
391     */
392    private static function parseSqliteDatabaseUrlPath(array $url, array $params): array
393    {
394        if ($url['path'] === ':memory:') {
395            $params['memory'] = true;
396
397            return $params;
398        }
399
400        $params['path'] = $url['path']; // pdo_sqlite driver uses 'path' instead of 'dbname' key
401
402        return $params;
403    }
404
405    /**
406     * Parses the scheme part from given connection URL and resolves the given connection parameters.
407     *
408     * @param mixed[] $url    The connection URL parts to evaluate.
409     * @param mixed[] $params The connection parameters to resolve.
410     *
411     * @return mixed[] The resolved connection parameters.
412     *
413     * @throws DBALException If parsing failed or resolution is not possible.
414     */
415    private static function parseDatabaseUrlScheme(array $url, array $params): array
416    {
417        if (isset($url['scheme'])) {
418            // The requested driver from the URL scheme takes precedence
419            // over the default custom driver from the connection parameters (if any).
420            unset($params['driverClass']);
421
422            // URL schemes must not contain underscores, but dashes are ok
423            $driver = str_replace('-', '_', $url['scheme']);
424            assert(is_string($driver));
425
426            // The requested driver from the URL scheme takes precedence over the
427            // default driver from the connection parameters. If the driver is
428            // an alias (e.g. "postgres"), map it to the actual name ("pdo-pgsql").
429            // Otherwise, let checkParams decide later if the driver exists.
430            $params['driver'] = self::$driverSchemeAliases[$driver] ?? $driver;
431
432            return $params;
433        }
434
435        // If a schemeless connection URL is given, we require a default driver or default custom driver
436        // as connection parameter.
437        if (! isset($params['driverClass']) && ! isset($params['driver'])) {
438            throw DBALException::driverRequired($params['url']);
439        }
440
441        return $params;
442    }
443}
444