[FEATURE] Doctrine: Implement SchemaMigrationService
[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\Configuration;
19 use Doctrine\DBAL\DriverManager;
20 use Doctrine\DBAL\Events;
21 use Doctrine\DBAL\Types\Type;
22 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
23 use TYPO3\CMS\Core\Database\Schema\SchemaColumnDefinitionListener;
24 use TYPO3\CMS\Core\Database\Schema\Types\EnumType;
25 use TYPO3\CMS\Core\Database\Schema\Types\SetType;
26 use TYPO3\CMS\Core\Utility\GeneralUtility;
27
28 /**
29 * Manager that handles opening/retrieving database connections.
30 *
31 * It's a facade to the actual Doctrine DBAL DriverManager that implements TYPO3
32 * specific functionality like mapping individual tables to different database
33 * connections.
34 *
35 * getConnectionForTable() is the only supported way to get a connection that
36 * honors the table mapping configuration.
37 */
38 class ConnectionPool
39 {
40 /**
41 * @var string
42 */
43 const DEFAULT_CONNECTION_NAME = 'Default';
44
45 /**
46 * @var Connection[]
47 */
48 protected static $connections = [];
49
50 /**
51 * @var array
52 */
53 protected $customDoctrineTypes = [
54 EnumType::TYPE => EnumType::class,
55 SetType::TYPE => SetType::class,
56 ];
57
58 /**
59 * Creates a connection object based on the specified table name.
60 *
61 * This is the official entry point to get a database connection to ensure
62 * that the mapping of table names to database connections is honored.
63 *
64 * @param string $tableName
65 * @return Connection
66 */
67 public function getConnectionForTable(string $tableName): Connection
68 {
69 if (empty($tableName)) {
70 throw new \UnexpectedValueException(
71 'ConnectionPool->getConnectionForTable() requires a table name to be provided.',
72 1459421719
73 );
74 }
75
76 $connectionName = self::DEFAULT_CONNECTION_NAME;
77 if (!empty($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName])) {
78 $connectionName = (string)$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName];
79 }
80
81 return $this->getConnectionByName($connectionName);
82 }
83
84 /**
85 * Creates a connection object based on the specified identifier.
86 *
87 * This method should only be used in edge cases. Use getConnectionForTable() so
88 * that the tablename<>databaseConnection mapping will be taken into account.
89 *
90 * @param string $connectionName
91 * @return Connection
92 * @throws \Doctrine\DBAL\DBALException
93 * @internal
94 */
95 public function getConnectionByName(string $connectionName): Connection
96 {
97 if (empty($connectionName)) {
98 throw new \UnexpectedValueException(
99 'ConnectionPool->getConnectionByName() requires a connection name to be provided.',
100 1459422125
101 );
102 }
103
104 if (isset(static::$connections[$connectionName])) {
105 return static::$connections[$connectionName];
106 }
107
108 if (empty($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][$connectionName])
109 || !is_array($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][$connectionName])
110 ) {
111 throw new \RuntimeException(
112 'The requested database connection named "' . $connectionName . '" has not been configured.',
113 1459422492
114 );
115 }
116
117 $connectionParams = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][$connectionName];
118 if (empty($connectionParams['wrapperClass'])) {
119 $connectionParams['wrapperClass'] = Connection::class;
120 }
121
122 if (!is_a($connectionParams['wrapperClass'], Connection::class, true)) {
123 throw new \UnexpectedValueException(
124 'The "wrapperClass" for the connection name "' . $connectionName .
125 '" needs to be a subclass of "' . Connection::class . '".',
126 1459422968
127 );
128 }
129
130 static::$connections[$connectionName] = $this->getDatabaseConnection($connectionParams);
131
132 return static::$connections[$connectionName];
133 }
134
135 /**
136 * Creates a connection object based on the specified parameters
137 *
138 * @param array $connectionParams
139 * @return Connection
140 */
141 protected function getDatabaseConnection(array $connectionParams): Connection
142 {
143 // Default to UTF-8 connection charset
144 if (empty($connectionParams['charset'])) {
145 $connectionParams['charset'] = 'utf-8';
146 }
147
148 /** @var Connection $conn */
149 $conn = DriverManager::getConnection($connectionParams);
150 $conn->setFetchMode(\PDO::FETCH_ASSOC);
151 $conn->prepareConnection($connectionParams['initCommands'] ?? '');
152
153 // Register custom data types
154 foreach ($this->customDoctrineTypes as $type => $className) {
155 if (!Type::hasType($type)) {
156 Type::addType($type, $className);
157 }
158 }
159
160 // Register all custom data types in the type mapping
161 foreach ($this->customDoctrineTypes as $type => $className) {
162 $conn->getDatabasePlatform()->registerDoctrineTypeMapping($type, $type);
163 }
164
165 // Handler for building custom data type column definitions
166 // in the SchemaManager
167 $conn->getDatabasePlatform()->getEventManager()->addEventListener(
168 Events::onSchemaColumnDefinition,
169 GeneralUtility::makeInstance(SchemaColumnDefinitionListener::class)
170 );
171
172 return $conn;
173 }
174
175 /**
176 * Returns the connection specific query builder object that can be used to build
177 * complex SQL queries using and object oriented approach.
178 *
179 * @param string $tableName
180 * @return QueryBuilder
181 */
182 public function getQueryBuilderForTable(string $tableName): QueryBuilder
183 {
184 if (empty($tableName)) {
185 throw new \UnexpectedValueException(
186 'ConnectionPool->getQueryBuilderForTable() requires a connection name to be provided.',
187 1459423448
188 );
189 }
190
191 return $this->getConnectionForTable($tableName)->createQueryBuilder();
192 }
193
194 /**
195 * Returns an array containing the names of all currently configured connections.
196 *
197 * This method should only be used in edge cases. Use getConnectionForTable() so
198 * that the tablename<>databaseConnection mapping will be taken into account.
199 *
200 * @internal
201 * @return array
202 */
203 public function getConnectionNames(): array
204 {
205 return array_keys($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']);
206 }
207
208 /**
209 * Returns the list of custom Doctrine data types implemented by TYPO3.
210 * This method is needed by the Schema parser to register the types as it
211 * does not require a database connection and thus the types don't get
212 * registered automatically.
213 *
214 * @internal
215 * @return array
216 */
217 public function getCustomDoctrineTypes(): array
218 {
219 return $this->customDoctrineTypes;
220 }
221 }