vendor/doctrine/dbal/src/DriverManager.php line 211

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