Laravels test suite has an assertion called
$this->assertSoftDeleted()
But is there the inverse? To assert that something is not soft deleted?
I've tried assertDatabaseHas but doesn't work.
I have done it this way $this->assertTrue(! $goal->fresh()->trashed());
Works perfect.
But is there another, better, more ---elegant--- and laravel's way?
Something not being soft deleted just means that the deleted_at column is null. You could use the assertDatabaseHas method where that column is empty:
$this->assertDatabaseHas('goals', [
'id' => $goal->id,
'deleted_at' => null
]);
If you wanted to make this a bit more dynamic, you could extend your base TestCase class with a custom method that will do this for you and allow you to keep your tests with a readable assertion name, and additionally do things for you like find the appropriate table/column names automatically:
public function assertModelNotSoftDeleted(Model $model)
{
return $this->assertDatabaseHas($model->getTable(), [
$model->getKeyName() => $model->getKey(),
$model::DELETED_AT => null,
]);
}
Related
In Laravel, I have a persons model that has a many-to-many relationship with its group. The person's name needs to be unique in its group but not on the persons table. How would ensure that?
My validation is done in the controller store method using $request->validate(['name => ...
I currently save the new person in a controller using simply - Person::create([...
My simple approach is using a composite primary keys on pivot table and use basic exception handling like try catch stuff whenever inserting data is fail due to migration
$table->foreignId('group_id') // Add any modifier to this column
$table->foreignId('person_id') // Add any modifier to this column
$table->primary(['group_id', 'person_id']);
If you want to do it on controller, make sure to setup relationship. Then just use Rule::notIn() Validation
'name' => [
'required',
Rule::notIn(/* put your logic here */),
],
You can use 'exist' rule in Laravel Validation like that:
'name' => 'exists:group,name,person_id,'.$id
For more info you can check here:
https://laravel.com/docs/9.x/validation#rule-unique
Our Laravel app uses PostgreSQL and before we insert a register in a table we reserve the next valid id by calling PostgreSQL nextval function.
I want to test the insert method of UserRepository. As our database expects to receive an id I would need to generate it also in the test method that tests the insert method, so I use Laravel's factories make method like this to create a fake id:
$userModel = UserModel::factory()->make([
'id' => $this->userRepository->nextValId()
// Another solution 'id' => $this->faker->unique()->randomNumber(5)
]);
$this->userRepository->insert($userModel->toArray());
This might not seem too complex, but imagine I now have an insertMany method that inserts several users at once, and I want to test this insertMany method. In this case the creation with Laravel's factory() gets more complicated. To start off, this code wouldn't work now as the ids of all users would be the same:
$collection = UserModel::factory(4)->make([
'id' => $this->userRepository->nextValId()
// Another solution 'id' => $this->faker->unique()->randomNumber(5)
]);
$this->userRepository->insertMany($collection->toArray());
So I have to rewrite it like this:
$collection = UserModel::factory(4)->make();
$users = $collection->toArray();
$users[0]['id'] = $this->userRepository->nextValId();
$users[1]['id'] = $this->userRepository->nextValId();
$users[2]['id'] = $this->userRepository->nextValId();
$users[3]['id'] = $this->userRepository->nextValId();
$this->userRepository->insertMany($users);
So the question is: is there a one-liner for this use case? Is there a way to tell inside the array of the make that a key has to be different for every model created by the factory? (In this case, I would want $this->userRepository->nextValId() to act for each one of the models created.)
Of course I do not consider using Laravel's factories create method as it would create the register already in the database table and the later insertion would throw a duplicate key exception.
Side note: this is a fake example, not real, so it is possible that there might be a little bug. The code pasted is not proved. Mine is different, but the idea is the same as the one in the example used.
I have found a solution using Laravel's factory sequences:
https://laravel.com/docs/9.x/database-testing#sequences
This would be the cleanest code in my opinion:
$collection = UserModel::factory()->count(4)->state(new Sequence(
fn ($sequence) => ['id' => $this->userRepository->nextValId()]
// Or this using DatabaseTransactions trait: fn ($sequence) => ['id' => $sequence->index + 1]
))->make();
$this->userRepository->insertMany($collection->toArray());
'call_signs' => [
'required', 'array', 'min:1',
'call_signs.*.call_sign' => [
Rule::unique('call_signs', 'call_sign')->whereNull('deleted_at')->ignore($user->id, 'user_id')
]
],
this is my unique code
it gives me the error
call_signs is an array
BadMethodCallException
Method Illuminate\Validation\Validator::validateUnique:callSigns,callSign,"12",userId,deletedAt,"NULL" does not exist.
my Callsign table has softDelete
Laravel's Unique Validation Rule provides only ignore method and not whereNull method. Please note that whereNull method is part of Database Query Builder.
Also, it is usually not good to check for uniqueness on soft deletes on tables. You should consider using hard delete for such use case.
Anyway, if you want to use for some reason, you can create your own unique custom validation rule. You can read more about it here, contains example also.
According to Laravel's documentation on defining relationships within model factories:
You may also attach relationships to models using Closure attributes in your factory definitions. For example, if you would like to create a new User instance when creating a Post, you may do the following:
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => function () {
return factory(App\User::class)->create()->id;
}
];
});
The issue I'm running into is the reference to create() within the relationship definition. It seems to me that this doesn't belong here.
It works great if I am wanting to persist my relationships to the database:
factory(App\Post::class)->create();
By running the code directly above this, a new App\Post and a new App\User will be created and both persisted to the database.
But if I just want to new up the model(s) and not persist anything (at all) to the database by running:
factory(App\Post::class)->make();
It does what I want up to a certain point. A new App\Post instance is created but not persisted, however App\Comment is created and is persisted to the database.
It seems to me, that what I really want is something like this:
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => function () {
// here I only want to declare the relationship,
// not decide whether I want to create() or make()
// the relationship, something like:
return factory(App\User::class)->createOrMake()->id;
// or perhaps something like:
return factory(App\User::class)->id;
}
];
});
The end result is that I want the related data to respect what I'm trying to do from the top of the call, down. Make everything. Or create everything.
Am I doing something wrong here? Or is this something that doesn't currently exist?
Thanks!
You want the lazy() method for your related models!
At least in Laravel 5.5.
I found your question here because I was having the same issue. I found the answer thanks to Laravel's beautifully written code -- lazy() was defined just above the make() XDebug took me to -- and I've tried it out and it seems to work.
This is what I'm doing:
$factory->define(App\ArtItem::class, function (Faker $faker) {
return [
// 'id' => $faker->randomDigit,
'slug' => $faker->unique->word,
'artist_id' => factory(App\Artist::class)->lazy(),
'image_id' => factory(App\Image::class)->lazy(),
'created_at' => $faker->dateTime,
'updated_at' => $faker->dateTime,
'deleted_at' => $faker->dateTime,
];
});
Lazy() is an interesting creature that returns a closure, so you can't do factory(App\Image::class)->lazy()->id but I'm still seeing it successfully setting the correct image_id and artist_id.
I certainly hope you found the solution long before this, but maybe this'll help someone else!
This is finally solvable now using Factory Callbacks added in Laravel 5.6.12.
$factory->afterMaking(Post::class, static function (Post $post) {
if (!$post->user_id) {
$post->user()->associate(factory(User::class)->make(['id' => 0]));
}
});
$factory->afterCreating(Post::class, static function (Post $post) {
if (!$post->user_id) {
$post->user()->associate(factory(User::class)->create())->save();
}
});
Now, whenever you make a Post, if you don't pass it a user_id, it will make a User for you and set the relationship without saving it. But if you create a Post, it will persist a new User to the database and save its ID to the Post model.
Note that both the afterMaking and afterCreating closures are called when you create a factory model. I am temporarily setting the User id to 0 in case the foreign key posts.user_id was defined not null. It will be updated to the correct value in afterCreating. This is a terrible hack, but it works until we get a proper beforeCreating hook which would allow us to create the related model first.
I want a database with two tables Users and Companies and the users table has a foreign key with the company id. So 1 company can have multiple users.
I created the models for this in laravel and also created a factory for each of the tables.
In my seeders I want to create multiple data lines at once and found this solution:
factory(App\Company::class, 10)->create();
This works fine for the company table. But I need to extend this for the users table so it searches for available company Ids.
Any ideas how I can do this?
If I can't search for available Ids, I would also be happy to extend it with the "company_id" field and give it random value from 1-10 (beacause I know that these are the Ids for now).
So basically I want to extend this to use the fields from the factory, but extend it with another field "company_id":
factory(App\Users::class, 10)->create();
Here is the User factory code:
$factory->define(App\User::class, function (Faker\Generator $faker) {
return [
'first_name' => $faker->firstName,
'last_name' => $faker->lastName,
'postcode' => $faker->postcode,
'city' => $faker->city
];
});
When dealing with such relationships, defining model factories can be a pain. You have a few options though.
Random blind IDs
If you're sure that a common range of IDs is available, you can just generate a random ID to make the relationship work:
$factory->define(App\User::class, function (Faker\Generator $faker) {
return [
// ...
'company_id' => rand(1, 10),
];
});
Inline factories
This was the most widely used approach as promoted by Laracast's screencasts:
$factory->define(App\User::class, function (Faker\Generator $faker) {
return [
// ...
'company_id' => factory(App\Company::class)->create()->id,
];
});
Or you can go the other way around and create one or more users in the company factory. This way, you only need to run factory() on one of your entities.
Random ID from database
As of Laravel's 5.2 release, you have access to an Eloquent method called inRandomOrder:
$factory->define(App\User::class, function (Faker\Generator $faker) {
return [
// ...
'company_id' => Company::inRandomOrder()->first()->id,
];
});
Alternatives for Laravel releases before 5.2:
// Laravel 4.2.7 - 5.1:
User::orderByRaw("RAND()")->get();
// Laravel 4.0 - 4.2.6:
User::orderBy(DB::raw('RAND()'))->get();
// Laravel 3:
User::order_by(DB::raw('RAND()'))->get();
// Source: http://stackoverflow.com/a/13931676/65732
It looks like that, you want to create Companies and Users and also you want to attach users with companies at the same time. If this is what you are trying to achieve then you can do it easily using something like this:
factory(App\Company::class, 10)->create()->each(function($factory) {
$factory->users()->save(factory(App\User::class)->make());
});
In this case, you have to define the users relationship method in your Company model using the right foreign key.
This will create 10 companies and return a collection so on that collection using each method a single user will be created and attached with that company. Also, check the documentation.
If you're using Laravel 5.2 or above you can use this:
'company_id' => Company::inRandomOrder()->first()->id