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