Laravel Nested Relationship on Factory for Testing - php

I got a problem, i have to admit i don't find any solution.
I'm actually developping some testing for functionnalities and Factories are blocking me.
First I'm trying to add with factories an Entity called "Tasklist" which contains one or many "sections" which contains one or many "actions".
I have a 3 level deep relationship.
Here are my factories:
$factory->define(\App\V2\Models\Tasklist::class, function (\Faker\Generator $faker) {
return [
'id_course' => \App\V2\Models\Program::all()->random(1)->id,
'id_event' => \App\V2\Models\Stage::all()->random(1)->id,
'id_course_rounds' => \App\V2\Models\ProgramRound::all()->random(1)->id,
'name' => $faker->word,
'display_name' => $faker->word,
'color' => 0,
'key' => str_random(16),
'auto_active' => 1,
'status' => 1,
];
});
$factory->define(\App\V2\Models\TasklistSection::class, function (\Faker\Generator $faker) {
return [
'id_tasklist' => function(){
return factory(\App\V2\Models\Tasklist::class)->create()->id;
},
'number' => 1,
'title' => $faker->word,
'text' => $faker->text(100),
'status' => 1
];
});
$factory->define(\App\V2\Models\TasklistAction::class, function(\Faker\Generator $faker) {
return [
'id_tasklists_section' => factory(\App\V2\Models\TasklistSection::class)->create()->id,
'number' => rand(1, 10),
'title' => $faker->word,
'percent' => $faker->numberBetween(0, 100),
'status' => 1
];
});
In my testing class, i'm trying to generate a tasklist with 1 section with one action. The only way i found actually was something like that:
$task = factory(Tasklist::class, 2)->create()
->each(function($t){
$t->sections()->save(factory(TasklistSection::class)->create()
->each(function($s){
$s->actions()->save(factory(TasklistAction::class)->create());
})
);
});
To this code, if I delete the second each, it works, i got 2 tasklists with each 1 sections. In fact, the each is disturbing me.
I would like to create only one tasklist, with one or several sections with one or several actions on it.
But the each only accept Collection input the save method accepts only model input and not collection.
Does somebody have an idea how to deal with that ?

One approach can be this:
create task with sections and store them in the variable and then loop through each task section and add actions to it like this:
$tasklist = factory(App\Tasklist::class)->create();
$tasklist->sections()->saveMany(factory(App\TasklistSection::class, 3)->make());
foreach ($tasklist->sections as $section){
$section->actions()->saveMany(factory(App\TasklistAction::class, 3)->make());
}
this will work as expected.

Related

Laravel Faker Factory Relationship

I have two factories one for categories and another for products. When I run the factory I want to create x number of products for each category generated. how would I write the code to product this?
the definition for the categories is written as this:
return [
'name' => $this->faker->word,
'slug' => Str::slug($this->faker->unique()->word, '-'),
];
and the definition for the product is written as such:
return [
'category_id' => 1, //instead of 1 the category id used i want to be random
'name' => $this->faker->word,
'slug' => Str::slug($this->faker->unique()->word, '-'),
'description' => $this->faker->paragraph,
'price' => $this->faker->randomFloat(2, 0, 10000),
'is_visible' => 1,
'is_featured' => 1
];
as you can see I hardcoded the category_id, I wasnt too sure how to have it automatically generate and create a product per category that exists. I have the factory for the category written as this, to create 10 items
Category::factory()
->count(10)
->create();
I tried this for trial and error thinking it would work but I get an error that category_id cannot be null .
Product::factory()
->has(Category::factory()->count(2))
->count(20)
->create();
$factory->define(Product::class, function (Faker $faker) {
return [
'category_id' => factory(Category::class), //instead of 1 the category id used i want to be random
'name' => $this->faker->word,
'slug' => Str::slug($this->faker->unique()->word, '-'),
'description' => $this->faker->paragraph,
'price' => $this->faker->randomFloat(2, 0, 10000),
'is_visible' => 1,
'is_featured' => 1
];
});
By setting the attribute to an instance of factory() Laravel will lazily create that model as well and automatically associate it
I am using kind of different syntax, but I think it will work / you can change it
In your Category.php model
public function products() {
return $this->hasMany(Product::class);
}
In seeder
factory(App\Category::class, 10)->create()->each(function($c) {
$c->products()->save(factory(App\Product::class)->make());
}); // each Category will have 1 product
Laravel Database Testing Relationships
You simply need to pass a CategoryFactory to category_id.
return [
'category_id' => Category::factory(),
// ...
];
You can read more about factories here: https://laravel.com/docs/8.x/database-testing#defining-relationships-within-factories
If you want to create multiple products per each created category, you can do something like this:
// CategoryProductSeeder
$categories = Category::factory(50)->create();
$categories->each(function ($category) {
$categories->products()->saveMany(
Product::factory(10)->make()
);
});
this is what worked for me since I'm using laravel 8.
product definition:
return [
'category_id' => Category::factory(),
'name' => $this->faker->word,
'slug' => Str::slug($this->faker->unique()->word, '-'),
'description' => $this->faker->paragraph,
'price' => $this->faker->randomFloat(2, 0, 1000),
'is_visible' => 1,
'is_featured' => 1
];
seeder:
Product::factory()
->has(Category::factory())->count(50)
->create();
created 50 categories and 50 products. 1 category assigned to each product.

Matching factory values in Laravel 7

Good Afternoon,
I'm trying to create a Laravel factory where 2 of the 'columns' have the same values every time its called and the rest of the factory can be random.
For instance, I have the following columns in my DB
name
email
phone_number
status_message
status_code
I currently have my factory as follows;
$factory->define(Brand::class, function (Faker $faker) {
return [
'name' => $faker->unique()->company,
'email' => $faker->companyEmail,
'phone_number' => $faker->phoneNumber
];
});
This part works perfectly, as it should, the problem is that each specific status message comes with an individual status code. Is there a way I could add an array of status messages with a status code and have the factory pick a set at random for that record?
The status code / messages are listed below in array format;
[
'3e2s' => 'tangled web',
'29d7' => 'get certified',
'2r5g' => 'art of war',
]
I hope this makes sense. any help would be greatly appreciated.
as i can understand u need to pick random from this array u mentioned in above
$factory->define(Brand::class, function (Faker $faker) {
$data = [
'3e2s' => 'tangled web',
'29d7' => 'get certified',
'2r5g' => 'art of war',
];
$statusCode = array_rand($data);
$statusMessage = $data[$statusCode];
return [
'name' => $faker->unique()->company,
'email' => $faker->companyEmail,
'phone_number' => $faker->phoneNumber,
'status_message' => $statusMessage,
'status_code' => $statusCode,
];
});

Laravel phpunit test nested eager loading

I have this nested relation im abit unsure how i assertJson the response within the phpunit test.
FilmController
public function show(string $id)
{
$film = Film::with([
'account.user:id,account_id,location_id,name',
'account.user.location:id,city'
])->findOrFail($id);
}
FilmControllerTest
public function getFilmTest()
{
$film = factory(Film::class)->create();
$response = $this->json('GET', '/film/' . $film->id)
->assertStatus(200);
$response
->assertExactJson([
'id' => $film->id,
'description' => $film->description,
'account' => $film->account->toArray(),
'account.user' => $film->account->user->toArray(),
'account.user.location' => $film->account->user->location->toArray()
]);
}
Obviously this isnt working because its returning every column for the user im a little unfamiliar with how you test nested relations with the code you need so im unsure with a toArray can anyone help out?
Testing is a place where you throw DRY (don't repeat yourself) out and replace it with hard coded solutions. Why? simply, you want the test to always produce the same results and not be bound up on model logic, clever methods or similar. Read this amazing article.
Simply hard code the structure you expect to see. If you changed anything in your model to array approach, the test would still pass even thou your name was not in the response. Because you use the same approach for transformation as testing. I have tested a lot of Laravel apps by now and this is the approach i prefers.
$account = $film->account;
$user = $account->user;
$location = $user->location;
$response->assertExactJson([
'description' => $film->description,
'account' => [
'name' => $account->name,
'user' => [
'name' => $user->name,
'location' => [
'city' => $location->city,
],
],
],
]);
Don't test id's the database will handle those and is kinda redundant to test. If you want to check these things i would rather go with assertJsonStructure(), which does not assert the data but checks the JSON keys are properly set. I think it is fair to include both, just always check the JSON structure first as it would likely be the easiest to pass.
$response->assertJsonStructure([
'id',
'description',
'account' => [
'id',
'name',
'user' => [
'id',
'name',
'location' => [
'id',
'city',
],
],
],
]);

Yii2 Kartik EditableColumn Dropdown Relation Returns wrong Value

I have an issue with a Gridview using kartik\grid\EditableColumn, after changing the value I am returned the wrong value for the column when it updates. I am returned the dropdown key/main table integer rather than the string contained in a linked table.
I have two tables
Leads - columns id and status_id
Related fields - model, field, related_value, related_value
The relation is based on in this case
model:"Leads",
field:"status_id",
related_id:status_id
I have the following relation in my model
public function getStatus()
{
return $this->hasOne(RelatedFields::className(), ["related_id" => "status_id"])->andOnCondition(["status.field" => "status_id", "status.model"=>"Leads"])->from(["status" => RelatedFields::tableName()]);
}
I also created the following as a test based on this link
public function getStatusValue()
{
return $this->status->related_value;
}
Here is the column code
[
'class' => 'kartik\grid\EditableColumn',
'attribute' => 'status_id',
'value'=>'status.related_value',
//'value' => function($model){ return $model->status->related_value; },
//'value' => function($model){ return $model->StatusValue; },
//'refreshGrid' => true,//Works but not nice
'vAlign'=>'middle',
'hAlign'=>'center',
'pageSummary' => true,
'readonly' => false,
'width'=>'10%',
'filter'=>Html::activeDropDownList($searchModel, 'status', ArrayHelper::map(RelatedFields::Find()->where(['model' =>"Leads","field"=>"status_id"])->all(), 'related_id', 'related_value'),['class' => 'form-control','prompt' => Yii::t('app', '')]),
'editableOptions'=> [
//'attribute'=>'status_id',
//'value'=>'status.related_value',
//'header' => 'profile',
//'format' => Editable::FORMAT_BUTTON,
'inputType' => Editable::INPUT_DROPDOWN_LIST,
'data'=> ArrayHelper::map(RelatedFields::Find()->where(['model' =>"Leads","field"=>"status_id"])->all(), 'related_id', 'related_value'),
]
],
Commented out are a number of lines in my attempts to fix the issue as well as combinations of them, however all result in the wrong value.
If for example I select the related value "New" which has a related_id 1, after the column has been updated I get the value 1 instead of "New".
When the table is first loaded/reloaded the value does show correctly.
I could reload the grid, but this seems wrong just to fix 1% of the data shown on the page.
I your model take a public variable $status_value
create an assigning value method
public function getStatusValue()(){
return $this->status_value= $this->status->related_value;
}
Now in Gridview use getStatusValueenter code heremethod with assigning value as below
use yii\helpers\Url;
$gridColumns = [
[
'class' => 'kartik\grid\EditableColumn',
'attribute' => 'status_value',
'pageSummary' => true,
'readonly' => false,
'value' => function($model){ return $model->statusValue; }, // assign value from getStatusValue method
'editableOptions' => [
'header' => 'status_value',
'inputType' => kartik\editable\Editable::INPUT_TEXT,
'options' => [
'pluginOptions' => [
]
]
],
],
];
If you follow Kartik guide, he suggest to add EditableColumnAction to better handle the editable column:
The EditableColumnAction offers a quick easy way to setup your
controller action for updating, saving and managing the EditableColumn
output from GridView. This action class extends from yii\rest\Action
and hence all properties available with yii\rest\Action are applicable
here. The basic setup of the column involves setting up the controller
action and the EditableColumn.
So you need to add an EditableColumnAction in your controller to handle the update of the model:
public function actions()
{
return ArrayHelper::merge(parent::actions(), [
'edit-lead' => [
'class' => EditableColumnAction::class,
'modelClass' => Leads::class
]
]);
}
In your GridView editable column configuration, include the above
controller action for processing the Editable within editableOptions.
For example
And in your column code you need to add the action to editableOptions property:
'editableOptions' => [
...
'formOptions' => ['action' => ['/leads/edit-lead']]
]
Now, according to the guide, you can add to your action the outputValue property:
'outputValue' => function (Leads $model) {
return $model->status->related_value;
}

Factories on Laravel 5.2 not working as expected

I have a very strange problem with Laravel 5.2 factories.
I have recently upgraded from Laravel 5.1 to 5.2 following the upgrade guide on the Laravel website. All works as exepected except one factory. Yes the others work ok. Here are two of the factories:
$factory->define(App\Client::class, function (Faker\Generator $faker) {
return [
'name' => $faker->company,
'building' => $faker->buildingNumber,
'street' => $faker->streetName,
'town' => $faker->city,
'postcode' => $faker->postcode,
'country' => 'UK',
'telephone' => $faker->phoneNumber,
'fax' => $faker->phoneNumber,
];
});
$factory->define(App\Shift::class, function (Faker\Generator $faker) {
return [
'client_id' => $faker->numberBetween($min = 1, $max = 15),
'user_id' => $faker->numberBetween($min = 1, $max = 15),
'start' => $faker->dateTimeBetween($startDate='now', $endDate='+60 days'),
'public' => $faker->boolean(),
];
});
The top factory works no problem but the second one doesn't run at all cause my db seed to throw an error because its not populating the client_id which is a foreign key.
The only difference between the two models is that the client model doesn't use timestamps where as the shift model does. Other than that they are identical.
I will keep plugging away but any help to shed light on this would be greatly received.
When you add your own constructor, are you making sure to call parent::__construct() inside it?

Categories