Description
- Laravel Version: 8.x
- PHP Version: 8.0RC5
- Database Driver & Version: MariaDB
Description:
As this is a difficult subject I will try my best to explain it properly, it is an issue that needs to be addressed properly.
PHP 8.0 changes
In PHP 8.0, the pdo transaction logic was rewritten to some extend.
When issuing a PDO::rollback(), this will now throw a PDOException if the transaction was already closed.
* I was not able to find any official documentation on this yet, but was able to confirm this with the different PHP versions
Background Implicit Commits with Transactions
MySQL has a thing called implicit commits, to demonstrate what this does I have created the following example. I have used MariaDB as this has access to the in_transaction
session var to demonstrate the working.
create table Table1
(
name varchar(10) null
);
START TRANSACTION;
SHOW VARIABLES WHERE Variable_name = 'in_transaction' # Returns 1
INSERT INTO Table1 (name) values ('before');
SHOW VARIABLES WHERE Variable_name = 'in_transaction' # Returns 1
create table Table2
(
name varchar(10) null
);
SHOW VARIABLES WHERE Variable_name = 'in_transaction' # Returns 0
INSERT INTO Table1 (name) values ('after');
SHOW VARIABLES WHERE Variable_name = 'in_transaction' # Returns 0
ROLLBACK ;
# Table 1 now contains both rows.
So imagine a UnitTest executing any Implicit commit commands, then it commits the transaction. So when running Migration command in a test, or stored procedures, or seeders that have implicit commands in them, a rollback is no longer possible as the transaction was already closed.
In php <=7.4.12 no exception was thrown, so most of the times, you would have no idea why you have data leakage in your test
PHP 8.0 now throwns an exception.
Laravel
How does this affect Laravel?
- Many applications use DatabaseTransactions or RefreshDatabase traits, which no longer work when implicit commits have been issued in a test.
- When a rollback is happening with
DB::transaction
, a newPDOException
is thrown.
Consider the following test file:
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class MyTest extends TestCase
{
use DatabaseTransactions;
public function testMe()
{
DB::unprepared('CREATE TABLE a (col varchar(1) null)');
}
}
This will generate the following error when running the test:
There was 1 error:
1) Tests\Unit\MyTest::testMe
PDOException: There is no active transaction
/app/vendor/laravel/framework/src/Illuminate/Database/Concerns/ManagesTransactions.php:258
/app/vendor/laravel/framework/src/Illuminate/Database/Concerns/ManagesTransactions.php:237
/app/vendor/laravel/framework/src/Illuminate/Foundation/Testing/DatabaseTransactions.php:31
/app/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:237
/app/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:153
Dump of the exception object:
PDOException {#4783
#message: "There is no active transaction"
#code: 0
#file: "./vendor/laravel/framework/src/Illuminate/Database/Concerns/ManagesTransactions.php"
#line: 258
+errorInfo: null
trace: {
./vendor/laravel/framework/src/Illuminate/Database/Concerns/ManagesTransactions.php:258 { …}
./vendor/laravel/framework/src/Illuminate/Database/Concerns/ManagesTransactions.php:237 { …}
./vendor/laravel/framework/src/Illuminate/Foundation/Testing/DatabaseTransactions.php:31 { …}
./vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:237 { …}
./vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:153 { …}
./vendor/phpunit/phpunit/src/Framework/TestCase.php:1188 { …}
./vendor/phpunit/phpunit/src/Framework/TestResult.php:730 { …}
./vendor/phpunit/phpunit/src/Framework/TestCase.php:883 { …}
./vendor/phpunit/phpunit/src/Framework/TestSuite.php:669 { …}
./vendor/phpunit/phpunit/src/Framework/TestSuite.php:669 { …}
./vendor/phpunit/phpunit/src/Framework/TestSuite.php:669 { …}
./vendor/phpunit/phpunit/src/TextUI/TestRunner.php:667 { …}
./vendor/phpunit/phpunit/src/TextUI/Command.php:148 { …}
./vendor/phpunit/phpunit/src/TextUI/Command.php:101 { …}
./vendor/phpunit/phpunit/phpunit:61 { …}
}
}
Laravel code
Laravel calls the PHP's rollback command as follows:
framework/src/Illuminate/Database/Concerns/ManagesTransactions.php
Lines 236 to 240 in 6ecfdb5
framework/src/Illuminate/Database/Concerns/ManagesTransactions.php
Lines 274 to 281 in 6ecfdb5
What has changed?
This exception was not thrown on PHP7.4.12, it is thrown on PHP 8.0RC5
I encountered this issue myself when trying to run my application on 8.0RC5
, I can imagine a lot of others will encounter this exception.
Next steps?
Now the question arises what to do with the new Exception that is thrown? As it is currently quite vague as to why it is happening by just looking at the error message, and it cost me a lot of research to find out the cause of it.
I think we need to do at least the following:
- Warn users that their test contains implicit commits leading to leaked data during test runs which greatly reduces their isolation.
- Still have a way to "ignore" the exception to make sure there will be a smooth transition when moving to 8.0.
- Think carefully about the effect of this new exception, how will this affect
DB::transaction()
when a rollback is initiated?
Please let me know if I can help you out in any way with this issue.
Steps to reproduce
Run the following on PHP 8:
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class MyTest extends TestCase
{
use DatabaseTransactions;
public function testMe()
{
DB::unprepared('CREATE TABLE a (col varchar(1) null)');
}
}