Factory creating multiple models with different number of relationships - php

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

Related

Laravel Eloquent - Model extends other model

I have a question about extending my own Models eloquent.
In the project I am currently working on is table called modules and it contains list of project modules, number of elements of that module, add date etc.
For example:
id = 1; name = 'users'; count = 120; date_add = '2007-05-05';
and this entity called users corresponds to model User (Table - users) so that "count" it's number of Users
and to update count we use script running every day (I know that it's not good way but... u know).
In that script is loop and inside that loop a lot of if statement (1 per module) and inside the if a single query with count. According to example it's similar to:
foreach($modules as $module) {
if($module['name'] == 'users') {
$count = old_and_bad_method_to_count('users', "state = 'on'");
}
}
function old_and_bad_method_to_count($table, $sql_cond) {}
So its look terrible.
I need to refactor that code a little bit, because it's use a dangerous function instead of Query/Builder or Eloquent/Model and looks bad.
I came up with an idea that I will use a Models and create Interface ElementsCountable and all models that do not have an interface will use the Model::all()->count(), and those with an interface will use the interface method:
foreach ($modules as $module) {
$className = $module->getModelName();
if($className) {
$modelInterfaces = class_implements($className);
if(isset($modelInterfaces[ElementsCountable::class])) {
/** #var ElementsCountable $className */
$count = $className::countModuleElements();
} else {
/** #var Model $className */
$count = $className::all()->count();
}
}
}
in method getModelName() i use a const map array (table -> model) which I created, because a lot of models have custom table name.
But then I realize that will be a good way, but there is a few records in Modules that use the same table, for example users_off which use the same table as users, but use other condition - state = 'off'
So it complicated things a little bit, and there is a right question: There is a good way to extends User and add scope with condition on boot?
class UserOff extends User
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(function (Builder $builder) {
$builder->where('state', '=', 'off');
});
}
}
Because I have some concerns if this is a good solution. Because all method of that class NEED always that scope and how to prevent from method withoutGlobalScope() and what about other complications?
I think it's a good solution to create the UserOff model with the additional global scope for this purpose.
I also think the solution I would want to implement would allow me to do something like
$count = $modules->sum(function ($module) {
$className = $module->getModelName();
return $className::modulesCount();
}
I would create an interface ModulesCountable that mandates a modulesCount() method on each of the models. The modulesCount() method would return either the default count or whatever current implementation you have in countModuleElements().
If there are a lot of models I would probably use a trait DefaultModulesCount for the default count, and maybe the custom version too eg. ElementsModuleCount if that is consistent.

How to get unique values from faker?

I would like to ask how to generate unique value from faker?
I know this is a familiar question actually, you may put some duplicate links e.g. link 1, link 2 but unfortunately, these links does not answer my problem.
Here is my code below. I tried unique(true) but same result.
return [
'user_id' => $this->faker->unique()->numberBetween(1, 10),
//more code here
];
Below is the result that I got. As you can see there are lots of duplicate "5" inserted.
The factory is the real issue here not the faker. Calling of factory I mean.
Let's say you have User and User_Information model for example since you have not mention any models in your question above.
I assume you call the factory like below in which it creates a model one by one separately up to 10 that makes unique() of faker useless.
\App\Models\User_Information::factory()->create(10);
My solution to this problem is to use a loop to make unique() functional.
$max = 10;
for($c=1; $c<=$max; $c++) {
\App\Models\User_Information::factory()->create();
}
NOTE: $max must not be greater to User::count(), else it will return an OverflowException error.
In my case I had a setup like this
class DomainFactory extends Factory {
protected $model = Domain::class;
public function definition() {
return ['name' => $this->faker->unique()->domainWord()]
}
}
// Seeder
for ($i = 0; $i < 10; $i++) {
$domain = Domain::factory()->create();
...
}
Which did NOT generate unique values for name because I basically create a new factory and with that a new faker in each loop run. I had to pull the factory out of the loop:
// Seeder
$factory = Domain::factory();
for ($i = 0; $i < 10; $i++) {
$domain = $factory->create();
...
}

create multiple comments for each user with faker php library model factories

I want to generate fake data with faker PHP library but I want for example create 3 comments for each user. How should I do this?
I do create 1 comment for each user with this code :
factory(App\User::class, 50)->create()->each(function ($u) {
$u->comments()->save(factory(App\Comment::class)->make());
});
I think it should be something like this:
factory(App\User::class, 50)->create()->each(function ($u) {
$u->comments()->saveMany(factory(App\Comment::class, 3)->make());
});
In case you want to create more than one comment, use ->saveMany() instead of ->save(). ->save() takes in an instance of Illuminate\Database\Eloquent\Model while ->saveMany() an instance of Illuminate\Database\Eloquent\Collection which is what factory(App\Comment::class, 3)->make() returns.
Note: I would randomize the number using rand(1, 5).
I found the solution :)
I used dd(factory(Comment::class,mt_rand(0,3))->make()) and I found that it returns the collection of 3 comments that is been created so I used foreach to create all of these 3 comments for my user using these lines of code :
$comments = factory(Comment::class,mt_rand(0,3))->make();
for ($i=0; $i < $comments->count(); $i++) {
$u->comments()->save($comments[$i]);
}

Is there a Laravel-way of testing how many times an event has fired?

I am using the base PHPUnit config file with the default TestCase.php file.
I am seeking a way to check if an event is fired multiple times during a test. Current code looks like:
function test_fires_multiple_events()
{
$this->expectsEvents(SomeEvent::class);
}
Ideally, I would want:
function test_fires_multiple_events()
{
$this->expectsEvents(SomeEvent::class)->times(3);
}
After walking through the code in MocksApplicationServices, I was able to piggyback the trait, and the fields it captures when the withoutEvents() method.
The extension would look like:
function test_fires_multiple_events()
{
$this->withoutEvents(); // From MocksApplicationServices
$c = collect($this->firedEvents)
->groupBy(function($item, $key) { return get_class($item); })
->map(function($item, $key) { return $item->count(); });
$this->assertsEqual(3, $c->get(SomeEvent::class));
}
To step through what collection is doing:
1) collect($this->firedEvents): Takes the captured events and stores them in a collection. It's important to note, that MocksApplicationServices pushes each event into an array, meaning it is already keeping count.
2) groupBy(function($item, $key) { return get_class($item); }): Groups by the class name, meaning we have a collection of arrays now.
3) map(function($item, $key) { .. }: Simply tally up the children.
This results in a structure like:
Illuminate\Support\Collection (1) (
protected items -> array (2) [
'App\Events\SomeEvent' => integer 2
'App\Events\SomeOtherEvent' => integer 1
]
)
Edit
Just to clean it up a bit - you could add the following to your base test case file:
public function assertEventFiredTimes($event, $count)
{
$this->assertEquals(
count(collect($this->firedEvents)
->groupBy(function($item, $key) { return get_class($item); })
->get($event, [])),
$count
);
}
And then in your tests:
$this->assertEventFiredTimes(SomeEvent::class, 3);
Not forgetting to add withoutEvents().
Someone in laracasts faced this same problem and found the solution:
https://laracasts.com/discuss/channels/general-discussion/testing-class-based-events-in-laravel-5
Basically: you just need to mock the Event dispatcher to expect the event class that you're using. After that you can check all you need.
Laravel got you covered with event fakes:
Event::fake(MyEvent::class)
// ...
// Pass integer as callback to assertDispatched
Event::assertDispatched(MyEvent::class, 3);
// Or use the undocumented method assertDispatchedTimes directly
Event::assertDispatchedTimes(MyEvent::class, 3)
code: https://github.com/laravel/framework/blob/master/src/Illuminate/Support/Testing/Fakes/EventFake.php#L47-L79
I think you need to inject the event dispatcher to your class an then mock it so you could do something like:
$dispatcher->shouldReceive('fire')->with(SomeEvent::class)->times(3);

How to manually create a new empty Eloquent Collection in Laravel 4

How do we create a new Eloquent Collection in Laravel 4, without using Query Builder?
There is a newCollection() method which can be overridden by that doesn't really do job because that is only being used when we are querying a set result.
I was thinking of building an empty Collection, then fill it with Eloquent objects. The reason I'm not using array is because I like Eloquent Collections methods such as contains.
If there are other alternatives, I would love to hear them out.
It's not really Eloquent, to add an Eloquent model to your collection you have some options:
In Laravel 5 you can benefit from a helper
$c = collect(new Post);
or
$c = collect();
$c->add(new Post);
OLD Laravel 4 ANSWER
$c = new \Illuminate\Database\Eloquent\Collection;
And then you can
$c->add(new Post);
Or you could use make:
$c = Collection::make(new Post);
As of Laravel 5. I use the global function collect()
$collection = collect([]); // initialize an empty array [] inside to start empty collection
this syntax is very clean and you can also add offsets if you don't want the numeric index, like so:
$collection->offsetSet('foo', $foo_data); // similar to add function but with
$collection->offsetSet('bar', $bar_data); // an assigned index
I've actually found that using newCollection() is more future proof....
Example:
$collection = (new Post)->newCollection();
That way, if you decide to create your own collection class for your model (like I have done several times) at a later stage, it's much easier to refactor your code, as you just override the newCollection() function in your model
Laravel >= 5.5
This may not be related to the original question, but since it's one of the first link in google search, i find this helpful for those like me, who are looking for how to create empty collection.
If you want to manually create a new empty collection, you can use the collect helper method like this:
$new_empty_collection = collect();
You can find this helper in Illuminate\Support\helpers.php
snippet:
if (! function_exists('collect')) {
/**
* Create a collection from the given value.
*
* #param mixed $value
* #return \Illuminate\Support\Collection
*/
function collect($value = null)
{
return new Collection($value);
}
}
Just to add on to the accepted answer, you can also create an alias in config/app.php
'aliases' => array(
...
'Collection' => Illuminate\Database\Eloquent\Collection::class,
Then you simply need to do
$c = new Collection;
In Laravel 5 and Laravel 6 you can resolve the Illuminate\Database\Eloquent\Collection class out of the service container and then add models into it.
$eloquentCollection = resolve(Illuminate\Database\Eloquent\Collection::class);
// or app(Illuminate\Database\Eloquent\Collection::class). Whatever you prefer, app() and resolve() do the same thing.
$eloquentCollection->push(User::first());
For more information about understanding resolving objects out of the service container in laravel take a look here:
https://laravel.com/docs/5.7/container#resolving
I am using this way :
$coll = new Collection();
$coll->name = 'name';
$coll->value = 'value';
$coll->description = 'description';
and using it as normal Collection
dd($coll->name);
It is better to use the Injection Pattern and after $this->collection->make([]) than new Collection
use Illuminate\Support\Collection;
...
// Inside of a clase.
...
public function __construct(Collection $collection){
$this->collection = $collection;
}
public function getResults(){
...
$results = $this->collection->make([]);
...
}
What worked for me was to name the use namespace and instantiate it directly:
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
# Usage
$this->latest_posts = new EloquentCollection();
Allowed me to merge two data subsets of eloquent collection results, this maintains the relationships - a regular collection (collect()) loses relationship and probably some more metadata.
$limit = 5;
$this->latest_posts = new EloquentCollection();
$pinned_posts = PinnedPostReference::where('category', $category)->get();
if($pinned_posts->count() > 0) {
foreach($pinned_posts as $ppost) {
$this->latest_posts->push($ppost->post);
}
}
# Another Eloquent result set ($regular_posts)
foreach($regular_posts as $regular_post) {
$this->latest_posts->push($regular_post);
}

Categories