Error in Github Action for Laravel / PHPUnit - 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.

Related

"There is no active transaction" in Doctrine migrations

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;
}

Export user roles with Sonata admin

I'm using SonataAdmin and FosUserBundle with Symfony 4.
I want to use the export feature to export whole users' data in CSV, JSON ...
When a trigger the export, the roles column in the file is empty or null.
In the UserAdmin class, I have overridden the getExportFields function with the call of a specific method to get the role as explained in this post. Sonata admin export fields with collection fields
But it doesn't work.
Example in my case:
public function getExportFields()
{
return [
'id',
'username',
'roles' => 'rolesExported'
];
}
And in my User Entity:
public function getRolesExported()
{
$exportedRoles = [];
foreach ($this->getRealRoles() as $role) {
$exportedRoles[] = $role->__toString();
}
return $this->rolesExported = implode(' - ', $exportedRoles);
}
In this case, when I trigger the export, my web browser shows the error
'website is inaccessible' with no error in the dev.log.
When I delete 'roles' => 'rolesExported' in the getExportFields function, the export is well triggered.
SonataAdmin version : 3.35
FosUserBundle version : 2.1.2
Symfony version: 4.3.2 (I know that I need to update it)
I suspect that the __toString() call causes a problem.
although the post you used as your inspiration explicitly says that it wants to export collections, I assume you might want to export an array.
Since I don't know the type of your $role objects, for debugging purposes first replace $role->__toString() with gettype($role), so the line is:
$exportedRoles[] = gettype($role);
I see three cases here:
object or for multiple roles object - object - ..., in that case, you should select a method of Role that returns a proper string or create one at that place, like $exportedRoles[] = $role->getName();
string or for multiple roles string - string - ..., in that case your "real" roles is just an array, and you can replace the contents of your function with return implode(' - ', $this->getRealRoles());
array or for multiple roles array - array - ..., in that case you have an array for each role, and those don't provide __toString. Select a method to construct the exported role, like $exportedRoles[] = $role['name'];

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

Add data when running Symfony migrations

I have a Symfony project that is using the DoctrineMigrations bundle, and I have a really simple question: When I run a migration (e.g., when I'm pushing an update to production), how can I insert data to the database?
For example: I have an Entity which is the type of an add. The entity is:
private $addType; // String
private $type1; // Boolean
private $type2; // Boolean
private $type3; // Boolean
I add another field ($type4), and I want to add a new record to the database, with this values:
$addType = 'Type number 4';
$type1 = false;
$type2 = false;
$type3 = false;
$type4 = true;
How can this be done with DoctrineMigrations? Is it possible?
Using the Entity Manager as suggested in another answer is not a good idea, as it leads to troubles later.
In the first migration, I created a table with users and populated some users via $em->persist($user); which seemed fine at the beginning.
But after a month, I added a phone column to my User model. And Doctrine generates INSERT statements with this column within the first migration, which fails due to the non-existing column phone. Of course it doesn't exist yet in the first migration. So it is better to go with pure SQL INSERTs.
I just asked a related related question.
It is possible to use the migrations bundle to add data to the database. If you add a new property and use the doctrine mapping then the
php app/console doctrine:migrations:diff
command will generate a new migration file. You can just put your insert statements inside this file using the syntax:
$this->addSql('INSERT INTO your_table (name) VALUES ("foo")');
Make sure you put it after the auto-generated schema changes though. If you want to separate your schema changes and your data changes then you can use
php app/console doctrine:migrations:generate
to create an empty migrations file to put your insert statements in.
Like I said in my related question, this is one way to do it, but it requires manually creating these if you want to change this data in the database.
Edit:
Since this answer seems to get a few views I think it's worth adding that to more clearly separate the data changes from the schema changes there is a postUp method that can be overridden and that will be called after the up method.
https://www.doctrine-project.org/projects/doctrine-migrations/en/3.0/reference/migration-classes.html#postup
I've "found" the correct way to solve my problem (insert data after running migrations, using my entity classes).
Here is: https://stackoverflow.com/a/25960400
The idea is to declare the migration as ContainerAware, and then, from the postUp function, call the DI to get the EntityManager. It's really easy, and you can use all your entities and repositories.
// ...
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class Version20130326212938 extends AbstractMigration implements ContainerAwareInterface
{
private $container;
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
public function up(Schema $schema)
{
// ... migration content
}
public function postUp(Schema $schema)
{
$em = $this->container->get('doctrine.orm.entity_manager');
// ... update the entities
}
}
when you make the new field you need to enter this annotation "options={"default":1}" and it should work.
/**
* #var boolean
* #ORM\Column(name="type4", type="boolean", options={"default":1})
*/
private $type4 = true;
Took me some time to figure this out :)
It does, if you know how to format the array;
$this->connection->insert('user', ['id' => 1, 'gender' => 'Male']);
this is good solution for me. Just use bin/console make:migration and when migration is generated just edit if and add "DEFAULT TRUE":
$this->addSql('ALTER TABLE event ADD active TINYINT(1) NOT NULL DEFAULT TRUE');
It doesn't sound a good idea to fill date in migration, not its responsibility, symfony has a way of doing that. https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html

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