Laravel Polymorphic Database Seeder Factory - laravel-5

How can I create a database seeder factory for the following configuration?
User
// create_users_table.php
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
...
}
// User.php
public function notes()
{
return $this->morphMany('App\Note', 'noteable');
}
Complex
// create_complex_table.php
Schema::create('complex', function (Blueprint $table) {
$table->increments('id');
...
}
// Complex.php
public function notes()
{
return $this->morphMany('App\Note', 'noteable');
}
Notes
// create_notes_table.php
Schema::create('notes', function (Blueprint $table) {
$table->increments('id');
$table->integer('noteable_id');
$table->string('noteable_type');
...
}
// Note.php
public function noteable()
{
return $this->morphTo();
}
I am struggling to see the most robust way of ensuring that I am not just filling in random id's that may not exist.

I have improved upon HyperionX's answer and removed the static elements from it.
$factory->define(App\Note::class, function (Faker $faker) {
$noteable = [
App\User::class,
App\Complex::class,
]; // Add new noteables here as we make them
$noteableType = $faker->randomElement($noteables);
$noteable = factory($noteableType)->create();
return [
'noteable_type' => $noteableType,
'noteable_id' => $noteable->id,
...
];
});
Basically, we pick one of the noteable classes at random, then call it's own factory to get an instance of noteable, thus we get rid of the staticness of the OP's answer.

If you are using a morph map the given solutions won't work because the type won't be the same as the class name.
This will work in combination with a morph map.
Until Laravel 7
$factory->define(App\Note::class, function (Faker $faker) {
$noteable = $faker->randomElement([
App\User::class,
App\Complex::class,
]);
return [
'noteable_id' => factory($noteable),
'noteable_type' => array_search($noteable, Relation::$morphMap),
...
];
});
From Laravel 8
public function definition(): array
{
/** #var class-string<\App\Models\User|\App\Models\Complex> $noteable */
$noteable = $this->faker->randomElement([
App\Models\User::class,
App\Models\Complex::class,
]);
return [
'noteable_type' => array_search($noteable, Relation::$morphMap),
'noteable_id' => $noteable::factory(),
];
}
More information about morph map could be found here: https://laravel.com/docs/8.x/eloquent-relationships#custom-polymorphic-types

Although a bit more static than I would like, here is my solution:
I created exactly 20 models of each class, that way I could ensure the Notes that are created don't try to link to something that may not exist, leaving a dangling Note object.
// NotesFactory.php
$factory->define(App\Note::class, function (Faker $faker) {
$noteable = [
App\User::class,
App\Complex::class,
];
return [
'noteable_id' => $faker->numberBetween(0,20),
'noteable_type' => $faker->randomElement($noteable),
...
];
});

class CandidateFactory extends Factory {
protected $model = \App\Models\Candidate::class;
public function definition() {
$applicants = [
Contact::class,
Advertiser::class,
];
/** #var Model $applicant */
$applicant = Arr::random( $applicants )::factory()->create();
return [
'applicant_type' => $applicant->getMorphClass(),
'applicant_id' => $applicant->getKey(),
];
}
}
Then
/**
* #test
*/
public function candidate_has_applicants() {
$candidate = Candidate::factory()->create();
$this->assertInstanceOf( Candidate::class, $candidate );
$this->assertInstanceOf( Authenticatable::class, $candidate->applicant );
}

Updated Answer
If you don't care about controlling which morphable model is created:
public function definition()
{
return [
'content' => $this->faker->paragraph(),
'noteable_id' => function (array $attributes) {
return $attributes['noteable_type']::factory();
}),
'noteable_type' => $this->faker->randomElement([
Complex::factory(),
User::factory()
]),
'title' => $this->faker->sentence()
];
}
Original answer
I have another solution that does not imply the use of the randomElement, which is good by the way, but can be problematic when you need control on the morphable model that is being created. You still need to create model factories for the Note, User and Complex models. Then the run method of the Databaseeder class would look like this:
public function run()
{
$userNotes = Note::factory()->count(10)->for(
User::factory(), 'noteable'
)->create();
$complexNotes = Note::factory()->count(10)->for(
Complex::factory(), 'noteable'
)->create();
}
There is another approach by using the factory states. You still need to define model factories for your 3 models. Additionally you define two state transformation methods in the NoteFactory class.
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class NoteFactory extends Factory
{
public function definition()
{
return [
'content' => $this->faker->paragraph(),
'title' => $this->faker->sentence()
];
}
public function forComplex()
{
return $this->state(function (array $attributes) {
return [
'noteable_type' => Complex::class,
'noteable_id' => Complex::factory()
];
});
}
public function forUser()
{
return $this->state(function (array $attributes) {
return [
'noteable_type' => User::class,
'noteable_id' => User::factory()
];
});
}
}
In this case the Databaseeder class run method would look like this:
public function run()
{
$userNotes = Note::factory()->count(10)->forUser()->create();
$complexNotes = Note::factory()->count(10)->forComplex()->create();
}

You could also do this without temporary variables like this:
$factory->define(App\Note::class, function (Faker $faker) {
return [
'noteable_type' => $faker->randomElement([
App\User::class,
App\Complex::class,
]),
'noteable_id' => function (array $note) {
return factory($note['noteable_type']);
},
...
];
})

You can create 10 user with 10 notes then create 10 complex with 10 notes.
like this 👇
public function run()
{
factory(User::class,10)->create()->each(
fn($user)=>
factory(Note::class,10)
->create(["noteable_id"=>$user,"noteable_type"=>User::class]),
);
factory(Complex::class,10)->create()->each(
fn($complex)=>
factory(Note::class,10)
->create(["noteable_id"=>$complex,"noteable_type"=>Complex::class]),
);
}

If you already created instances of the concrete Notables, perhaps via different factories, you may don't want to create any more new instances of it. In this case, you could extend Barracuda's solution:
$factory->define(App\Note::class, function (Faker $faker) {
$noteable = [
User::class,
Complex::class,
];
$noteableType = $faker->randomElement($noteable);
if ($noteableType === User::class) {
$noteableId = User::all()->random()->id;
} else {
$noteableId = Complex::all()->random()->id;
}
return [
'noteable_type' => $noteableType,
'noteable_id' => $noteableId,
...
];
});
I realise this would require a little bit of additional overhead to maintain the if/else branches but you won't have the problem of using IDs that don't exist.
Perhaps there is also a more elegant and generic way to get the Model::all() based on the Model::class, however I am unaware of it.

My preference is to use it this way, according to the factory structure, in new versions of Laravel.
$notable = $this->faker->randomElement([
[
'id' => User::all()->random(),
'type' => User::class,
],
[
'id' => Complex::all()->random(),
'type' => Complex::class,
]
]);
return [
'notable_id' => $notable['id'],
'notable_type' => $notable['type'],
...
];

You can extend model factory with state and pass to it related model, in state grab needed information and return merged results with default definition()
class NoteFactory extends Factory
{
protected $model = Note::class;
public function definition()
{
return [
'title' => $this->faker->sentence,
'note' => $this->faker->sentence,
//...
];
}
//new method that specify related model
public function forModel($model)
{
return $this->state(function () use ($model){
return [
'noteable_type' => $model->getMorphClass(), //return App\Lead etc
'noteable_id' => $model->id,
];
});
}
}
//usage
Note::factory()
->forModel($lead) //created method passing instance of App\Lead object
->create();

Related

Laravel Algolia Scout, whereIn on relationships

I am working on a Laravel project. I am using Scout based on Algolia. But I struggling to apply whereIn on the relationships. I have 2 models as follow.
Place.php
class Place extends Model
{
use Searchable, Localizable;
protected $with = [
'images',
'phones',
'emails',
'categories'
];
protected $casts = [
'is_featured' => 'boolean'
];
public function categories()
{
return $this->belongsToMany(Category::class, 'place_category');
}
public function searchableAs()
{
return "places_index";
}
public function toSearchableArray()
{
$record = $this->toArray();
$record['_geoloc'] = [
'lat' => $record['latitude'],
'lng' => $record['longitude'],
];
$record['categories'] = $this->categories->map(function ($data) {
return [
'id' => $data['id'],
'en_name' => $data['en_name'],
'mm_name' => $data['mm_name'],
];
})->toArray();
unset($record['created_at'], $record['updated_at'], $record['latitude'], $record['longitude']);
unset($record['images'], $record['phones'], $record['emails']);
return $record;
}
}
Category.php
class Category extends Model
{
use Searchable;
protected $touches = [
'places',
];
public function places()
{
return $this->belongsToMany(Place::class, 'place_category');
}
}
Now, I am searching the Place models/ data filtering by category. As you can see, I have also indexed the categories with places in toSearchableArray method.
I am trying to achieve something like this.
Place::search($keyword)->whereIn('categories', ????);//how can I filter by the Ids here
How can I do that?

Laravel seed create wrong number of rows in DB

I'm trying to create a seeder with relationships
public function run()
{
factory(Company::class, 10)->create()->each(function ($company){
$company->buildings()->saveMany(factory(Building::class, 5)->create()->each(function ($building){
$building->facilities()->saveMany(factory(App\Models\Facility::class,5)->make());
}));
});
}
That code should creeate 10 companies and 5 buildings for each company and 5 facilities for each building.
companies 10
buildings 50
facilities 250
but I get
companies 300
buildings 310
facilties 250
which doesn't make sense
my factories:
$factory->define(Company::class, function (Faker $faker) {
$name = $faker->lastName;
$company = $name.' Company';
return [
'name' => $company,
'shortName' => $name
];
});
$factory->define(Building::class, function (Faker $faker) {
return [
'name' => 'Hotel '.$faker->lastName,
'company_id' => function () {
return factory(App\Models\Company::class)->create()->id;
}
];
});
$factory->define(Facility::class, function (Faker $faker) {
$elements = array('lavabo','ducha');
return [
'name' => $faker->randomElement($elements).' '.$faker->numerify('Habitacion ###'),
'building_id' => function () {
return factory(App\Models\Building::class)->create()->id;
}
];
});
And the databaseSeeder.php
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* #return void
*/
public function run()
{
$this->call(UserSeeder::class);
$this->call(CompaniesSeeder::class);
}
}
Solution
The problem comes from here:
$company->buildings()->saveMany(factory(Building::class, 5)->create()->each(function ($building){
$building->facilities()->saveMany(factory(App\Models\Facility::class,5)->make());
}));
Every time you create a new building, and facility, it is creating a new company in your factory as well.
Simply pass the appropriate parameters to your create function to let Faker know to not create new company and building.
Change from:
factory(Building::class, 5)->create()
To:
factory(Building::class, 5)->create(['company_id' => $company->id]);
And apply the same logic for Facility.
factory(App\Models\Facility::class,5)->make(['building_id' => $building->id])
Side note
You can also simplify your factories by replacing your callback function like this:
$factory->define(Building::class, function (Faker $faker) {
return [
'name' => 'Hotel '.$faker->lastName,
'company_id' => factory(Company::class),
];
});

how to add extra data into create method with laravel

public function store(Request $request) {
$user = Book::create([
'user_id' => auth()->id(),
'name => $request->name,
'year => $request->year
)];
}
The above code is able to store into Database.
I want to know how to add below extra data TOGETHER.
I found out that merge was not working as it is not collection.
Tried to chain but was not working.
public function data() {
$array = [
'amount' => 30,
'source' => 'abcdef',
];
return $array;
}
You can catch create Event in Model.
This code may can help you.
/**
* to set default value in database when new record insert
*
*/
public static function bootSystemTrait()
{
static::creating(function ($model) {
$model->amount = 30;
});
}
You can write this code into your model. It will execute every time when you create record using Laravel model.
If you want to modify it you can use property into Model class. Something like this:
class TestClass extends Model
{
public static $amount = 0;
static::creating(function ($model) {
$model->amount = self::$amount;
});
}
Good Luck.

Laravel 5.4 Eloquent not Saving certain fields

I have been working on logging all user activities in Model events... But for some reason, the records are stored in the user_action table but the action_model field. This is my code.
User Action Class
`
class UserAction extends Model
{
use SoftDeletes;
protected $table = 'user_actions';
/**
*
*/
protected $fillable = [
'user_id','action', ' action_model', 'action_id'
];
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}
`
UserActionObervable
class UserActionObserver
{
public function saved($model)
{
$type = $model->getTable();
// dd($type); when dump the value exists
if ($model->wasRecentlyCreated == true) {
// Data was just created
$action = 'created';
} else {
// Data was updated
$action = 'updated';
}
if (Sentinel::check()) {
UserAction::create([
'user_id' => Sentinel::getUser()->id,
'action' => $action,
'action_model' => 'model', //I tried to pass in constant
'action_id' => $model->id
]);
}
}
public function deleting($model)
{
if (Sentinel::check()) {
UserAction::create([
'user_id' => Sentinel::getUser()->id,
'action' => 'deleted',
'action_model' => $model->getTable(),
'action_id' => $model->id
]);
}
}
}
This is the schema
Schema::create('user_actions', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned()->nullable();
$table->string('action'); // created / updated / deleted
$table->string('action_model');
$table->integer('action_id')->nullable();
$table->timestamps();
$table->engine = 'InnoDB';
$table->foreign('user_id')->references('id')
->on('users')->onDelete('cascade');
});
protected $fillable = [
'user_id','action', ' action_model', 'action_id'
];
Try to remove the space before action_model. The field in $fillable doesn't match the key you're passing into the create() method.

Filter and sort in gridview by related field (2 degrees away)

My model Order has the following functions:
public function getAccount()
{
return $this->hasOne(Account::className(), ['id' => 'account_id']);
}
public function getChairName()
{
return $this->account->chairName;
}
The problem is that chairName itself is a related field (Created here in another model Account):
public function getChair0()
{
return $this->hasOne(Category::className(), ['id' => 'chair']);
}
public function getChairName()
{
return $this->chair0->name;
}
In my OrderSearch model I am now trying to search and filter by the name of the chair. In Account the chair is stored with an id that is linked to the model Category.
I have been looking at this link but it didn't help me solve my issue:
http://www.yiiframework.com/wiki/621/filter-sort-by-calculated-related-fields-in-gridview-yii-2-0/
MY OrderSearch model looks like this:
<?php
namespace app\models;
use Yii;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use app\models\OrderHistory;
[...]
class OrderHistorySearch extends OrderHistory
{
[...]
public $chairName;
[...]
public function rules()
{
return [
[...]
[['chairName'], 'safe'],
[...]
];
}
[...]
public function scenarios() { [...] }
public function search($params)
{
$query = OrderHistory::find();
$query->joinWith(['employee', 'account', 'item']);
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
[...]
$dataProvider->sort->attributes['chairName'] = [
'asc' => ['account.chair' => SORT_ASC],
'desc' => ['account.chair' => SORT_DESC],
];
[...]
$this->load($params);
if (!$this->validate()) { [...] }
$query->andFilterWhere([ [...] ]);
$query->andFilterWhere(['like', 'category.name', $this->chairName]);
return $dataProvider;
}
}
I reckon my mistake lies somewhere here:
$query->joinWith(['employee', 'account', 'item']);
The problem lies definitely in the fact that I am trying to join a table that is currently not joining.

Resources