vendor/doctrine/doctrine-bundle/src/ConnectionFactory.php line 77

Open in your IDE?
  1. <?php
  2. namespace Doctrine\Bundle\DoctrineBundle;
  3. use Doctrine\Common\EventManager;
  4. use Doctrine\DBAL\Configuration;
  5. use Doctrine\DBAL\Connection;
  6. use Doctrine\DBAL\Connection\StaticServerVersionProvider;
  7. use Doctrine\DBAL\ConnectionException;
  8. use Doctrine\DBAL\DriverManager;
  9. use Doctrine\DBAL\Exception as DBALException;
  10. use Doctrine\DBAL\Exception\DriverException;
  11. use Doctrine\DBAL\Exception\DriverRequired;
  12. use Doctrine\DBAL\Exception\InvalidWrapperClass;
  13. use Doctrine\DBAL\Exception\MalformedDsnException;
  14. use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
  15. use Doctrine\DBAL\Platforms\AbstractPlatform;
  16. use Doctrine\DBAL\Tools\DsnParser;
  17. use Doctrine\DBAL\Types\Type;
  18. use Doctrine\Deprecations\Deprecation;
  19. use InvalidArgumentException;
  20. use function array_merge;
  21. use function class_exists;
  22. use function is_subclass_of;
  23. use function method_exists;
  24. use function trigger_deprecation;
  25. use const PHP_EOL;
  26. /** @phpstan-import-type Params from DriverManager */
  27. class ConnectionFactory
  28. {
  29. /** @internal */
  30. public const DEFAULT_SCHEME_MAP = [
  31. 'db2' => 'ibm_db2',
  32. 'mssql' => 'pdo_sqlsrv',
  33. 'mysql' => 'pdo_mysql',
  34. 'mysql2' => 'pdo_mysql', // Amazon RDS, for some weird reason
  35. 'postgres' => 'pdo_pgsql',
  36. 'postgresql' => 'pdo_pgsql',
  37. 'pgsql' => 'pdo_pgsql',
  38. 'sqlite' => 'pdo_sqlite',
  39. 'sqlite3' => 'pdo_sqlite',
  40. ];
  41. /** @var mixed[][] */
  42. private array $typesConfig = [];
  43. private DsnParser $dsnParser;
  44. private bool $initialized = false;
  45. /** @param mixed[][] $typesConfig */
  46. public function __construct(array $typesConfig, ?DsnParser $dsnParser = null)
  47. {
  48. $this->typesConfig = $typesConfig;
  49. $this->dsnParser = $dsnParser ?? new DsnParser(self::DEFAULT_SCHEME_MAP);
  50. }
  51. /**
  52. * Create a connection by name.
  53. *
  54. * @param mixed[] $params
  55. * @param array<string, string> $mappingTypes
  56. * @phpstan-param Params $params
  57. *
  58. * @return Connection
  59. */
  60. public function createConnection(array $params, ?Configuration $config = null, ?EventManager $eventManager = null, array $mappingTypes = [])
  61. {
  62. if (! method_exists(Connection::class, 'getEventManager') && $eventManager !== null) {
  63. throw new InvalidArgumentException('Passing an EventManager instance is not supported with DBAL > 3');
  64. }
  65. if (! $this->initialized) {
  66. $this->initializeTypes();
  67. }
  68. $overriddenOptions = [];
  69. /** @psalm-suppress InvalidArrayOffset We should adjust when https://github.com/vimeo/psalm/issues/8984 is fixed */
  70. if (isset($params['connection_override_options'])) {
  71. trigger_deprecation('doctrine/doctrine-bundle', '2.4', 'The "connection_override_options" connection parameter is deprecated');
  72. $overriddenOptions = $params['connection_override_options'];
  73. unset($params['connection_override_options']);
  74. }
  75. $params = $this->parseDatabaseUrl($params);
  76. // URL support for PrimaryReplicaConnection
  77. if (isset($params['primary'])) {
  78. $params['primary'] = $this->parseDatabaseUrl($params['primary']);
  79. }
  80. if (isset($params['replica'])) {
  81. foreach ($params['replica'] as $key => $replicaParams) {
  82. $params['replica'][$key] = $this->parseDatabaseUrl($replicaParams);
  83. }
  84. }
  85. /** @psalm-suppress InvalidArrayOffset We should adjust when https://github.com/vimeo/psalm/issues/8984 is fixed */
  86. if (! isset($params['pdo']) && (! isset($params['charset']) || $overriddenOptions || isset($params['dbname_suffix']))) {
  87. $wrapperClass = null;
  88. if (isset($params['wrapperClass'])) {
  89. if (! is_subclass_of($params['wrapperClass'], Connection::class)) {
  90. if (class_exists(InvalidWrapperClass::class)) {
  91. throw InvalidWrapperClass::new($params['wrapperClass']);
  92. }
  93. /* @phpstan-ignore staticMethod.notFound */
  94. throw DBALException::invalidWrapperClass($params['wrapperClass']);
  95. }
  96. $wrapperClass = $params['wrapperClass'];
  97. $params['wrapperClass'] = null;
  98. }
  99. $connection = DriverManager::getConnection(...array_merge([$params, $config], $eventManager ? [$eventManager] : []));
  100. $params = $this->addDatabaseSuffix(array_merge($connection->getParams(), $overriddenOptions));
  101. $driver = $connection->getDriver();
  102. /** @phpstan-ignore arguments.count (DBAL < 4.x doesn't accept an argument) */
  103. $platform = $driver->getDatabasePlatform(
  104. ...(class_exists(StaticServerVersionProvider::class)
  105. ? [new StaticServerVersionProvider($params['serverVersion'] ?? $params['primary']['serverVersion'] ?? '')]
  106. : []
  107. ),
  108. );
  109. if (! isset($params['charset'])) {
  110. if ($platform instanceof AbstractMySQLPlatform) {
  111. $params['charset'] = 'utf8mb4';
  112. if (isset($params['defaultTableOptions']['collate'])) {
  113. Deprecation::trigger(
  114. 'doctrine/doctrine-bundle',
  115. 'https://github.com/doctrine/dbal/issues/5214',
  116. 'The "collate" default table option is deprecated in favor of "collation" and will be removed in doctrine/doctrine-bundle 3.0. ',
  117. );
  118. $params['defaultTableOptions']['collation'] = $params['defaultTableOptions']['collate'];
  119. unset($params['defaultTableOptions']['collate']);
  120. }
  121. if (! isset($params['defaultTableOptions']['collation'])) {
  122. $params['defaultTableOptions']['collation'] = 'utf8mb4_unicode_ci';
  123. }
  124. } else {
  125. $params['charset'] = 'utf8';
  126. }
  127. }
  128. if ($wrapperClass !== null) {
  129. $params['wrapperClass'] = $wrapperClass;
  130. } else {
  131. $wrapperClass = Connection::class;
  132. }
  133. $connection = new $wrapperClass($params, $driver, $config, $eventManager);
  134. } else {
  135. $connection = DriverManager::getConnection(...array_merge([$params, $config], $eventManager ? [$eventManager] : []));
  136. }
  137. if (! empty($mappingTypes)) {
  138. $platform = $this->getDatabasePlatform($connection);
  139. foreach ($mappingTypes as $dbType => $doctrineType) {
  140. $platform->registerDoctrineTypeMapping($dbType, $doctrineType);
  141. }
  142. }
  143. return $connection;
  144. }
  145. /**
  146. * Try to get the database platform.
  147. *
  148. * This could fail if types should be registered to an predefined/unused connection
  149. * and the platform version is unknown.
  150. *
  151. * @link https://github.com/doctrine/DoctrineBundle/issues/673
  152. *
  153. * @throws DBALException
  154. */
  155. private function getDatabasePlatform(Connection $connection): AbstractPlatform
  156. {
  157. try {
  158. return $connection->getDatabasePlatform();
  159. } catch (DriverException $driverException) {
  160. $class = class_exists(DBALException::class) ? DBALException::class : ConnectionException::class;
  161. /* @phpstan-ignore new.interface */
  162. throw new $class(
  163. 'An exception occurred while establishing a connection to figure out your platform version.' . PHP_EOL .
  164. "You can circumvent this by setting a 'server_version' configuration value" . PHP_EOL . PHP_EOL .
  165. 'For further information have a look at:' . PHP_EOL .
  166. 'https://github.com/doctrine/DoctrineBundle/issues/673',
  167. 0,
  168. $driverException,
  169. );
  170. }
  171. }
  172. /**
  173. * initialize the types
  174. */
  175. private function initializeTypes(): void
  176. {
  177. foreach ($this->typesConfig as $typeName => $typeConfig) {
  178. if (Type::hasType($typeName)) {
  179. Type::overrideType($typeName, $typeConfig['class']);
  180. } else {
  181. Type::addType($typeName, $typeConfig['class']);
  182. }
  183. }
  184. $this->initialized = true;
  185. }
  186. /**
  187. * @param array<string, mixed> $params
  188. *
  189. * @return array<string, mixed>
  190. */
  191. private function addDatabaseSuffix(array $params): array
  192. {
  193. if (isset($params['dbname']) && isset($params['dbname_suffix'])) {
  194. $params['dbname'] .= $params['dbname_suffix'];
  195. }
  196. foreach ($params['replica'] ?? [] as $key => $replicaParams) {
  197. if (! isset($replicaParams['dbname'], $replicaParams['dbname_suffix'])) {
  198. continue;
  199. }
  200. $params['replica'][$key]['dbname'] .= $replicaParams['dbname_suffix'];
  201. }
  202. if (isset($params['primary']['dbname'], $params['primary']['dbname_suffix'])) {
  203. $params['primary']['dbname'] .= $params['primary']['dbname_suffix'];
  204. }
  205. return $params;
  206. }
  207. /**
  208. * Extracts parts from a database URL, if present, and returns an
  209. * updated list of parameters.
  210. *
  211. * @param mixed[] $params The list of parameters.
  212. * @phpstan-param Params $params
  213. *
  214. * @return mixed[] A modified list of parameters with info from a database
  215. * URL extracted into individual parameter parts.
  216. * @phpstan-return Params
  217. *
  218. * @throws DBALException
  219. */
  220. private function parseDatabaseUrl(array $params): array
  221. {
  222. /** @psalm-suppress InvalidArrayOffset Need to be compatible with DBAL < 4, which still has `$params['url']` */
  223. if (! isset($params['url'])) {
  224. return $params;
  225. }
  226. try {
  227. $parsedParams = $this->dsnParser->parse($params['url']);
  228. } catch (MalformedDsnException $e) {
  229. throw new MalformedDsnException('Malformed parameter "url".', 0, $e);
  230. }
  231. if (isset($parsedParams['driver'])) {
  232. // The requested driver from the URL scheme takes precedence
  233. // over the default custom driver from the connection parameters (if any).
  234. unset($params['driverClass']);
  235. }
  236. $params = array_merge($params, $parsedParams);
  237. // If a schemeless connection URL is given, we require a default driver or default custom driver
  238. // as connection parameter.
  239. if (! isset($params['driverClass']) && ! isset($params['driver'])) {
  240. if (class_exists(DriverRequired::class)) {
  241. throw DriverRequired::new($params['url']);
  242. }
  243. throw DBALException::driverRequired($params['url']);
  244. }
  245. unset($params['url']);
  246. return $params;
  247. }
  248. }