TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,505 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Database;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Configuration as DoctrineConfiguration;
|
||||
use Doctrine\DBAL\Connection as DoctrineConnection;
|
||||
use Doctrine\DBAL\Connection\StaticServerVersionProvider;
|
||||
use Doctrine\DBAL\Driver;
|
||||
use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Doctrine\DBAL\Platforms\MariaDBPlatform as DoctrineMariaDBPlatform;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform as DoctrineMySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform as DoctrinePostgreSQLPlatform;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\DBAL\ServerVersionProvider;
|
||||
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||
use TYPO3\CMS\Core\Database\Platform\PlatformInformation;
|
||||
use TYPO3\CMS\Core\Database\Query\BulkInsertQuery;
|
||||
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
|
||||
use TYPO3\CMS\Core\Database\Schema\SchemaInformation;
|
||||
use TYPO3\CMS\Core\Package\Cache\PackageDependentCacheIdentifier;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Note: This is a fragile base class. It owns no contract of its own and specializes Doctrine's
|
||||
* concrete Connection by overriding quoting, insert/update/delete and connect behavior.
|
||||
* Subclassing is forced on us though: Doctrine's DriverManager only accepts a `wrapperClass` that
|
||||
* is a Doctrine\DBAL\Connection, and DBAL exposes no high-level connection interface to compose
|
||||
* against, so a real composition decorator is not possible.
|
||||
*
|
||||
* Integrators may in turn point the `wrapperClass` connection option at a subclass of this
|
||||
* class, but because of the fragility described above such subclasses are not covered by the
|
||||
* backward-compatibility promise.
|
||||
*/
|
||||
class Connection extends DoctrineConnection
|
||||
{
|
||||
/**
|
||||
* Represents a SQL NULL data type.
|
||||
*/
|
||||
public const PARAM_NULL = ParameterType::NULL;
|
||||
|
||||
/**
|
||||
* Represents a SQL INTEGER data type.
|
||||
*/
|
||||
public const PARAM_INT = ParameterType::INTEGER;
|
||||
|
||||
/**
|
||||
* Represents a SQL CHAR, VARCHAR data type.
|
||||
*/
|
||||
public const PARAM_STR = ParameterType::STRING;
|
||||
|
||||
/**
|
||||
* Represents a SQL large object data type.
|
||||
*/
|
||||
public const PARAM_LOB = ParameterType::LARGE_OBJECT;
|
||||
|
||||
/**
|
||||
* Represents a boolean data type.
|
||||
*/
|
||||
public const PARAM_BOOL = ParameterType::BOOLEAN;
|
||||
|
||||
/**
|
||||
* Represents an array of integer values.
|
||||
*/
|
||||
public const PARAM_INT_ARRAY = ArrayParameterType::INTEGER;
|
||||
|
||||
/**
|
||||
* Represents an array of string values.
|
||||
*/
|
||||
public const PARAM_STR_ARRAY = ArrayParameterType::STRING;
|
||||
|
||||
private ExpressionBuilder $expressionBuilder;
|
||||
private array $prepareConnectionCommands = [];
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the Connection class.
|
||||
*
|
||||
* @param array $params The connection parameters.
|
||||
* @param Driver $driver The driver to use.
|
||||
* @param DoctrineConfiguration|null $config The configuration, optional.
|
||||
*/
|
||||
public function __construct(#[\SensitiveParameter] array $params, Driver $driver, ?DoctrineConfiguration $config = null)
|
||||
{
|
||||
parent::__construct($params, $driver, $config);
|
||||
if (!$config instanceof Configuration) {
|
||||
throw new \InvalidArgumentException('TYPO3 Connection expects the custom TYPO3 Configuration object to be given', 1782369775);
|
||||
}
|
||||
$this->expressionBuilder = GeneralUtility::makeInstance(ExpressionBuilder::class, $this, $config->getContainer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the DatabasePlatform for the connection and initializes custom types and event listeners.
|
||||
*/
|
||||
protected function connect(): ConnectionInterface
|
||||
{
|
||||
if ($this->_conn !== null) {
|
||||
return $this->_conn;
|
||||
}
|
||||
// Early return if the connection is already open and custom setup has been done.
|
||||
$connection = parent::connect();
|
||||
foreach ($this->prepareConnectionCommands as $command) {
|
||||
$this->executeStatement($command);
|
||||
}
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of a SQL query builder.
|
||||
*/
|
||||
public function createQueryBuilder(): QueryBuilder
|
||||
{
|
||||
return GeneralUtility::makeInstance(QueryBuilder::class, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes a string so it can be safely used as a table or column name, even if
|
||||
* it is a reserved name.
|
||||
* EXAMPLE: tableName.fieldName => "tableName"."fieldName"
|
||||
*
|
||||
* Delimiting style depends on the underlying database platform that is being used.
|
||||
*
|
||||
* Note that this does not call the parent implementation, because both
|
||||
* Doctrine DBAL `Connection::quoteIdentifier()` and `AbstractPlatform::quoteIdentifier()`
|
||||
* are deprecated and will be removed with Doctrine DBAL 5.0. The quoting is done
|
||||
* here instead, identical to the removed implementation.
|
||||
*
|
||||
* @param string $identifier The name to be quoted.
|
||||
* @return string The quoted name.
|
||||
*/
|
||||
public function quoteIdentifier(string $identifier): string
|
||||
{
|
||||
if ($identifier === '*') {
|
||||
return $identifier;
|
||||
}
|
||||
$platform = $this->getDatabasePlatform();
|
||||
if (!str_contains($identifier, '.')) {
|
||||
return $platform->quoteSingleIdentifier($identifier);
|
||||
}
|
||||
return implode('.', array_map($platform->quoteSingleIdentifier(...), explode('.', $identifier)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes an array of column names, so it can be safely used, even if the name is a reserved name.
|
||||
* Delimiting style depends on the underlying database platform that is being used.
|
||||
*/
|
||||
public function quoteIdentifiers(array $input): array
|
||||
{
|
||||
return array_map($this->quoteIdentifier(...), $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes an associative array of column-value so the column names can be safely used, even
|
||||
* if the name is a reserved name.
|
||||
* Delimiting style depends on the underlying database platform that is being used.
|
||||
*/
|
||||
public function quoteColumnValuePairs(array $input): array
|
||||
{
|
||||
return array_combine($this->quoteIdentifiers(array_keys($input)), array_values($input));
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if the column types are specified by column name or using
|
||||
* positional information. In the first case quote the field names
|
||||
* accordingly.
|
||||
*/
|
||||
protected function quoteColumnTypes(array $input): array
|
||||
{
|
||||
if (!is_string(key($input))) {
|
||||
return $input;
|
||||
}
|
||||
return $this->quoteColumnValuePairs($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes like wildcards for given string value.
|
||||
*
|
||||
* @param string $value The value to be quoted.
|
||||
* @return string The quoted value.
|
||||
*/
|
||||
public function escapeLikeWildcards(string $value): string
|
||||
{
|
||||
return addcslashes($value, '_%');
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a table row with specified data.
|
||||
*
|
||||
* All SQL identifiers are expected to be unquoted and will be quoted when building the query.
|
||||
*
|
||||
* @param string $tableName The name of the table to insert data into.
|
||||
* @param array $data An associative array containing column-value pairs.
|
||||
* @param array $types Types of the inserted data.
|
||||
* @return int The number of affected rows.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function insert(string $tableName, array $data, array $types = []): int
|
||||
{
|
||||
$this->ensureDatabaseValueTypes($tableName, $data, $types);
|
||||
return parent::insert(
|
||||
$this->quoteIdentifier($tableName),
|
||||
$this->quoteColumnValuePairs($data),
|
||||
$this->quoteColumnTypes($types)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk inserts table rows with specified data.
|
||||
* All SQL identifiers are expected to be unquoted and will be quoted when building the query.
|
||||
*
|
||||
* @param string $tableName The name of the table to insert data into.
|
||||
* @param array $data An array containing associative arrays of column-value pairs or just the values to be inserted.
|
||||
* @param array $columns An array containing the column names of the data which should be inserted.
|
||||
* @param array $types Types of the inserted data.
|
||||
* @return int The number of affected rows.
|
||||
*/
|
||||
public function bulkInsert(string $tableName, array $data, array $columns = [], array $types = []): int
|
||||
{
|
||||
$totalAffectedRows = 0;
|
||||
$columnLength = $columns !== [] ? count($columns) : 1000;
|
||||
$maxBindParameters = PlatformInformation::getMaxBindParameters($this->getDatabasePlatform());
|
||||
$maxChunkSize = (int)(($maxBindParameters / $columnLength) / 2);
|
||||
$chunks = array_chunk($data, $maxChunkSize);
|
||||
foreach ($chunks as $chunk) {
|
||||
$query = GeneralUtility::makeInstance(BulkInsertQuery::class, $this, $tableName, $columns);
|
||||
foreach ($chunk as $values) {
|
||||
$this->ensureDatabaseValueTypes($tableName, $values, $types);
|
||||
$query->addValues($values, $types);
|
||||
}
|
||||
$totalAffectedRows += $query->execute();
|
||||
}
|
||||
return $totalAffectedRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an SQL SELECT statement on a table.
|
||||
* All SQL identifiers are expected to be unquoted and will be quoted when building the query.
|
||||
*
|
||||
* @param string[] $columns The columns of the table which to select.
|
||||
* @param string $tableName The name of the table on which to select.
|
||||
* @param array $identifiers The selection criteria. An associative array containing column-value pairs.
|
||||
* @param string[] $groupBy The columns to group the results by.
|
||||
* @param array $orderBy Associative array of column name/sort directions pairs.
|
||||
* @param int $limit The maximum number of rows to return.
|
||||
* @param int $offset The first result row to select (when used with limit)
|
||||
* @return Result The executed statement.
|
||||
*/
|
||||
public function select(
|
||||
array $columns,
|
||||
string $tableName,
|
||||
array $identifiers = [],
|
||||
array $groupBy = [],
|
||||
array $orderBy = [],
|
||||
int $limit = 0,
|
||||
int $offset = 0
|
||||
) {
|
||||
$query = $this->createQueryBuilder();
|
||||
$query->select(...$columns)->from($tableName);
|
||||
foreach ($identifiers as $identifier => $value) {
|
||||
$query->andWhere($query->expr()->eq($identifier, $query->createNamedParameter($value)));
|
||||
}
|
||||
foreach ($orderBy as $fieldName => $order) {
|
||||
$query->addOrderBy($fieldName, $order);
|
||||
}
|
||||
if (!empty($groupBy)) {
|
||||
$query->groupBy(...$groupBy);
|
||||
}
|
||||
if ($limit > 0) {
|
||||
$query->setMaxResults($limit);
|
||||
$query->setFirstResult($offset);
|
||||
}
|
||||
return $query->executeQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an SQL UPDATE statement on a table.
|
||||
* All SQL identifiers are expected to be unquoted and will be quoted when building the query.
|
||||
*
|
||||
* @param string $tableName The name of the table to update.
|
||||
* @param array $data An associative array containing column-value pairs.
|
||||
* @param array $identifier The update criteria. An associative array containing column-value pairs.
|
||||
* @param array $types Types of the merged $data and $identifier arrays in that order.
|
||||
* @return int The number of affected rows.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function update(string $tableName, array $data, array $identifier = [], array $types = []): int
|
||||
{
|
||||
$this->ensureDatabaseValueTypes($tableName, $data, $types);
|
||||
return parent::update(
|
||||
$this->quoteIdentifier($tableName),
|
||||
$this->quoteColumnValuePairs($data),
|
||||
$this->quoteColumnValuePairs($identifier),
|
||||
$this->quoteColumnTypes($types)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an SQL DELETE statement on a table.
|
||||
* All SQL identifiers are expected to be unquoted and will be quoted when building the query.
|
||||
*
|
||||
* @param string $tableName The name of the table on which to delete.
|
||||
* @param array $identifier The deletion criteria. An associative array containing column-value pairs.
|
||||
* @param array $types The types of identifiers.
|
||||
* @return int The number of affected rows.
|
||||
*/
|
||||
public function delete(string $tableName, array $identifier = [], array $types = []): int
|
||||
{
|
||||
return parent::delete(
|
||||
$this->quoteIdentifier($tableName),
|
||||
$this->quoteColumnValuePairs($identifier),
|
||||
$this->quoteColumnTypes($types)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an SQL TRUNCATE statement on a table.
|
||||
* All SQL identifiers are expected to be unquoted and will be quoted when building the query.
|
||||
*
|
||||
* @param string $tableName The name of the table to truncate.
|
||||
* @param bool $cascade Not supported on many platforms but would cascade the truncate by following foreign keys.
|
||||
* @return int The number of affected rows. For a truncate this is unreliable as there is no meaningful information.
|
||||
*/
|
||||
public function truncate(string $tableName, bool $cascade = false): int
|
||||
{
|
||||
return $this->executeStatement(
|
||||
$this->getDatabasePlatform()->getTruncateTableSQL(
|
||||
$this->quoteIdentifier($tableName),
|
||||
$cascade
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an SQL SELECT COUNT() statement on a table and returns the count result.
|
||||
*
|
||||
* @param string $item The column/expression of the table which to count
|
||||
* @param string $tableName The name of the table on which to count.
|
||||
* @param array $identifiers The selection criteria. An associative array containing column-value pairs.
|
||||
* @return int The number of rows counted
|
||||
*/
|
||||
public function count(string $item, string $tableName, array $identifiers): int
|
||||
{
|
||||
$query = $this->createQueryBuilder();
|
||||
$query->count($item)->from($tableName);
|
||||
foreach ($identifiers as $identifier => $value) {
|
||||
$query->andWhere($query->expr()->eq($identifier, $query->createNamedParameter($value)));
|
||||
}
|
||||
return (int)$query->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version of the current platform if applicable, containing the platform as prefix.
|
||||
*
|
||||
* If no version information is available only the platform name will be shown.
|
||||
* If the platform name is unknown or unsupported the driver name will be shown.
|
||||
*
|
||||
* @internal only and not part of public API.
|
||||
*/
|
||||
public function getPlatformServerVersion(): string
|
||||
{
|
||||
$platform = $this->getDatabasePlatform();
|
||||
$version = trim($this->typo3_getServerVersionProvider()->getServerVersion());
|
||||
if ($version !== '') {
|
||||
$version = ' ' . $version;
|
||||
}
|
||||
return match (true) {
|
||||
// @todo Check if we should use 'MariaDB' now directly instead of MySQL as an alias.
|
||||
$platform instanceof DoctrineMariaDBPlatform => 'MySQL' . $version,
|
||||
$platform instanceof DoctrineMySQLPlatform => 'MySQL' . $version,
|
||||
$platform instanceof DoctrinePostgreSQLPlatform => 'PostgreSQL' . $version,
|
||||
default => (str_replace('Platform', '', array_reverse(explode('\\', $platform::class))[0])) . $version,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute commands after initializing a new connection.
|
||||
*/
|
||||
public function prepareConnection(string $commands): void
|
||||
{
|
||||
if (empty($commands)) {
|
||||
return;
|
||||
}
|
||||
$this->prepareConnectionCommands = GeneralUtility::trimExplode(
|
||||
LF,
|
||||
str_replace(
|
||||
'\' . LF . \'',
|
||||
LF,
|
||||
$commands
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the last inserted row.
|
||||
* If the underlying driver does not support identity columns, an exception is thrown.
|
||||
*
|
||||
* @return numeric-string
|
||||
*/
|
||||
public function lastInsertId(): string
|
||||
{
|
||||
return (string)parent::lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ExpressionBuilder for the connection.
|
||||
*/
|
||||
public function getExpressionBuilder(): ExpressionBuilder
|
||||
{
|
||||
return $this->expressionBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method ensures that data values a properly converted to their database equivalent.
|
||||
* Additionally, it adds the proper types to the type-array, if this has no manual preset types.
|
||||
* Note: Types are *only* added if not given externally.
|
||||
*
|
||||
* @internal Should be private, but mocked in tests currently.
|
||||
*/
|
||||
protected function ensureDatabaseValueTypes(string $tableName, array &$data, array &$types): void
|
||||
{
|
||||
$tableInfo = $this->getSchemaInformation()->getTableInfo($tableName);
|
||||
array_walk($data, function (mixed &$value, string $key) use ($tableInfo, &$types): void {
|
||||
// Use database schema field type in case no Type or ParameterType has been provided manually
|
||||
// for field `$key`, falling back to ParameterType::STRING in case field does not exists in
|
||||
// the schema, which is the default ParameterType used by doctrine anyway.
|
||||
if (!isset($types[$key]) && $tableInfo->hasColumnInfo($key)) {
|
||||
$types[$key] = $tableInfo->getColumnInfo($key)->getType();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal May vanish anytime, currently used core-internal at some places.
|
||||
*/
|
||||
public function getSchemaInformation(): SchemaInformation
|
||||
{
|
||||
return new SchemaInformation(
|
||||
$this,
|
||||
GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime'),
|
||||
GeneralUtility::makeInstance(CacheManager::class)->getCache('database_schema'),
|
||||
GeneralUtility::makeInstance(PackageDependentCacheIdentifier::class),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a function in a transaction.
|
||||
*
|
||||
* The function gets passed this Connection instance as an (optional) parameter.
|
||||
*
|
||||
* If an exception occurs during execution of the function or transaction commit,
|
||||
* the transaction is rolled back and the exception re-thrown.
|
||||
*
|
||||
* @param \Closure(self):T $func The function to execute transactionally.
|
||||
* @return T The value returned by $func
|
||||
* @throws \Throwable
|
||||
* @template T
|
||||
*/
|
||||
public function transactional(\Closure $func): mixed
|
||||
{
|
||||
/** @var \Closure(DoctrineConnection):T $func Required to satisfy PHPStan. */
|
||||
return parent::transactional($func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the suitable `ServerVersionProvider`, which could be the connection itself or
|
||||
* a `StaticServerVersionProvider` based on either of following configuration values:
|
||||
*
|
||||
* - $params['serverVersion']
|
||||
* - $params['primary']['serverVersion']
|
||||
*
|
||||
* This is an extract from {@see \Doctrine\DBAL\Connection::getDatabasePlatform()} and handled as internal for
|
||||
* now and will be tried to provide upstream making it API and is the reason why it is a prefixed method.
|
||||
*
|
||||
* It's currently only used in internal {@see self::getPlatformServerVersion()}.
|
||||
*
|
||||
* @internal only and not part of public API.
|
||||
*/
|
||||
protected function typo3_getServerVersionProvider(): ServerVersionProvider
|
||||
{
|
||||
$params = $this->getParams();
|
||||
return match (true) {
|
||||
isset($params['serverVersion']) => new StaticServerVersionProvider($params['serverVersion']),
|
||||
isset($params['primary']['serverVersion']) => new StaticServerVersionProvider($params['primary']['serverVersion']),
|
||||
default => $this,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user