guava / laravel-populator
A laravel package for seeding testing and production data.
Requires (Dev)
- larastan/larastan: ^2.9
- laravel/pint: ^1.16
- orchestra/testbench: ^8.0
- phpstan/phpstan: ^1.11
- spatie/phpunit-watcher: ^1.24
README
Laravel populator's goal is to provide an unified way to populate your database with predefined data, while keeping your migrations intact.
There's a lot of tutorials and opinions about how to do that. Some people manually insert data into the database inside migrations and some use seeders. In both cases, a lot of people tend to use Model::create()
, which at first sight seems like the easiest way and it even works. However, if you change your Model's structure in the future, there's a chance your migrations stop working. For example when tinkering with the fillable
property.
Laravel populator solves this problem while maintaining a great developer experience when dealing with your database data.
Installation
You can install the package with composer:
composer require guava/laravel-populator
You can optionally publish the database migrations if you plan to enable tracking
php artisan vendor:publish --provider 'Guava\LaravelPopulator\PopulatorServiceProvider' --tag 'migrations' php artisan migrate
or publish the config
php artisan vendor:publish --provider 'Guava\LaravelPopulator\PopulatorServiceProvider' --tag 'config'
which currently provides the following options
config/populator.php
return [ 'tracking' => false, 'population_model' => '\Guava\LaravelPopulator\Models\Population', ];
How it works
There are three major terms that Laravel Populator introduces:
Populators: These are basically named folders inside your /database/populators
folder. They contain bundles.
Bundles: A bundle is collection of records of specific model. Bundles are part of the populator and are used to define the model of the records, default attributes or mutations.
Records: A record is the smallest unit and it represents a database record waiting to be populated. A record is a php file, which returns an array with key => value
pairs describing your model / databse entry.
Example
2023_01_20_203807_populate_initial_data.php
:
return new class extends Migration { public function up() { Populator::make('initial') // // bundles are located in /database/populators/initial/ ->environments(['local', 'testing']) ->bundles([ Bundle::make(User::class) ->mutate('password', fn($value) => Hash::make($value)) ->records([ 'admin' => [ 'name' => 'Administrator', 'email' => 'admin@example.tld', 'password' => 'my-strong-password', ], ]), Bundle::make(Tag::class, 'my-tags'), // records are located in /database/populators/initial/my-tags/ Bundle::make(Post::class) // records are located in /database/populators/initial/post/ ->generate('slug', fn(array $attributes) => Str::slug($attributes['name'])), Bundle::make(Permission::class) // records are located in /database/populators/initial/permission/ ->default('guard_name', 'web'), Bundle::make(Role::class) // records are located in /database/populators/initial/role/ ->default('guard_name', 'web'), ]); } }
example record /database/populators/initial/post/example-post.php
:
<?php return [ 'name' => 'Example post', 'content' => 'Lorem ipsum dolor sit amet', 'author' => 'admin', // could also be ID or specific column:value, like email:admin@example.tld 'tags' => ['Technology', 'Design', 'Off-topic'], ];
Usage
Using the generator command
Work in Progress
Manual
First you need to create a migration using:
php artisan make:migration populate_initial_data
Inside of the migration, you have to define your populator and it's bundles:
Populator::make('v1') ->bundles([ Bundle::make(User::class), ]) ->call()
Now you need to create the directory structure. Since we named our populator initial
and only have one bundle for the User
model, our structure will be:
/database/
/populators/
/initial/
/user/
Now we can add as many records to the user bundle as we'd like. To do so, simply create a php file
in the corresponding folder and name it how you want (it has to be unique across the bundle).
Let's create john-doe.php
to create our first user, John doe:
<?php return [ 'name' => 'John Doe', 'email' => 'john.doe@example.tld', 'password' => 'my-strong-password', ];
That's it! When the migration is run, it will create all records from the populator's bundles.
Please note that the password will not be hashed, in order to hash all passwords or to learn more about all the customization options, please refer to the documentation below.
Populators
Populators serve as a group of bundles of records that you want to populate. The reason for this kind of grouping is that during the lifetime of your application, you might want to add another batch of data to your application in mid-production. As we know, developers hate to come up with names and to avoid ending up with bundles named users1
, users2
, users-new
, yet-another-batch-of-users
, we decided to group them into populators so you only have to come up with a single name. :)
In case you end up needing to seed data in mid-production, we recommend naming your populators according to your version, such as v1.0
, v1.1
, v2.0
and so on.
Calling a populator
A populator is the entry point to everything this package offers. You can call the populator from anywhere you want, but we recommend calling them from migrations, like this:
Populator::make('v1') ->bundles([ // Your bundles here]) ->call()
This will call your populator and all it's defined bundles (more information in the Bundles section)
Reversing a populator
The records inserted by a populator can be removed if tracking is enabled when the populator was run.
Populator::make('v1') ->bundles([//your bundles to reverse or leave blank for all bundles in the populator]) ->rollback()
Rollbacks will filter using the following condition
- Population populator name
- Bundle model classes
- Bundle name
This allows you to control what bundles are rolled back from a populator.
For example you can rollback a subset of the bundles from the populator
Populator::make('initial') ->bundles([ Bundle::make(User::class), Bundle::make(Post::class), ]) ->call(); //User and Post entries now exist Populator::make('initial') ->bundles([Bundle::make(Post::class)]) ->rollback(); //Post entries were removed
Environment
Populators can be set to be executed only on specific environments. You might most likely want to seed different data for your local environment and your production environment.
You can easily do so using the environments
method:
Populator::make('v1') ->environments(['local']) ... ->call()
This populator will only be executed on the local environment.
Bundles
Bundles are like blueprints for all your records, they define default attributes or common modifiers so you don't need to repeat them in every record. This is done by chaining additional methods described below.
Creating a bundle is as simply as this:
Bundle::make(Model::class, 'optional-name'),
Passing a name is optional and defines the name of the directory inside the populator's directory. If omitted, the name will be auto-generated from the model's class name. For example for the model Foo
the name will be foo
, for the model FooBar
it would be foo-bar
.
Environment
Similar to populators, you can also define specific environments for each bundle separately. To do so, chain the environments
method on the Bundle itself:
Bundle::make(User::class) ->environments(['production'])
Mutators
You can define mutators on any of the model's attributes in order to mutate the value before it's stored in the database.
For example you might want to hash all passwords:
Bundle::make(User::class) ->mutate('password', fn($value) => Hash::make($value))
Default
You can define default attributes for all records if they are not set. This is useful if you have a lot of records with the same attributes.
For example, you might want to add guard_name
to all permissions (spatie/laravel-permission):
Bundle::make(Permission::class) ->default('guard_name', 'web'),
Generated
In case you want to have default or generated values for an attribute, you can chain a generated()
method to your Bundle.
A common use case might be if you for example wanted to generate a slug from from another attribute:
Bundle::make(Post::class) ->generate('slug', fn($attributes) => Str::slug($attributes['name']))
Records
If you only have a small amount of records to create, it might be cumbersome to create the whole directory structure. For these cases you can create them from inside your migration using the record
and records
methods.
For example, to quickly create an admin account, you could do the following:
... Bundle::make(User::class) ->mutate('password', fn($value) => Hash::make($value)) ->records([ 'admin' => [ 'name' => 'Administrator', 'email' => 'admin@example.tld', 'password' => 'admin123', ], ]); ...
Override insert behavior
If you need to customize the insertion behavior for records you can call performInsertUsing()
on a bundle.
For example, to perform an updateOrCreate instead of an insert
Bundle::make(User::class) ->performInsertUsing(function (array $data, Bundle $bundle) { return $bundle->model::updateOrCreate( Arr::only($data, ['email']), Arr::except($data, ['email']) )->getKey(); });
Relations
Records can of course have relations with other records. Currently supported relations are:
- one to one and it's inverse
- one to many and it's inverse
- many to many (
belongsToMany
) - polymorphic one to one and it's inverse
- polymorphic one to many and it's inverse (
morphMany
)
Referencing other records
Other records can be referenced in multiple ways, the package tries all three of them in this specific order.
By their identifier (key)
This works only from within the same populator (across all bundles). The key is the name of the file, so in case of the john-doe.php
example file from earlier, the key would be john-doe
.
In case you supplied the records to the bundle directly via the records()
method, the key is the array key you set.
By their primary key
If no key was found, the package assumes the provided key is the primary key and attempts to find the record in the database. If the record exists, the primary key will be used.
by a (preferably unique) column
You can also reference records using any column you like. For example, to reference John Doe from other records, you could reference their e-mail using email:john.doe@example.tld
. The package will then attempt to find the record with that e-mail address.
One to One
Let's say we have a User
model that has one Address
relation. You can define the relation in the Address
bundle like this:
<?php return [ 'street' => 'Example Street', 'city' => 'Example City', 'state' => 'Example State', 'zip' => '12345', 'user' => 'admin', ];
This will attempt to associate the address with the user record admin
.
You could also reference the user their primary key:
return [ ... 'user' => 1, ];
Or using a column of your choice:
return [ ... 'user' => 'email:john.doe@example.tld', ];
One to One (Inverse)
Let's say we have a User
model that has one Address
relation. You can create an Address
from the User
record:
<?php return [ 'name' => 'Webmaster', 'email' => 'webmaster@example.tld', 'password' => 'my-strong-password', 'address' => [ 'street' => 'Example street', 'city' => 'Example city', 'zip' => '12345', 'state' => 'Example State', ] ];
This will create a user and associate it with a newly created address record with the specified attributes.
One to Many
Imagine we had a Posts
bundle that had a one to many relation to the author
(John Doe) created in the very first example. We simply use the record's key to associate it with the post.
<?php return [ 'name' => 'Example post', 'slug' => 'example-post', 'author' => 'john-doe', ];
Many to Many
If we wanted to modify the above example and also add a many to many
relation to tags
, it's as simple as this:
<?php return [ 'name' => 'Example post', 'slug' => 'example-post', 'author' => 1, 'tags' => ['technology', 'design', 'off-topic'] ];
This will attach the post with the specified tags.
Polymorphic One to Many
Now imagine we had a Comment
model which has a polymorphic relation to the Post
model.
Adding such a relationship is similar to a belongs to relation, but we need to pass an array with the key/primary key AND the class of the morph.
<?php return [ 'name' => 'A useful comment', 'author' => 1, 'post' => ['example-post', Post::class], ];
Polymorphic One to Many (Inverse)
Last but not least, assuming we have Like
model with a polymorphic relation to our Post model. Let's take another look at our example-post
record and extend it one last time by adding an inverse likes
relation.
<?php return [ 'name' => 'Example post', 'slug' => 'example-post', 'author' => 1, 'tags' => ['technology', 'design', 'off-topic'], 'likes' => [ [ 'user' => 'john.doe@example.tld', ], [ 'user' => 'jennifer.doe@example.tld', ] ] ];
This will automatically create two Like's with the defined attributes and a relationship to the post they have been created in.
Tracking inserted models
Inserted models by a populator can be tracked in the database by enabling the tracking feature in Laravel populator.
\Guava\LaravelPopulator\Models\Population::first() ->populatable; //provides access to the model that was inserted by Laravel populator.
Laravel populator provides a trait [HasPopulation.php](src%2FConcerns%2FHasPopulation.php)
to access
the Population from models
class User extends Model implements TracksPopulatedEntries { use HasPopulation; }
The model then has access to the Population relationship by $model->population
Enabling tracking
Tracking can be enabled either by enabling the feature inside the published configuration file or by enabling it in the boot method of a service provider.
Enabling by config:
config/populator.php
return [ 'tracking' => true, //...other options ];
Enabling by provider:
AppServiceProvider.php
public function boot() { \Guava\LaravelPopulator\Facades\Feature::enableTrackingFeature(); }
Marking models for tracking
Models must implement TracksPopulatedEntries.php to opt in for saving populations.
class User extends Model implements TracksPopulatedEntries { }
Swapping the Population model
The population model can be changed by setting the model class inside the published configuration file or by enabling it in the boot method of a service provider
Enabling by config:
config/populator.php
return [ 'population_model' => SomeModel::class, //...other options ];
Enabling by provider:
AppServiceProvider.php
public function boot() { \Guava\LaravelPopulator\Facades\Feature::customPopulationModel(SomeModel::class); }