"There is no active transaction" in Doctrine migrations - symfony

I try to apply migrations, first three of them to create table, the last one - insert data.
When i run php bin/console doctrine:migrations:migrate it gives me an error "There is no active transaction" after each migration and had stoped migration. So i had to run migrations:migrate 4 times.
Whats could be the problem?

This issue existed before, but now is visible after PHP 8 PDO.
I will quote a great explanation located at your
vendor/doctrine/migrations/docs/en/explanation/implicit-commits.rst:
Implicit commits
Since PHP8, if you are using some platforms with some drivers such as
MySQL with PDO, you may get an error that you did not get before when
using this library: There is no active transaction. It comes from
the fact that some platforms like MySQL or Oracle do not support DDL
statements (CREATE TABLE, ALTER TABLE, etc.) in transactions.
The issue existed before PHP 8 but is now made visible by e.g. PDO,
which now produces the above error message when this library attempts
to commit a transaction that has already been commited before.
Consider the following migration.
public function up(Schema $schema): void
{
$users = [
['name' => 'mike', 'id' => 1],
['name' => 'jwage', 'id' => 2],
['name' => 'ocramius', 'id' => 3],
];
foreach ($users as $user) {
$this->addSql('UPDATE user SET happy = true WHERE name = :name AND id = :id', $user);
}
$this->addSql('CREATE TABLE example_table (id INT AUTO_INCREMENT NOT NULL, title VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
}
When you run that migration, what actually happens with some platforms
is you get the updates inside an implicitly commited transaction, then
the CREATE TABLE happens outside that transaction, and then there
is an attempt to commit an non-existent transaction.
In that sort of situation, if you still wish to get the DML statements
inside a transaction, we recommend you split the migration in 2
migrations, as follows.
final class Version20210401193057 extends AbstractMigration
{
public function up(Schema $schema): void
{
$users = [
['name' => 'mike', 'id' => 1],
['name' => 'jwage', 'id' => 2],
['name' => 'ocramius', 'id' => 3],
];
foreach ($users as $user) {
$this->addSql('UPDATE user SET happy = true WHERE name = :name AND id = :id', $user);
}
}
}
final class Version20210401193058 extends AbstractMigration
{
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE example_table (id INT AUTO_INCREMENT NOT NULL, title VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
}
public function isTransactional(): bool
{
return false;
}
}
Please refer to the manual of your database platform to know if you
need to do this or not.
At the moment, this library checks if there is an active transaction
before commiting it, which means you should not encouter the error
described above. It will not be the case in the next major version
though, and you should prepare for that.
To help you deal with this issue, the library features a configuration
key called transactional. Setting it to false will cause new
migrations to be generated with the override method above, making new
migrations non-transactional by default.
Solution 1
Disable the parameter transactional in your migrations config file as explained here.
Solution 2
Disable transactional migration only this time by adding --all-or-nothing=0 in your php bin/console doctrine:migrations:migrate command.

If you use PHP 8.0, implements "isTransactional" in migration class and return false (See https://github.com/doctrine/DoctrineMigrationsBundle/issues/393)

For me following code worked
public function isTransactional(): bool
{
return false;
}

Related

Error in Github Action for Laravel / PHPUnit

I have an error in my github action for my laravel unit/feature tests using PHPUnit. My tests are passing locally. This is the error:
1) Tests\Feature\ClientControllerTest::test_clients_index_page_is_rendered
Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1 Cannot add a NOT NULL column with default value NULL (SQL: alter table "users" add column "role_id" integer not null)
It could be that I'm not fully understanding the way the testing works, but I'm using an in-memory sqllite database for testing. I have a migration for the users table and then another migration that adds a role_id to the user table after a roles table is created.
Not understanding why the error is occurring during the test_clients_index_page_is_rendered test because the database should already be up and populated at that point.
I don't know if it's because the roles table is not populated with data, and it's a foreign key on the users table. I would think that would fail locally as well though because I'm still using the in-memory database. I have a seeder to populate the roles, but I'm not calling it from anywhere in my tests. Not sure if I need to be doing that, or where to do it?
Here is the test mentioned in the error:
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\Role;
use App\Models\User;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ClientControllerTest extends TestCase
{
use RefreshDatabase;
protected $user;
public function setup() :void
{
parent::setUp();
$this->user = User::factory()->create();
}
public function test_clients_index_page_is_rendered()
{
$this->actingAs($this->user);
$response = $this->get('/clients');
$response->assertStatus(200);
}
Here are the related migrations that I suppose could also be causing the problems:
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('first_name');
$table->string('last_name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
});
}
}
And then later the roles table is added, and then the foreignkey:
Schema::table('users', function (Blueprint $table) {
$table->foreignIdFor(Role::class)->after('password');
});
I'm not even sure how to troubleshoot this issue as it's passing locally. Any help would be greatly appreciated.
Here's the full trace in case it helps:
1) Tests\Feature\ClientControllerTest::test_clients_index_page_is_rendered
Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1 Cannot add a NOT NULL column with default value NULL (SQL: alter table "users" add column "role_id" integer not null)
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Database/Connection.php:705
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Database/Connection.php:665
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Database/Connection.php:495
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Database/Schema/Blueprint.php:109
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php:363
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php:210
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php:261
/home/runner/work/momentum/momentum/database/migrations/2022_01_01_222316_add_role_id_to_users_table.php:19
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:394
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:403
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:202
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:167
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:112
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/MigrateCommand.php:85
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:585
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/MigrateCommand.php:94
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:36
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Container/Util.php:40
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:93
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:37
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Container/Container.php:653
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Console/Command.php:136
/home/runner/work/momentum/momentum/vendor/symfony/console/Command/Command.php:298
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Console/Command.php:121
/home/runner/work/momentum/momentum/vendor/symfony/console/Application.php:1005
/home/runner/work/momentum/momentum/vendor/symfony/console/Application.php:299
/home/runner/work/momentum/momentum/vendor/symfony/console/Application.php:171
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Console/Application.php:94
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Console/Application.php:186
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php:263
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Testing/PendingCommand.php:260
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Testing/PendingCommand.php:413
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/InteractsWithConsole.php:66
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Foundation/Testing/RefreshDatabase.php:45
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Foundation/Testing/RefreshDatabase.php:20
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:122
/home/runner/work/momentum/momentum/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:91
/home/runner/work/momentum/momentum/tests/Feature/ClientControllerTest.php:19
Caused by
PDOException: SQLSTATE[HY000]: General error: 1 Cannot add a NOT NULL column with default value NULL
UPDATE
Adding my user factory:
namespace Database\Factories;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Factories\Factory;
class UserFactory extends Factory
{
public function definition()
{
return [
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'phone' => $this->faker->unique()->numerify('###-###-####'),
'email' => $this->faker->unique()->safeEmail,
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi',
'remember_token' => Str::random(10),
'role_id' => rand(1,4)
];
}
}
As you can see in the stack trace, exception thrown during the migration step.
This happens because you adding not null column to your table, which generally impossible. SQLite don't know what value should be set to role_id for records that already in the table, so it prevents you from this operation.
You can either add default value to role_id, or move it to initial migration.

Doctrine weird behavior, changes entity that I never persisted

I have this situation:
Symfony 4.4.8, in the controller, for some users, I change some properties of an entity before displaying it:
public function viewAction(string $id)
{
$em = $this->getDoctrine()->getManager();
/** #var $offer Offer */
$offer = $em->getRepository(Offer::class)->find($id);
// For this user the payout is different, set the new payout
// (For displaying purposes only, not intended to be stored in the db)
$offer->setPayout($newPayout);
return $this->render('offers/view.html.twig', ['offer' => $offer]);
}
Then, I have a onKernelTerminate listener that updates the user language if they changed it:
public function onKernelTerminate(TerminateEvent $event)
{
$request = $event->getRequest();
if ($request->isXmlHttpRequest()) {
// Don't do this for ajax requests
return;
}
if (is_object($this->user)) {
// Check if language has changed. If so, persist the change for the next login
if ($this->user->getLang() && ($this->user->getLang() != $request->getLocale())) {
$this->user->setLang($request->getLocale());
$this->em->persist($this->user);
$this->em->flush();
}
}
}
public static function getSubscribedEvents()
{
return [
KernelEvents::TERMINATE => [['onKernelTerminate', 15]],
];
}
Now, there is something very weird happening here, if the user changes language, the offer is flushed to the db with the new payout, even if I never persisted it!
Any idea how to fix or debug this?
PS: this is happening even if I remove $this->em->persist($this->user);, I was thinking maybe it's because of some relationship between the user and the offer... but it's not the case.
I'm sure the offer is persisted because I've added a dd('beforeUpdate'); in the Offer::beforeUpdate() method and it gets printed at the bottom of the page.
alright, so by design, when you call flush on the entity manager, doctrine will commit all the changes done to managed entities to the database.
Changing values "just for display" on an entity that represents a record in database ("managed entity") is really really bad design in that case. It begs the question what the value on your entity actually means, too.
Depending on your use case, I see a few options:
create a display object/array/"dto" just for your rendering:
$display = [
'payout' => $offer->getPayout(),
// ...
];
$display['payout'] = $newPayout;
return $this->render('offers/view.html.twig', ['offer' => $display]);
or create a new non-persisted entity
use override-style rendering logic
return $this->render('offers/view.html.twig', [
'offer' => $offer,
'override' => ['payout' => $newPayout],
]);
in your template, select the override when it exists
{{ override.payout ?? offer.payout }}
add a virtual field (meaning it's not stored in a column!) to your entity, maybe call it "displayPayout" and use the content of that if it exists

PHPUnit Form Test Failing on DateTime

I have several unit tests failing on my local machine because DateTime values differ:
4) Tests\AppBundle\Form\AssetStatusTypeTest::testValidForm with data set #0 (array('active'))
Failed asserting that two objects are equal.
--- Expected
+++ Actual
## ##
'status' => 'active'
'type' => null
'amount' => null
- 'createdAt' => 2018-09-20T20:34:47.047520+0200
+ 'createdAt' => 2018-09-20T20:34:47.047870+0200
'updatedAt' => null
'contract' => null
'admin' => null
The test:
public function testValidForm($data)
{
$form = $this->factory->create(AssetStatusType::class);
$object = Entity::fromArray(new Asset(), $data);
$form->submit($data);
$this->assertTrue($form->isSynchronized());
$this->assertEquals($object, $form->getData());
$view = $form->createView();
$children = $view->children;
foreach (array_keys($data) as $key) {
$this->assertArrayHasKey($key, $children);
}
}
Which is more or less copied from the docs.
Now, the createdAt field is set like this:
public function __construct()
{
if (empty($this->createdAt)) {
$this->createdAt = new \DateTime();
}
}
My main point is: This test is passing on our Jenkins. It does not locally on several developer machines. My first impulse was to check ini timezone settings.
I wonder how this is even possible. If I get it right, expected is when object was created and actual is when the form is submitted. So both objects can never have the same createdAt timestamp. Unless precision is probably super low.
The two machines are running different versions of PHP. PHP7.1 (and above) includes microseconds when it creates a DateTime object, but code running on PHP 5.x and 7.0, won't.
There are two ways to work with this:
Don't compare exact DateTimes, but convert them to seconds with $datetimeObj->format('U');. You'll still get occasional test failures when one test is created at say 1.99998 and the next call to create a datetime is at 2.0001, and so the test still fails when converted to seconds.
Use 'Clock Mocking'. Using some interesting PHP namespace tricks, the global time() function is overridden (sleep() too). You have to create new DateTime object to make sure they actually use the new version of the time() function, but the clock would effectively stop - and sleep()'s just turn into something more like $time += $seconds; - which also means that a sleep(3600); takes effective zero time.
The symfony/phpunit-bridge ClockMock.php can be used just as a library if you don't want to include it as a listener in your PHPunit configuration.

Strange Doctrine 2 behaviour

I am using a Symfony command to retrieve member details from an API afor updating in a local database.
I query my local database and then loop through each member to retrieve data and update individually. However, doctrine is not updating these member details.
I have tried to use persist and merge but none of them seems to be working which is really baffling. Below is the code I am using, hopefully an extra pair of eyes will spot what I am not:
$members = $em->getRepository('AppBundle:Member')->findByNot(array('pin' => "'NULL'"),array('updated_at' => 'ASC'),4,0);
foreach($members as $member)
{
$details = json_decode($api_url->apiConnect(array('login' => $member->getId(), 'password' => $member->getPassword())), true);
$member->setFirstName($details['firstName']);
$member->setLastName($details['lastName']);
$member->setCard($details['card']);
//$em->merge($member);
$em->persist($member);
$em->flush();
}
When I check the mysql query log, none of the update queries are run.

foreign key not persisted symfony2 after a clone

I'm using symfony + Doctrine and I'm stuck with a problem:
I cloned an existing object and I would like to change a FK on the clone. It should be like that:
$dafCloned = clone $daf;
$dafState = $dafStateRepository->findOneBy(
array(
'name' => 'saved',
'dafType' => 'invoice',
'company' => $daf->getSeller(),
));
$dafCloned->setDafState($dafState);
var_dump($dafState->getId());
var_dump($dafCloned->getDafState()->getId());
$this->em->persist($dafCloned);
$this->em->flush();
As you may have noticed, I got 2 var_dump here. Here are the print of the Custom Command calling this code :
int(5500)
int(5499)
5500 is the id I should have in db for $dafCloned, 5499 is the id I have for $daf.
I'd like to know WHY I got different id...My dafState should be the same. I'm probably missing something really stupid but I'm stuck on it since 9am...I even tried to delete all caches we have, moving flush() and persist() but cant help :s
EDIT : added the setDafState() method if needed, but this is basic :
public function setDafState(DafState $dafState) {
$this->dafState = $dafState;
return $this;
}
EDIT2 :
Here getDafState() :
/**
* Get dafState
*
* #return MyPath\Entity\DafState
*/
public function getDafState() {
return $this->dafState;
}
If you need more code sample, just ask for it, I'll edit ;)
For the object, both are huge (Doctrine Object) and i can't find any way to get what could be useful :s. I cant grep dafState on $daf Object, output is still huge.
EDIT 3 :
if ($daf->getId() == 8902) // daf test which should be duplicated
var_dump($dafCloned->getDafState() === $dafState);
output
bool(true)
$dafCloned = clone $daf; // Here your clone is the same object as the old one
$dafState = $dafStateRepository->findOneBy( // Here you get some fresh object
array(
'name' => 'saved',
'dafType' => 'invoice',
'company' => $daf->getSeller(),
));
$dafCloned->setDafState($dafState); // Because this object is still managed by the entity manager it will set the $dafState on the old object (tracked by Id most likely)
var_dump($dafState->getId()); // Show the Id on the fresh object
var_dump($dafCloned->getDafState()->getId()); // Show the Id on the old object
$this->em->persist($dafCloned); // overwrite the old object
$this->em->flush();
This Post will be helpful to you: How to re-save the entity as another row in Doctrine 2
I will update my answer if this doesn't solve your issue
Here we go.
Thanks to #cheesemacfly i find out i have a prePersistListener which was resetting my dafState !
So, next time you have something weird looking like the above problem, check your listener !

Resources