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