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