f32ba7022cc3d12d1164dc2f0e0200bca2b2f0e3
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Database / ConnectionPool.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Core\Database;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Doctrine\DBAL\DriverManager;
19 use Doctrine\DBAL\Events;
20 use Doctrine\DBAL\Types\Type;
21 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
22 use TYPO3\CMS\Core\Database\Schema\EventListener\SchemaAlterTableListener;
23 use TYPO3\CMS\Core\Database\Schema\EventListener\SchemaColumnDefinitionListener;
24 use TYPO3\CMS\Core\Database\Schema\EventListener\SchemaIndexDefinitionListener;
25 use TYPO3\CMS\Core\Database\Schema\Types\EnumType;
26 use TYPO3\CMS\Core\Database\Schema\Types\SetType;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28
29 /**
30 * Manager that handles opening/retrieving database connections.
31 *
32 * It's a facade to the actual Doctrine DBAL DriverManager that implements TYPO3
33 * specific functionality like mapping individual tables to different database
34 * connections.
35 *
36 * getConnectionForTable() is the only supported way to get a connection that
37 * honors the table mapping configuration.
38 */
39 class ConnectionPool
40 {
41 /**
42 * @var string
43 */
44 const DEFAULT_CONNECTION_NAME = 'Default';
45
46 /**
47 * @var Connection[]
48 */
49 protected static $connections = [];
50
51 /**
52 * @var array
53 */
54 protected $customDoctrineTypes = [
55 EnumType::TYPE => EnumType::class,
56 SetType::TYPE => SetType::class,
57 ];
58
59 /**
60 * Creates a connection object based on the specified table name.
61 *
62 * This is the official entry point to get a database connection to ensure
63 * that the mapping of table names to database connections is honored.
64 *
65 * @param string $tableName
66 * @return Connection
67 */
68 public function getConnectionForTable(string $tableName): Connection
69 {
70 if (empty($tableName)) {
71 throw new \UnexpectedValueException(
72 'ConnectionPool->getConnectionForTable() requires a table name to be provided.',
73 1459421719
74 );
75 }
76
77 $connectionName = self::DEFAULT_CONNECTION_NAME;
78 if (!empty($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName])) {
79 $connectionName = (string)$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName];
80 }
81
82 return $this->getConnectionByName($connectionName);
83 }
84
85 /**
86 * Creates a connection object based on the specified identifier.
87 *
88 * This method should only be used in edge cases. Use getConnectionForTable() so
89 * that the tablename<>databaseConnection mapping will be taken into account.
90 *
91 * @param string $connectionName
92 * @return Connection
93 * @throws \Doctrine\DBAL\DBALException
94 * @internal
95 */
96 public function getConnectionByName(string $connectionName): Connection
97 {
98 if (empty($connectionName)) {
99 throw new \UnexpectedValueException(
100 'ConnectionPool->getConnectionByName() requires a connection name to be provided.',
101 1459422125
102 );
103 }
104
105 if (isset(static::$connections[$connectionName])) {
106 return static::$connections[$connectionName];
107 }
108
109 if (empty($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][$connectionName])
110 || !is_array($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][$connectionName])
111 ) {
112 throw new \RuntimeException(
113 'The requested database connection named "' . $connectionName . '" has not been configured.',
114 1459422492
115 );
116 }
117
118 $connectionParams = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][$connectionName];
119 if (empty($connectionParams['wrapperClass'])) {
120 $connectionParams['wrapperClass'] = Connection::class;
121 }
122
123 if (!is_a($connectionParams['wrapperClass'], Connection::class, true)) {
124 throw new \UnexpectedValueException(
125 'The "wrapperClass" for the connection name "' . $connectionName .
126 '" needs to be a subclass of "' . Connection::class . '".',
127 1459422968
128 );
129 }
130
131 static::$connections[$connectionName] = $this->getDatabaseConnection($connectionParams);
132
133 return static::$connections[$connectionName];
134 }
135
136 /**
137 * Creates a connection object based on the specified parameters
138 *
139 * @param array $connectionParams
140 * @return Connection
141 */
142 protected function getDatabaseConnection(array $connectionParams): Connection
143 {
144 // Default to UTF-8 connection charset
145 if (empty($connectionParams['charset'])) {
146 $connectionParams['charset'] = 'utf-8';
147 }
148
149 // Force consistent handling of binary objects across datbase platforms
150 // MySQL returns strings by default, PostgreSQL streams.
151 if (strpos($connectionParams['driver'], 'pdo_') === 0) {
152 $connectionParams['driverOptions'][\PDO::ATTR_STRINGIFY_FETCHES] = true;
153 }
154
155 /** @var Connection $conn */
156 $conn = DriverManager::getConnection($connectionParams);
157 $conn->setFetchMode(\PDO::FETCH_ASSOC);
158 $conn->prepareConnection($connectionParams['initCommands'] ?? '');
159
160 // Register custom data types
161 foreach ($this->customDoctrineTypes as $type => $className) {
162 if (!Type::hasType($type)) {
163 Type::addType($type, $className);
164 }
165 }
166
167 // Register all custom data types in the type mapping
168 foreach ($this->customDoctrineTypes as $type => $className) {
169 $conn->getDatabasePlatform()->registerDoctrineTypeMapping($type, $type);
170 }
171
172 // Handler for building custom data type column definitions
173 // in the SchemaManager
174 $conn->getDatabasePlatform()->getEventManager()->addEventListener(
175 Events::onSchemaColumnDefinition,
176 GeneralUtility::makeInstance(SchemaColumnDefinitionListener::class)
177 );
178
179 // Handler for enhanced index definitions in the SchemaManager
180 $conn->getDatabasePlatform()->getEventManager()->addEventListener(
181 Events::onSchemaIndexDefinition,
182 GeneralUtility::makeInstance(SchemaIndexDefinitionListener::class)
183 );
184
185 // Handler for adding custom database platform options to ALTER TABLE
186 // requests in the SchemaManager
187 $conn->getDatabasePlatform()->getEventManager()->addEventListener(
188 Events::onSchemaAlterTable,
189 GeneralUtility::makeInstance(SchemaAlterTableListener::class)
190 );
191
192 return $conn;
193 }
194
195 /**
196 * Returns the connection specific query builder object that can be used to build
197 * complex SQL queries using and object oriented approach.
198 *
199 * @param string $tableName
200 * @return QueryBuilder
201 */
202 public function getQueryBuilderForTable(string $tableName): QueryBuilder
203 {
204 if (empty($tableName)) {
205 throw new \UnexpectedValueException(
206 'ConnectionPool->getQueryBuilderForTable() requires a connection name to be provided.',
207 1459423448
208 );
209 }
210
211 return $this->getConnectionForTable($tableName)->createQueryBuilder();
212 }
213
214 /**
215 * Returns an array containing the names of all currently configured connections.
216 *
217 * This method should only be used in edge cases. Use getConnectionForTable() so
218 * that the tablename<>databaseConnection mapping will be taken into account.
219 *
220 * @internal
221 * @return array
222 */
223 public function getConnectionNames(): array
224 {
225 return array_keys($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']);
226 }
227
228 /**
229 * Returns the list of custom Doctrine data types implemented by TYPO3.
230 * This method is needed by the Schema parser to register the types as it
231 * does not require a database connection and thus the types don't get
232 * registered automatically.
233 *
234 * @internal
235 * @return array
236 */
237 public function getCustomDoctrineTypes(): array
238 {
239 return $this->customDoctrineTypes;
240 }
241 }