Skip to content

Commit dd1a1d1

Browse files
committed
[TASK] Ensure decorated Connection->transactional() method works
TYPO3 decorates the `\Doctrine\DBAL\Connection` class with the custom implementation `\TYPO3\CMS\Core\Database\Connection` and uses is as the default connection managed by the `ConnectionPool`. The `transactional()` method is not used by TYPO3 directly yet while extension developers have a valid interest in a working state of the simplified transaction handling. This change adds some tests to ensure that simplified `transactional()` handling works and adds the method to the decoracted class along with a inline phpdocblock override to satisfy PHPStan revealed by the tests. Resolves: #104561 Releases: main, 13.4 Change-Id: If55b7d6e4f706971e810d44de0431f9343e305c2 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/87675 Tested-by: Christian Kuhn <lolli@schwarzbu.ch> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch> Tested-by: core-ci <typo3@b13.com> Reviewed-by: Stefan Bürk <stefan@buerk.tech> Reviewed-by: Benni Mack <benni@typo3.org> Tested-by: Stefan Bürk <stefan@buerk.tech>
1 parent d0c5ffb commit dd1a1d1

File tree

5 files changed

+196
-0
lines changed

5 files changed

+196
-0
lines changed

typo3/sysext/core/Classes/Database/Connection.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,4 +433,26 @@ public function getSchemaInformation(): SchemaInformation
433433
GeneralUtility::makeInstance(CacheManager::class)->getCache('database_schema')
434434
);
435435
}
436+
437+
/**
438+
* Executes a function in a transaction.
439+
*
440+
* The function gets passed this Connection instance as an (optional) parameter.
441+
*
442+
* If an exception occurs during execution of the function or transaction commit,
443+
* the transaction is rolled back and the exception re-thrown.
444+
*
445+
* @param \Closure(self):T $func The function to execute transactionally.
446+
*
447+
* @return T The value returned by $func
448+
*
449+
* @throws \Throwable
450+
*
451+
* @template T
452+
*/
453+
public function transactional(\Closure $func): mixed
454+
{
455+
/** @var \Closure(\Doctrine\DBAL\Connection):T $func Required to satisfy PHPStan. */
456+
return parent::transactional($func);
457+
}
436458
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
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+
namespace TYPO3\CMS\Core\Tests\Functional\Database\Connection;
19+
20+
use PHPUnit\Framework\Attributes\Test;
21+
use TYPO3\CMS\Core\Database\Connection;
22+
use TYPO3\CMS\Core\Database\ConnectionPool;
23+
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
24+
25+
final class TransactionalTest extends FunctionalTestCase
26+
{
27+
protected array $testExtensionsToLoad = [
28+
__DIR__ . '/../../Fixtures/Extensions/test_connection_transaction',
29+
];
30+
31+
#[Test]
32+
public function transactionInsertValidRecordIsPersisted(): void
33+
{
34+
$connection = (new ConnectionPool())->getConnectionForTable('tx_testconnectiontransaction');
35+
$expectedRecord = [
36+
'uid' => 1,
37+
'pid' => 0,
38+
'title' => 'Inserted record',
39+
];
40+
$record = $connection->transactional(function (Connection $connection): ?array {
41+
$lastInsertId = $connection->insert(
42+
'tx_testconnectiontransaction',
43+
[
44+
'pid' => 0,
45+
'title' => 'Inserted record',
46+
],
47+
);
48+
return $connection->select(
49+
['uid', 'pid', 'title'],
50+
'tx_testconnectiontransaction',
51+
[
52+
'uid' => $lastInsertId,
53+
]
54+
)->fetchAssociative() ?: null;
55+
});
56+
self::assertIsArray($record);
57+
self::assertSame($expectedRecord, $record);
58+
}
59+
60+
#[Test]
61+
public function transactionRecordsAreRolledBackWhenExceptionIsThrown(): void
62+
{
63+
$connection = (new ConnectionPool())->getConnectionForTable('tx_testconnectiontransaction');
64+
try {
65+
$connection->transactional(function (Connection $connection): array {
66+
// valid record
67+
$connection->insert(
68+
'tx_testconnectiontransaction',
69+
[
70+
'pid' => 0,
71+
'title' => 'Inserted record',
72+
],
73+
);
74+
// invalid record - throws exception
75+
$connection->insert(
76+
'tx_testconnectiontransaction',
77+
[
78+
'pid' => 0,
79+
'title' => 'Inserted record',
80+
'not_existing_field' => 0,
81+
],
82+
);
83+
$queryBuilder = $connection->createQueryBuilder();
84+
$queryBuilder->getRestrictions()->removeAll();
85+
return $queryBuilder
86+
->select('uid', 'pid', 'title')
87+
->from('tx_testconnectiontransaction')
88+
->executeQuery()
89+
->fetchAllAssociative();
90+
});
91+
} catch (\Throwable $t) {
92+
// Exception not checked here by intention. We want to check if database is still clean.
93+
}
94+
$queryBuilder = $connection->createQueryBuilder();
95+
$queryBuilder->getRestrictions()->removeAll();
96+
$records = $queryBuilder
97+
->select('uid', 'pid', 'title')
98+
->from('tx_testconnectiontransaction')
99+
->executeQuery()
100+
->fetchAllAssociative();
101+
self::assertSame([], $records);
102+
}
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
return [
4+
'ctrl' => [
5+
'title' => 'Connection Transaction TestTable',
6+
'label' => 'title',
7+
'tstamp' => 'tstamp',
8+
'crdate' => 'crdate',
9+
'languageField' => 'sys_language_uid',
10+
'transOrigPointerField' => 'l18n_parent',
11+
'transOrigDiffSourceField' => 'l18n_diffsource',
12+
'translationSource' => 'l10n_source',
13+
'sortby' => 'sorting',
14+
'delete' => 'deleted',
15+
'enablecolumns' => [
16+
'disabled' => 'hidden',
17+
],
18+
'versioningWS' => true,
19+
'origUid' => 't3_origuid',
20+
'security' => [
21+
'ignorePageTypeRestriction' => true,
22+
],
23+
],
24+
'columns' => [
25+
'title' => [
26+
'exclude' => true,
27+
'l10n_mode' => 'prefixLangTitle',
28+
'label' => 'Title',
29+
'config' => [
30+
'type' => 'input',
31+
'size' => 30,
32+
'required' => true,
33+
],
34+
],
35+
],
36+
];
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "typo3tests/test-connection-transaction",
3+
"type": "typo3-cms-extension",
4+
"description": "This extension adds example tt_content CTypes",
5+
"license": "GPL-2.0-or-later",
6+
"require": {
7+
"typo3/cms-core": "14.0.*@dev"
8+
},
9+
"extra": {
10+
"typo3/cms": {
11+
"extension-key": "test_connection_transaction"
12+
}
13+
}
14+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
$EM_CONF[$_EXTKEY] = [
6+
'title' => 'Provides configuration to test connection transaction',
7+
'description' => 'Provides configuration to test connection transaction',
8+
'category' => 'example',
9+
'version' => '14.0.0',
10+
'state' => 'stable',
11+
'author' => 'Stefan Bürk',
12+
'author_email' => 'stefan@buerk.tech',
13+
'author_company' => '',
14+
'constraints' => [
15+
'depends' => [
16+
'typo3' => '14.0.0',
17+
],
18+
'conflicts' => [],
19+
'suggests' => [],
20+
],
21+
];

0 commit comments

Comments
 (0)