I'm using the following code to create 20 posts, each of which has 3 comments.
Post::factory()
->times(20)
->has(Comment::factory()->times(3))
->create()
Instead I'd like to create 20 posts, each of which has a random number of comments (e.g. post 1 has 2 comments, post 2 has 4 comments, etc.)
This did not work, each post had the same (random) number of comments.
Post::factory()
->times(20)
->has(Comment::factory()->times(rand(1, 5)))
->create()
How can I achieve this?
It's not possible to have a dynamic number of related models per model if you are using ->times as far as I know. You can instead try:
collect(range(0,19))
->each(function () {
Post::factory()
->has(Comment::factory()->times(rand(1,5)))
->create();
});
This should create 20 posts one by one with a random number of comments on each. It may be a bit slower but probably not by much
I would use a factory method to do this. Add a method to your Post factory like this:
<?php
namespace Database\Factories\App;
use App\Comment;
use App\Post;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory
{
public function definition(): array
{
return [
// ...
];
}
public function addComments(int $count = null): self
{
$count = $count ?? rand(1, 5);
return $this->afterCreating(
fn (Post $post) => Comment::factory()->count($count)->for($post)->create()
);
}
}
Then in your test, you can simply call it like this:
Post::factory()->count(20)->addComments()->create();
Updated: That should work:
Inspired by apokryfos. If that not work, that will:
for($i=0; $i<20; $i++)
{
$times = rand(1,5);
Post::factory()
->has(Comment::factory()->times($times))
->create();
}
I want to loop over a collection of items and attach a relationship based on if a particular condition is satisfied. Here is my code
public function bulkAssign()
{
$trainers = MasterTrainer::all();
for ($i=0; $i < count($trainers); $i++) {
$this->assignToManager($trainers[$i]);
}
// return redirect()->back()->with('success', 'Project Managers Assigned Successfully');
}
private function assignToManager($trainer)
{
$manager = ProjectManager::where('state', $trainer->state)->first();
return $trainer->update([
'project_manager_id' => $manager->id
]);
}
What I get is it attaches only the first manager to all the elements in the collection. What am i doing wrong?
can you inline the func for now? do some sort of echo/debugging?
but also I see several issues:
yes do use foreach because that is a bit better and you avoid having to use $i (making code a little more easy to read)
you are not attaching a relationship, you are setting a project_manager_id (i say this because initially i automatically thought you were going to dynamically add a relationship to model)
without knowing your db schema.. could you not do some sort of trick to avoid having to do this nth times?
$manager = ProjectManager::where('state', $trainer->state)->first();
you could either do:
$states = $trainers->pluck('states');
$managers = // do a query to get one trainer per state using group by
foreach ($trainers... ) {
$manager = $managers->where('state', $trainers->state)->first() // this is collection not eloquent
$trainer->update([
'project_manager_id' => $manager->id
]);
other would be to create a scope where you do a sub query to get manager id when u query for trainers
Introduction
What up folks, I got a question about model factories and multiple unique columns:
Background
I have a model named Image. This model has language support stored in a separate model, ImageText. ImageText has an image_id column, a language column and a text column.
ImageText has a constraint in MySQL that the combination image_id and language has to be unique.
class CreateImageTextsTable extends Migration
{
public function up()
{
Schema::create('image_texts', function ($table) {
...
$table->unique(['image_id', 'language']);
...
});
}
...
Now, I want each Image to have several ImageText models after seeding is done. This is easy with model factories and this seeder:
factory(App\Models\Image::class, 100)->create()->each(function ($image) {
$max = rand(0, 10);
for ($i = 0; $i < $max; $i++) {
$image->imageTexts()->save(factory(App\Models\ImageText::class)->create());
}
});
Problem
However, when seeding this using model factories and faker, you are often left with this message:
[PDOException]
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '76-gn' for key 'image_texts_image_id_language_unique'
This is because at some point, inside that for loop, the faker will random the same languageCode twice for an image, breaking the unique constraint for ['image_id', 'language'].
You can update your ImageTextFactory to say this:
$factory->define(App\Models\ImageText::class, function (Faker\Generator $faker) {
return [
'language' => $faker->unique()->languageCode,
'title' => $faker->word,
'text' => $faker->text,
];
});
But then, you instead get the problem that the faker will run out of languageCodes after enough imageTexts have been created.
Current solution
This is currently solved by having two different factories for the ImageText, where one resets the unique counter for languageCodes and the seeder calls the factory which resets te unique counter before entering the for loop to create further ImageTexts. But this is code duplication, and there should be a better way to solve this.
The question
Is there a way to send the model you are saving on into the factory? If so, I could have a check inside the factory to see if the current Image has any ImageTexts attached already and if it doesn't, reset the unique counter for languageCodes. My goal would be something like this:
$factory->define(App\Models\ImageText::class, function (Faker\Generator $faker) {
$firstImageText = empty($image->imageTexts());
return [
'language' => $faker->unique($firstImageText)->languageCode,
'title' => $faker->word,
'text' => $faker->text,
];
});
Which of course currently gives:
[ErrorException]
Undefined variable: image
Is it possible to achieve this somehow?
I solved it
I searched a lot for a solution to this problem and found that many others also experienced it. If you only need one element on the other end of your relation, it's very straight forward.
The addition of the "multi column unique restriction" is what made this complicated. The only solution I found was "Forget the MySQL restriction and just surround the factory creation with a try-catch for PDO-exceptions". This felt like a bad solution since other PDOExceptions would also get caught, and it just didn't feel "right".
Solution
To make this work I divided the seeders to ImageTableSeeder and ImageTextTableSeeder, and they are both very straight forward. Their run commands both look like this:
public function run()
{
factory(App\Models\ImageText::class, 100)->create();
}
The magic happens inside the ImageTextFactory:
$factory->define(App\Models\ImageText::class, function (Faker\Generator $faker) {
// Pick an image to attach to
$image = App\Models\Image::inRandomOrder()->first();
$image instanceof App\Models\Image ? $imageId = $image->id : $imageId = null;
// Generate unique imageId-languageCode combination
$imageIdAndLanguageCode = $faker->unique()->regexify("/^$imageId-[a-z]{2}");
$languageCode = explode('-', $imageIdAndLanguageCode)[1];
return [
'image_id' => $imageId,
'language' => $languageCode,
'title' => $faker->word,
'text' => $faker->text,
];
});
This is it:
$imageIdAndLanguageCode = $faker->unique()->regexify("/^$imageId-[a-z]{2}");
We use the imageId in a regexify-expression and add whatever is also included in our unique combination, separated in this case with a '-' character. This will generate results like "841-en", "58-bz", "96-xx" etc. where the imageId is always a real image in our database, or null.
Since we stick the unique tag to the language code together with the imageId, we know that the combination of the image_id and the languageCode will be unique. This is exactly what we need!
Now we can simply extract the created language code, or whatever other unique field we wanted to generate, with:
$languageCode = explode('-', $imageIdAndLanguageCode)[1];
This approach has the following advantages:
No need to catch exceptions
Factories and Seeders can be separated for readability
Code is compact
The disadvantage here is that you can only generate key combinations where one of the keys can be expressed as regex. As long as that's possible, this seems like a good approach to solving this problem.
I built on Rkey's answer to suit my needs:
problem
I have two integer fields that together should be unique, these are product_id and branch_id.
solution
Heres's my approach:
Get the total number of products and branches. Since the ids are generated from 1, the ids shall range from 1 to the-total-count-of-items-in-the-table(s).
Create all possible unique values that can be created from product_id and branch_id by creating a string separated by a character, in this case -
Generate unique random values from this set using the randomElements function.
Split the random element back to product_id and branch_id
$branch_count = Branch::all()->count();
$product_count = Product::all()->count();
$branch_products = [];
for ($i = 1; $i <= $branch_count; $i++) {
for ($j = 1; $j <= $product_count; $j++) {
array_push($branch_products, $i . "-" . $j);
}
}
$branch_and_product = $this->faker->unique->randomElement($branch_products);
$branch_and_product = explode('-', $branch_and_product);
$branch_id = $branch_and_product[0];
$product_id = $branch_and_product[1];
return [
// other fields
// ...
"branch_id" => $branch_id,
"product_id" => $product_id
];
Your solution only works for things that can be regexified as a combination. There are many use cases where a combination of multiple separate Faker generated numbers/strings/other objects need to be unique and cannot be regexified.
For such cases you can do something like so:
$factory->define(App\Models\YourModel::class, function (Faker\Generator $faker) {
static $combos;
$combos = $combos ?: [];
$faker1 = $faker->something();
while($faker2 = $faker->somethingElse() && in_array([$faker1, $faker2], $combos) {}
$combos[] = [$faker1, $faker2];
return ['field1' => $faker1, 'field2' => $faker2];
});
For your specific question / use case, here's a solution on the same lines:
$factory->define(App\Models\ImageText::class, function (Faker\Generator $faker) {
static $combos;
$combos = $combos ?: [];
// Pick an image to attach to
$image = App\Models\Image::inRandomOrder()->first();
$image instanceof App\Models\Image ? $imageId = $image->id : $imageId = null;
// Generate unique imageId-languageCode combination
while($languageCode = $faker->languageCode && in_array([$imageId, $languageCode], $combos) {}
$combos[] = [$imageId, $languageCode];
return [
'image_id' => $imageId,
'language' => $languageCode,
'title' => $faker->word,
'text' => $faker->text,
];
});
Here is another way you can handle the unique constraint problem in table seeder class.
I will take a model called JobCategory as an example.
For JobCategory, the column "title" has a unique constraint.
In the factory class:
$factory->define(JobCategory::class, function (Faker $faker) {
return [
'title' => $faker->words(3, true),
'description' => $faker->paragraphs(2, true),
];
});
Then, in the seeder class:
class JobCategoryTableSeeder extends Seeder
{
private $failures = 0;
/**
* Run the database seeds.
*
* #return void
*/
public function run()
{
try {
factory(JobCategory::class, 30)->create();
} catch(Exception $e) {
if($this->failures > 5) {
print_r("Seeder Error. Failure count for current entity: " . $this->failures);
return;
}
$this->failures++;
$this->run(); // retry again until the number of failure is greater than 5
}
}
}
Explanation:
The idea is to catch the exception which could result from unique constraint failure and then retry seeding by calling the method recursively until an exit condition is met.
I the example above, I want to create 30 records, but due to exceptions retries, I might get more or less than 30 records.
I chose 5 retries, you can use any appropriate number of retries.
I'm using Laravel 8.x and I don't know if the column function definition that I use works in previous versions.
I had the same problem and use a diferent aproach.
I create the ImageTextFactory this way:
<?php
namespace Database\Factories;
use App\Models\ImageText;
use Illuminate\Database\Eloquent\Factories\Factory;
class ImageTextFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected $model = ImageText::class;
/**
* The number of models created till now.
*
* #var integer
*/
protected $created = 0;
/**
* Define the model's default state.
*
* #return array
*/
public function definition()
{
$this->created++;
return [
'language' => function (array $attributes) {
$count = ImageText::where(
'image_id',
$attributes['image_id']
)
->count();
$reset = $this->created == 1 && $count == 0;
return $this->faker->unique($reset)->languageCode();
},
'title' => $this->faker->word(),
'text' => $this->faker->sentence(),
];
}
}
Then I call the factory from the seeder as:
Image::factory()
->count(10)
->has(
ImageText::factory()->count(rand(0, 10))
)->create();
With the function in the definition I am able to check if there is previously defined ImageText for that image_id and how many Models are generated. As an ImageTextFactory instance is generated for each ImageFactory it automatically resets the $created counter to 0; and as the Seeders will always creates images in a sequential order, it must no generate problems.
It has a disadvantage, if the factory is called for Models that already exists, it will generate an OverflowException from Faker, as there is no new id to reset the unique constraint. It should only be generated with the has method.
I'm writing a system around an existing database structure using Laravel 4.1. The current system is based around two websites which use their own table, a and b, both of which are identical. This is an unavoidable problem until we rewrite the other system.
I need to be able to query both tables at the same time using Eloquents Query Builder, so I may need to get a list of rows from both tables, or INSERT or UPDATE from either at any time.
Currently we have a model for both tables, but no way to link between them and implement the missing methods such as all or find.
Our thought is to have an Interface which will bind these results together, however we're not sure how to go about this at all.
<?php
interface HotelInterface {
public function all();
public function find();
}
use Illuminate\Database\Model;
class Hotel implements HotelInterface {
}
?>
Is all we have so far.
I asked on the Laravel forums and got the answer I was looking for! I'm reposting here incase.
What we're actually after is a Repository which would look like this:
<?php
class HotelRepository {
public $A;
public $B;
public function __construct(A $A, B $B) {
$this->A = $A;
$this->B = $B;
}
public function find($iso = NULL, $hotelid = NULL) {
$A = $B = NULL;
if($iso !== NULL) {
$A = $this->A->where('country', $iso);
$B = $this->B->where('country', $iso);
if($hotelid !== NULL) {
$A = $A->where('id', $hotelid);
$B = $B->where('id', $hotelid);
}
}
if($hotelid !== NULL) {
if($A->first()) {
return $A->first();
}
if($B->first()) {
return $B->first();
}
}else{
return $A->get()->merge($B->get());
}
}
public function all() {
$aCollection = $this->A->all();
$bCollection = $this->B->all();
return $aCollection->merge($bCollection);
}
}
Now in the controller where I want to call this, I just add:
<?php
class HomeController extends BaseController {
public function __construct(HotelRepository $hotels) {
$this->hotels = $hotels;
}
}
And I can now use $this->hotels to access the find and all method that I created.
If the tables are identical you should only need one model, the connection is the only thing that needs to change. Have a read here: http://fideloper.com/laravel-multiple-database-connections.
There's a Eloquent method on() for specifying the connection, here's an example:
The Eloquent example looks like what you need:
$results = Model::on('mysql')->find(1);
Add both connections to your database config and then change the on() part depending on which DB you need to query.
Update: misunderstood the question
If you only need to change the table and not the database, you can use setTable()
$model = new Model
$model->setTable('b');
$model->find(1);
Although that may get confusing.
Instead, you could also define a base model and then extend it with the only difference being the table protected $table = 'b';
You can always change the default database for the one you need. I use this Config::set('database.default', 'chronos');, where chronos is one of my databases. When I need to change to the "other", I just change de database name. You can call it wherever you want. I think that what you're looking for is just switch between the databases.
You need to have two different models, one for each table on each database, though.
Let me know if I got it wrong.
I am creating a new Portlet in Yii. this widget show most recent Comment of a Issue. I want to show this only if Issue has comment and do not show it (event title) if Issue doesn't have any comments.
So my psudo code in view file as below:
Check number of comments process:
<?php
$countIssue = count($model->issues);
$i = 0; $j = 0;
while($i < $countIssue)
{
$j += $model->issues[$i]->commentCount;
$i ++;
}
?>
if ($countIssue >0 ) {
if ($j >0)
Display the widget
}
Else
Don't display the widget
I am just wonderring is my code suitable for MVC model. Could you give me a direction? Should I bring the Check number of comment process to Model or Controller , or is the above Ok fo MVC pattern?
Thank you!
First, I would have moved this logic into the run() method of your portlet class (the one that extends CPorlet).
Next, I would have defined a STAT relation in Issue class. This relation is only for counting of comments and would allow you to use a statement like:
$issue = Issue::model()->findByPk($issue_id);
// $comments_count below is exactly what you would expect... .
$comments_count = $issue->commentsCount;
Finally, combining it all I recommend the following approach inside the portlet's run() method as follows:
If ($someIssue->commentsCount > 0) {
// do something and in the end, when you want to render the portlet, you do...
$this->render("view_file_name");
}
I think there are many MVC friendly ways to do this, mainly the idea is to put data logic in Models then handle requests through Controllers while ideally Views should only be used for displaying purposes.
Personally i will use Yii named-scopes (which comes originally from Rails) to achieve the most recent filter like this:
Model:
class Comment extends CActiveRecord
{
......
public function scopes()
{
return array(
'recently'=>array(
'order'=>'create_time DESC',
'limit'=>5,
),
);
}
}
To get a list comments if only the issue has some you can do something like this in the Controller:
if($issue->comments)
$isseComments = Comment::model()->recently()->findAll(); //using the named scope to the recent ones only
$this->render('your_action',array(
.....
'issue' => $issue,
'issueComments'=>$isseComments,
));
And your View will remain tidy & clean:
if($issueComments > 0)
//Display the widget
else
// Don't