Package Data | |
---|---|
Maintainer Username: | guidocella |
Package Create Date: | 2016-12-16 |
Package Last Update: | 2024-03-14 |
Home Page: | |
Language: | PHP |
License: | MIT |
Last Refreshed: | 2024-11-19 03:20:21 |
Package Statistics | |
---|---|
Total Downloads: | 55,683 |
Monthly Downloads: | 521 |
Daily Downloads: | 25 |
Total Stars: | 74 |
Total Watchers: | 6 |
Total Forks: | 8 |
Total Open Issues: | 0 |
This is a package to populate Laravel's Eloquent ORM's models by guessing the best Faker formatters for their attributes from their columns' names and types. It is based on Faker's other ORMs' adapters, but was released as a stand-alone package because several functionalities were added.
Require this package with Composer
$ composer require --dev guidocella/eloquent-populator
Or manually add it to the development dependencies in your composer.json
and run composer update
. You may want to substitute it for Faker, since it will be required by Populator.
"guidocella/eloquent-populator": "^2"
Either type hint EloquentPopulator\Populator
in your DatabaseSeeder
's run
method to have it dependency injected if you're using Laravel 5.4+
public function run(EloquentPopulator\Populator $populator)
{
or call the populator
helper with no arguments to get a Populator instance.
$populator = populator();
Now for each model you want to create, call add
passing the class and the number of instances to generate. Let's add 10 users and 5 posts.
$populator->add(User::class, 10)
->add(Post::class, 5);
Populator will try to guess the best Faker formatter to use for each column based on its name. For example, if a column is called first_name
, it will use $faker->firstName
. If unable to guess by the column's name, it will guess the formatter by the column's type. For example, it will use $faker->text
for a VARCHAR
column, or a Carbon instance for a TIMESTAMP
.
To customize the values of certain columns, pass them as the 3rd argument. If they're Faker formatters, you'll have to wrap them in a closure to get different values for each row.
$populator->add(Post::class, 5, [
'content' => function () use ($faker) {
return $faker->paragraph;
}
]);
Any closure passed will receive the model instance and the previously inserted primary keys.
$populator->add(Post::class, 5, [
'user_id' => function ($post, $insertedPKs) {
return array_random($insertedPKs[User::class]);
},
'user_type' => function ($post) {
return $post->user->type;
},
]);
The model received by the closures will have non-closure attributes and closure attributes of columns that come before in the database already set.
You can also pass an array of functions as the 4th argument and they'll be called before the model's insertion.
$populator->add(Post::class, 5, [], [
function ($post, $insertedPKs) {
$post->doSomethingBeforeSaving();
},
]);
If only the class name is passed, the number of models to populate defaults to 1.
Finally, call seed
to populate the database with the added models.
$populator->add(User::class, 10)
->add(Post::class, 5)
->seed();
If a column that wasn't overridden is nullable, each value inserted by seed
will have a 50% of being set to null or to the guessed formatter.
seed
returns the inserted primary keys and runs one insert per 500 rows of every model to speed up the seeding (the chunking in blocks of 500 rows is because SQL limits how many rows you can insert at once).
Even though it bulk inserts the models, timestamps are still populated with random datetimes since their formatters are guessed along those of the other columns, and even mutators and JSON casts will work (internally Populator fills a model and gets its attributes every time to bulk insert them later), however Eloquent events won't fire.
If you want the events to be dispatched, you can use execute()
as an alternative to seed()
. It creates the added models one by one, and returns all the created models or collections, depending on whether their quantity was 1 or greater, indexed by model class name.
$createdModels = $populator->add(User::class)
->add(Post::class, 5)
->execute();
$userModel = $createdModels[User::class];
$postCollection = $createdModels[Post::class];
execute()
doesn't make nullable columns optional, since it is more likely to be used for testing than for seeding, and when testing creating models with predictable values can be more useful.
If a model shares a relationship with another one that was previously added, Populator will associate them.
If a model belongs to another one that was added before it, Populator will associate the child model to a random one of its potential owners.
$populator->add(User::class, 5)
->add(Phone::class);
$phone = $populator->execute()[Phone::class];
$phone->user; // One of the users that were created.
If a model has a Morph To relation to models that were added before it, Populator will associate it to a random one of them.
$populator->add(Post::class, 5)
->add(Video::class, 5)
->add(Comment::class);
$comment = $populator->execute()[Comment::class];
$comment->commentable; // One of the posts or videos that were created.
Associating multiple Morph To relations on a single model is currently not supported.
If a model has a Belongs To Many or inverse Morph To Many relation to another one that has already been added, by default seed
will attach a number between 0 and the quantity specified for the related model of the related model's instances to it.
$insertedPKs = $populator->add(Role::class, 5)
->add(User::class)
->seed();
$user = User::find($insertedPKs[User::class][0]);
$user->roles->count(); // 0 to 5.
To attach a specific number of models, call attachQuantities
after add
with an array of the quantities indexed by the class names of the related models.
$populator->add(User::class)->attachQuantities([Role::class => 5, Club::class => 0]);
Any extra column on pivot tables will have its formatter guessed and be populated.
$populator->add(Role::class, 5)
->add(User::class);
$user = $populator->execute()[User::class];
// Assume that the role_user table has an expires_at timestamp.
$user->roles[0]->pivot->expires_at; // A random datetime.
You can override the formatters of extra attributes of pivot tables with pivotAttributes
. It accepts an array with the related models' class names as keys, and the arrays of attributes as values.
$populator->add(User::class)->pivotAttributes([
Role::class => [
'expires_at' => Carbon::now(),
],
]);
In order not to repeat attributes across your seeders and your tests, you can define them in Laravel's Model Factory, and Populator will merge them with the guessed ones. Populator uses the Model Factory only as a convenient place to store custom attributes to reuse. You don't have to define all the other attributes for which the guessed formatters are good enough.
$factory->define(User::class, function (Faker\Generator $faker) {
return [
'avatar' => $faker->imageUrl
];
});
$user = $populator->add(User::class)->execute()[User::class];
$user->imageUrl; // The URL of a random image.
$user->email; // A random email, since all of the other columns' formatters are guessed as normal.
Factory states are supported as well.
$populator->add(User::class)->states('premium', 'delinquent');
States will work even if you define them without defining their model in the factory. Populator will create a dummy definition of the model automatically if you do so.
Populator will call closure attributes in factory definitions and states together with those of custom attributes and with the same arguments. So like with custom attributes, they will receive the model with non-closure attributes and the return values of closure attributes that come before in the database set. This means that you can do something like this:
$factory->define(Post::class, function (Faker\Generator $faker) {
return [
'user_type' => function ($post, $insertedPKs) {
return $post->user->type;
},
];
});
$post = $populator->add(Post::class, ['user_id' => 1])->execute()[Post::class];
$post->user_type; // The type of user 1.
When testing you can use make
and create
. They call execute
internally, and return the last added model or collection.
Like with those of the Model Factory, the difference between them is that create
persists the model to the database, while make
doesn't.
You can chain these methods from the populator
helper, whose arguments, like factory
, can be class|class,state|class,quantity|class,state,quantity
. The custom attributes can be passed to make
and create
.
populator(User::class)->make(); // Same as $populator->add(User::class)->make()
populator(User::class, 'admin')->create();
populator(User::class, 10)->make(['name' => 'custom name']);
populator(User::class, 'admin', 10)->create(['name' => 'custom name']);
add
returns an instance of EloquentPopulator\ModelPopulator
which manages single models and has these make
and create
methods that accept only the custom attributes. But you can also call make
and create
directly on EloquentPopulator\Populator
as a shortcut. The make
and create
in EloquentPopulator\Populator
have the same signature as add
.
$post = $populator->make(Post::class, ['content' => 'custom content']);
// Same as $populator->add(Post::class)->make(['content' => 'custom content'])
$users = $populator->create(User::class, 10);
$populator->create()
/ make()
can be used as an alternative to calling the helper multiple times for saving single models before or after seed
, for example the admin user (don't try to add the same model multiple times as it will overwrite the previous one), and for Morph To and Belongs To Many relations, for which we'll see examples later.
When creating a single a model, you can also pass custom attributes or a state as the second argument of create
, make
or add
.
$populator->make(User::class, ['admin' => true]);
$populator->create(User::class, 'admin', $otherAttributes);
Furthermore, you can call raw
to make
a model and convert it to an array.
$user = $populator->raw(User::class, ['name' => 'foo']);
$user['name']; // "foo"
execute
is mostly useful to add a model and its children, and then return the parent model.
$parent = populator()->add(Parent::class)->add(Child::class)->execute()[Parent::class];
Owning models of Belongs To relations are created recursively without having to add
them.
$post = populator(Post::class)->make();
$post->user; // The created user.
$post->user->company; // The created company.
If you don't want an owning model to be created, specify the value of the foreign key.
$post = populator(Post::class)->make(['user_id' => null]);
$post->user; // null
$post = populator(Post::class)->make(['user_id' => 1]);
$post->user->id; // 1
For Morph To relations, unless you pass the foreign key and morph type as custom attributes or define them in the factory, you'll have to add the owning model to be associated before creating the child, since Populator has no way of knowning who its owners are otherwise.
$comment = populator()->add(Post::class)->make(Comment::class);
// Or
$comment = populator(Post::class)->make(Comment::class);
Notice how even if make
and create
are chained from add
or the helper with a class passed, if the first argument is a string, Populator interprets it as a different model class to create rather than the one that was just added, and redirects the call to the EloquentPopulator\Populator
versions of these methods. This way you can easily populate Morph To and Belongs To Many relations in one line, or specify custom attributes of a parent model before creating its child.
By default Populator will attach all of the added instances of many-to-many related models when using create
or execute
.
$user = populator()->add(Role::class, 20)->create(User::class);
$user->roles->count(); // 20.
You can call setters before create
like this:
$user = populator()->add(Role::class, 20)
->add(User::class)->pivotAttributes([
Role::class => [
'expires_at' => Carbon::now(),
],
])
->create();
If a model uses the Themsaid\Multilingual\Translatable
or the Dimsav\Translatable\Translatable
trait, Populator will translate it in all of their configured languages.
$product = populator(Product::class)->create();
// Let's assume en is the current locale.
$product->name; // "Fuga voluptas illo qui voluptates aut ipsam."
$product->nameTranslations->es; // "Nulla vero qui magni quo aut quo." (Multilingual)
$product->{'name:es'}; // "Nulla vero qui magni quo aut quo." (Translatable)
When using make
with Laravel Translatable the translations will still be created, but they won't be saved to the database.
You can override the formatters of one locale with Laravel's attribute->locale JSON syntax from the custom attributes or factory definition/states.
$factory->define(Product::class, function () {
return [
'name->en' => 'English name'
];
});
$product = populator(Product::class)->make(['name->es' => 'Spanish name']);
$product->name; // "English name"
$product->nameTranslations->es; // "Spanish name"
$product->nameTranslations->fr; // "Omnis ex soluta omnis."
You can override the formatters of one locale with Translatable's attribute:locale syntax from the main model's custom attributes or factory definition/states. :locale can be omitted for the current locale.
$factory->define(Product::class, function () {
return [
'name' => 'English name';
];
});
$product = populator(Product::class)->make(['name:es' => 'Spanish name']);
$product->name; // "English name"
$product->{'name:es'}; // "Spanish name"
$product->{'name:fr'}; // "Omnis ex soluta omnis."
You can override the formatters of all locales with translatedAttributes
.
$product = populator(Product::class)->translatedAttributes(['name' => 'custom name'])->make();
$product->name; // "custom name"
$product->nameTranslations->es; // "custom name"
You can override the formatters of all the translations with translationAttributes
, or through the translation model's factory definition/states, which are merged like those of regular models.
$product = populator(Product::class)->translationAttributes(['name' => 'custom name'])->make();
$product->{'name:en'}; // "custom name"
$product->{'name:es'}; // "custom name"
$factory->define(ProductTranslation::class, function () {
return ['name' => 'custom name'];
});
$product = populator(Product::class)->make();
$product->{'name:en'}; // "custom name"
$product->{'name:es'}; // "custom name"
To apply states to the translation model, use translationStates
.
$factory->state(ProductTranslation::class, 'laravel', ['name' => 'Laravel']);
populator(Product::class)->translationStates('laravel')->make()->name; // "Laravel"
If you don't want to translate in all of your available languages, pass an array with the locales in which you want the translations to be created in to translateIn
. If you don't want any translation, pass an empty array.
$populator->translateIn(['en']);
$product = populator(Product::class)->make();
$product->nameTranslations->toArray(); // Only the en key. (Multilingual)
$product->translations; // Only the English translation. (Translatable)
Calling translateIn
on Populator sets the default locales, but you can also chain it from add
to set the locales only for a certain model.
$populator->translateIn([])
->add(Product) // Product won't be translated.
->add(Role::class)->translateIn(['en', 'es']); // Role will be translated in English and Spanish.
->execute();