ConnectionPool.php 9.5 KB
Newer Older
1
<?php
2

3
declare(strict_types=1);
4
5
6
7
8
9
10
11
12
13
14
15
16
17

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

18
19
namespace TYPO3\CMS\Core\Database;

20
use Doctrine\DBAL\DriverManager;
21
22
use Doctrine\DBAL\Events;
use Doctrine\DBAL\Types\Type;
23
24
25
26
use TYPO3\CMS\Core\Database\Driver\PDOMySql\Driver as PDOMySqlDriver;
use TYPO3\CMS\Core\Database\Driver\PDOPgSql\Driver as PDOPgSqlDriver;
use TYPO3\CMS\Core\Database\Driver\PDOSqlite\Driver as PDOSqliteDriver;
use TYPO3\CMS\Core\Database\Driver\PDOSqlsrv\Driver as PDOSqlsrvDriver;
27
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
28
29
30
31
32
33
use TYPO3\CMS\Core\Database\Schema\EventListener\SchemaAlterTableListener;
use TYPO3\CMS\Core\Database\Schema\EventListener\SchemaColumnDefinitionListener;
use TYPO3\CMS\Core\Database\Schema\EventListener\SchemaIndexDefinitionListener;
use TYPO3\CMS\Core\Database\Schema\Types\EnumType;
use TYPO3\CMS\Core\Database\Schema\Types\SetType;
use TYPO3\CMS\Core\Utility\GeneralUtility;
34
35
36
37
38
39
40
41

/**
 * Manager that handles opening/retrieving database connections.
 *
 * It's a facade to the actual Doctrine DBAL DriverManager that implements TYPO3
 * specific functionality like mapping individual tables to different database
 * connections.
 *
42
 * getConnectionForTable() is the only supported way to get a connection that
43
44
45
46
47
48
49
50
51
52
53
54
55
56
 * honors the table mapping configuration.
 */
class ConnectionPool
{
    /**
     * @var string
     */
    const DEFAULT_CONNECTION_NAME = 'Default';

    /**
     * @var Connection[]
     */
    protected static $connections = [];

57
58
59
60
61
62
63
64
    /**
     * @var array
     */
    protected $customDoctrineTypes = [
        EnumType::TYPE => EnumType::class,
        SetType::TYPE => SetType::class,
    ];

65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
    /**
     * List of custom drivers and their mappings to the driver classes.
     *
     * @var string[]
     */
    protected static $driverMap = [
        'pdo_mysql' => PDOMySqlDriver::class,
        'pdo_sqlite' => PDOSqliteDriver::class,
        'pdo_pgsql' => PDOPgSqlDriver::class,
        'pdo_sqlsrv' => PDOSqlsrvDriver::class,
        // TODO: not supported yet, need to be checked later
//        'pdo_oci' => PDOOCIDriver::class,
//        'drizzle_pdo_mysql' => DrizzlePDOMySQLDriver::class,
    ];

80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
    /**
     * Creates a connection object based on the specified table name.
     *
     * This is the official entry point to get a database connection to ensure
     * that the mapping of table names to database connections is honored.
     *
     * @param string $tableName
     * @return Connection
     */
    public function getConnectionForTable(string $tableName): Connection
    {
        if (empty($tableName)) {
            throw new \UnexpectedValueException(
                'ConnectionPool->getConnectionForTable() requires a table name to be provided.',
                1459421719
            );
        }

98
        $connectionName = self::DEFAULT_CONNECTION_NAME;
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
        if (!empty($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName])) {
            $connectionName = (string)$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName];
        }

        return $this->getConnectionByName($connectionName);
    }

    /**
     * Creates a connection object based on the specified identifier.
     *
     * This method should only be used in edge cases. Use getConnectionForTable() so
     * that the tablename<>databaseConnection mapping will be taken into account.
     *
     * @param string $connectionName
     * @return Connection
114
     * @throws \Doctrine\DBAL\Exception
115
116
117
118
119
120
121
122
123
124
125
126
127
128
     */
    public function getConnectionByName(string $connectionName): Connection
    {
        if (empty($connectionName)) {
            throw new \UnexpectedValueException(
                'ConnectionPool->getConnectionByName() requires a connection name to be provided.',
                1459422125
            );
        }

        if (isset(static::$connections[$connectionName])) {
            return static::$connections[$connectionName];
        }

129
130
        $connectionParams = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][$connectionName] ?? [];
        if (empty($connectionParams)) {
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
            throw new \RuntimeException(
                'The requested database connection named "' . $connectionName . '" has not been configured.',
                1459422492
            );
        }

        if (empty($connectionParams['wrapperClass'])) {
            $connectionParams['wrapperClass'] = Connection::class;
        }

        if (!is_a($connectionParams['wrapperClass'], Connection::class, true)) {
            throw new \UnexpectedValueException(
                'The "wrapperClass" for the connection name "' . $connectionName .
                '" needs to be a subclass of "' . Connection::class . '".',
                1459422968
            );
        }

        static::$connections[$connectionName] = $this->getDatabaseConnection($connectionParams);

        return static::$connections[$connectionName];
    }

154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
    /**
     * Map custom driver class for certain driver
     *
     * @param array $connectionParams
     * @return array
     */
    protected function mapCustomDriver(array $connectionParams): array
    {
        // if no custom driver is provided, map TYPO3 specific drivers
        if (!isset($connectionParams['driverClass']) && isset(static::$driverMap[$connectionParams['driver']])) {
            $connectionParams['driverClass'] = static::$driverMap[$connectionParams['driver']];
        }

        return $connectionParams;
    }

170
171
172
173
174
175
176
177
178
179
    /**
     * Creates a connection object based on the specified parameters
     *
     * @param array $connectionParams
     * @return Connection
     */
    protected function getDatabaseConnection(array $connectionParams): Connection
    {
        // Default to UTF-8 connection charset
        if (empty($connectionParams['charset'])) {
180
            $connectionParams['charset'] = 'utf8';
181
        }
182

183
        $connectionParams = $this->mapCustomDriver($connectionParams);
184

185
186
187
188
189
        /** @var Connection $conn */
        $conn = DriverManager::getConnection($connectionParams);
        $conn->setFetchMode(\PDO::FETCH_ASSOC);
        $conn->prepareConnection($connectionParams['initCommands'] ?? '');

190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
        // Register custom data types
        foreach ($this->customDoctrineTypes as $type => $className) {
            if (!Type::hasType($type)) {
                Type::addType($type, $className);
            }
        }

        // Register all custom data types in the type mapping
        foreach ($this->customDoctrineTypes as $type => $className) {
            $conn->getDatabasePlatform()->registerDoctrineTypeMapping($type, $type);
        }

        // Handler for building custom data type column definitions
        // in the SchemaManager
        $conn->getDatabasePlatform()->getEventManager()->addEventListener(
            Events::onSchemaColumnDefinition,
            GeneralUtility::makeInstance(SchemaColumnDefinitionListener::class)
        );

        // Handler for enhanced index definitions in the SchemaManager
        $conn->getDatabasePlatform()->getEventManager()->addEventListener(
            Events::onSchemaIndexDefinition,
            GeneralUtility::makeInstance(SchemaIndexDefinitionListener::class)
        );

        // Handler for adding custom database platform options to ALTER TABLE
        // requests in the SchemaManager
        $conn->getDatabasePlatform()->getEventManager()->addEventListener(
            Events::onSchemaAlterTable,
            GeneralUtility::makeInstance(SchemaAlterTableListener::class)
        );

222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
        return $conn;
    }

    /**
     * Returns the connection specific query builder object that can be used to build
     * complex SQL queries using and object oriented approach.
     *
     * @param string $tableName
     * @return QueryBuilder
     */
    public function getQueryBuilderForTable(string $tableName): QueryBuilder
    {
        if (empty($tableName)) {
            throw new \UnexpectedValueException(
                'ConnectionPool->getQueryBuilderForTable() requires a connection name to be provided.',
                1459423448
            );
        }

        return $this->getConnectionForTable($tableName)->createQueryBuilder();
    }
243
244
245
246
247
248
249
250
251
252
253
254
255
256

    /**
     * Returns an array containing the names of all currently configured connections.
     *
     * This method should only be used in edge cases. Use getConnectionForTable() so
     * that the tablename<>databaseConnection mapping will be taken into account.
     *
     * @internal
     * @return array
     */
    public function getConnectionNames(): array
    {
        return array_keys($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']);
    }
257
258
259
260
261
262
263
264
265
266
267
268
269
270

    /**
     * Returns the list of custom Doctrine data types implemented by TYPO3.
     * This method is needed by the Schema parser to register the types as it
     * does not require a database connection and thus the types don't get
     * registered automatically.
     *
     * @internal
     * @return array
     */
    public function getCustomDoctrineTypes(): array
    {
        return $this->customDoctrineTypes;
    }
271
272

    /**
273
274
     * Reset internal list of connections.
     * Currently primarily used in functional tests to close connections and start
275
276
277
278
279
280
     * new ones in between single tests.
     */
    public function resetConnections(): void
    {
        static::$connections = [];
    }
281
}