Laravel 8 - Factories & Foreign Keys - php

I haven't been able to locate how to manage foreign keys when creating Factories and Seeders.
I have a users table and a blog_posts table. The blog_posts table has a foreign key user_id that references users.id. I would like to seed my database with users and blog posts.
I have seen how to create a new user for each seeded blog post with the following:
/**
* Define the BlogPost model's default state.
*
* #return array
*/
public function definition()
{
return [
'user_id' => User::factory(),
'created_at' => now(),
'updated_at' => now(),
'title' => $this->faker->words(5, true),
'body' => $this->faker->paragraphs(6, true)
];
}
...but I would like to reference existing users since I am performing the database seed like so:
/**
* Seed the application's database.
*
* #return void
*/
public function run()
{
$this->call([
UsersTableSeeder::class,
BlogPostsTableSeeder::class
]);
}

We can retrieve a random existing User and assign it in the BlogPost Factory as follows:
/**
* Define the BlogPost model's default state.
*
* #return array
*/
public function definition()
{
return [
'user_id' => User::inRandomOrder()->first()->id,
'created_at' => now(),
'updated_at' => now(),
'title' => $this->faker->words(5, true),
'body' => $this->faker->paragraphs(6, true)
];
}

Assuming your User model has a hasMany relationship called blogPosts, you could do the following:
User::factory()
->hasBlogPosts(6)
->create();
This would create your User and 6 associated BlogPosts.

Related

What causes the 'Unknown format "factory"' error in this Laravel 8 app?

I am working on a Laravel 8 app with users and posts.
The objective is to create a bunch of posts (I already have users).
namespace Database\Factories;
// import Post model
use App\Models\Post;
// import User model
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory {
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected $model = Post::class;
/**
* Define the model's default state.
*
* #return array
*/
public function definition() {
return [
'title' => $this->faker->sentence(3),
'description' => $this->faker->text,
'content' => $this->faker->paragraph,
'user_id' => $this->faker->factory(App\Models\User::class),
];
}
}
The problem
I run php artisan tinker then Post::factory()->count(100)->create() in the terminal and I get:
InvalidArgumentException with message 'Unknown format "factory"'
UPDATE
I replace my return statement with:
return [
'title' => $this->faker->sentence(3),
'description' => $this->faker->text,
'content' => $this->faker->paragraph,
'user_id' => User::factory(),
];
I get this in the terminal:
Class 'Database\Factories\UserFactory' not found
Questions:
Where is my mistake?
Does the fact that I get the error Class 'Database\Factories\UserFactory' not found mean that I need to
create a UserFactory factory? Because there isn't one. (I wanted
to create posts, not users).
I don't suppose there is $this->faker->factory(..).
You can do
'user_id' => App\Models\User::factory()->create()->id,
EDIT:
'user_id' => App\Models\User::factory(),
Creating a UserFactory factory and using the below return statement did the trick:
return [
'title' => $this->faker->sentence(3),
'description' => $this->faker->text,
'content' => $this->faker->paragraph,
'user_id' => User::factory(),
];
So, the PostFactory class looks like this:
class PostFactory extends Factory {
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected $model = Post::class;
/**
* Define the model's default state.
*
* #return array
*/
public function definition() {
return [
'title' => $this->faker->sentence(3),
'description' => $this->faker->text,
'content' => $this->faker->paragraph,
'user_id' => User::factory(),
];
}
}

Laravel 8.x, make fake model reationships?

I'm trying to run a faker factory for relationships, but the field always returns NULL. How do fake a model relationship without hitting the database?
I have a Map factory with a one-to-one relationship to a parent Event table. I need to fake this relationship for unit testing:
/**
* Define the model's default state.
*
* #return array
*/
public function definition()
{
return [
'event' => 'faker.' . join('_', $this->faker->words),
'category' => $this->faker->word,
'sub_category' => $this->faker->word,
'priority' => $this->faker->randomElement(['normal', 'high']),
'event' => Event::factory()->makeOne(),
];
}
This returns a fake model, but event is null, from the debugger:
result = {array} [5]
event = "faker.eum_voluptatibus_aut"
category = "libero"
sub_category = "aut"
priority = "high"
event = null
I tried using states, but the same thing happens:
public function disabled()
{
return $this->state([
'event' => Event::factory()->makeOne(['enabled' => false]),
]);
}
The object is returned with an empty event value. I need a faker object I can transverse down into: if ($object->event->enabled) [...]. How do I generate fake model relationships?
If you are using Laravel 8.x you must consider using methods used on the docs, it must look like that :
/**
* Define the model's default state.
*
* #return array
*/
public function definition()
{
return [
'event' => 'faker.' . join('_', $this->faker->words),
'category' => $this->faker->word,
'sub_category' => $this->faker->word,
'priority' => $this->faker->randomElement(['normal', 'high']),
'event_id' => Event::factory(),
];
}
/**
* Indicate that the map is disabled.
*
* #return \Illuminate\Database\Eloquent\Factories\Factory
*/
public function disabled()
{
return $this->state([
'event_id' => Event::factory()->create(['enabled' => false]),
]);
}
The only solution I found, so far, is to manually set the event key myself in my tests. It's not the ideal or elegant solution.
$fieldMap = Map::factory()->makeOne();
$fieldMap->event = Event::factory(['enabled' => false])->makeOne();
I don't like this approach. Why can't I define factories within factories?

Laravel preparing the database before a test for a model with many dependencies, is there a better way?

I am using Laravel 5.2 for my web app and the bundled testing tools which extend PHPUnit.
I am writing a test for a page that performs an update on a model. The test checks that one of the model's attributes hasn't changed prior to submitting the form in order to prevent two users from updating the model if they had it both open in their browser at the same time.
The test requires the use of a model in my Laravel web app, namely App\Models\Application, and I am using factories to create some fake data for this model. The Application model has many belongsTo relationships, which in turn requires that the related model is created within the test using factories prior to the creation of the Application model.
For example, the Application model has a belongsTo relationship with Applicant, so I need to create an Applicant model with a factory prior to creating an Application and an Applicant also has belongsTo relationships with User, which I also need to create before ... you get the idea.
So, in short, I am creating several model instances due to the relationship dependencies of one model which I want to test.
So this has left me wondering if I am taking the wrong approach? Is there a more simple approach where I only create the model that is under test?
Here's the code from my test:
/**
* #test
*/
public function throw_exception_when_application_status_is_modified_after_submission()
{
/**
* Arrange
*/
// create roles
factory(Role::class, 'admin')->create();
factory(Role::class, 'applicant')->create();
// create admin user
$adminUser = factory(User::class)->create();
$adminUser->attachRole(Role::whereName('admin')->first());
// create applicant user
$applicantUser = factory(User::class)->create();
$applicantUser->attachRole(Role::whereName('applicant')->first());
// create organisation with type
$organisationType = factory(OrganisationType::class)->create();
$organisation = factory(Organisation::class)->create();
$organisation->organisationTypes()->attach($organisation->id);
// create sub application
$subApplication = factory(ExperienceLetter::class)->create();
// create applicant
$applicant = factory(Applicant::class)->create([
'user_id' => $applicantUser->id
]);
// create application
$application = factory(Application::class)->make([
'status' => 'pending',
'application_id' => $subApplication->id,
'application_type' => 'App\Models\ExperienceLetter',
'organisation_id' => $organisation->id,
'applicant_id' => $applicant->id,
]);
// prep data
$data = [
'action' => 'accept',
'details' => 'email sent',
'application_id' => $application->id,
'application_status' => 'pending',
'verification' => 'email',
];
/**
* Act
*/
// log in
$this->actingAs($adminUser);
// alter status before call()
$application->status = 'verification';
$application->save();
$response = $this->call('POST', route('admin.application.action.store'), $data);
/**
* Assert
*/
$this->assertEquals(302, $response->status());
// assertSessionHasErrors
}
Thanks for any advice.
Are you creating all of those because of foreign key constraints? If you just want to check that one model without any relations then Schema::disableForeignKeyConstraints(); is your friend:
/**
* #test
*/
public function throw_exception_when_application_status_is_modified_after_submission()
{
\Schema::disableForeignKeyConstraints();
/**
* Arrange
*/
// create admin user
$adminUser = factory(User::class)->create();
$adminUser->attachRole(factory(Role::class, 'admin')->create());
// create application
$application = factory(Application::class)->make([
'status' => 'pending',
'application_id' => 0,
'application_type' => 'App\Models\ExperienceLetter',
'organisation_id' => 0,
'applicant_id' => 0,
]);
// prep data
$data = [
'action' => 'accept',
'details' => 'email sent',
'application_id' => $application->id,
'application_status' => 'pending',
'verification' => 'email',
];
/**
* Act
*/
// log in
$this->actingAs($adminUser);
// alter status before call()
$application->status = 'verification';
$application->save();
$response = $this->call('POST', route('admin.application.action.store'), $data);
\Schema::enableForeignKeyConstraints();
/**
* Assert
*/
$this->assertEquals(302, $response->status());
// assertSessionHasErrors
}

Two Models Displaying to Same View in Yii2

I am trying to get information from two models that are related, displayed in one view.
So what I am trying to accomplish is have the index view to show the list of people, if I then go into detail view of that particular person I want a list of attributes relevant to that person to appear.
I have the database setup so that when I create a new person a default row gets inserted into the attributes table with the id of the person under the column called person_id.
See my two model classes
People:
class People extends \yii\db\ActiveRecord
{
/**
* #inheritdoc
*/
public static function tableName()
{
return 'people';
}
/**
* #inheritdoc
*/
public function rules()
{
return [
[['dob', 'CURDATE'], 'safe'],
[['age'], 'integer'],
[['firstname', 'surname'], 'string', 'max' => 50]
];
}
/**
* #inheritdoc
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'firstname' => 'Firstname',
'surname' => 'Surname',
'dob' => 'Dob',
'age' => 'Age',
'CURDATE' => 'Curdate',
];
}
/**
* #return \yii\db\ActiveQuery
*/
public function getId0()
{
return $this->hasOne(Attributes::className(), ['person_id' => 'id']);
}
}
Attributes:
class Attributes extends \yii\db\ActiveRecord
{
/**
* #inheritdoc
*/
public static function tableName()
{
return 'attributes';
}
/**
* #inheritdoc
*/
public function rules()
{
return [
[['haircolor', 'eyecolor', 'weight', 'height', 'person_id'], 'required'],
[['weight', 'height', 'person_id'], 'integer'],
[['haircolor', 'eyecolor'], 'string', 'max' => 50]
];
}
/**
* #inheritdoc
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'haircolor' => 'Haircolor',
'eyecolor' => 'Eyecolor',
'weight' => 'Weight',
'height' => 'Height',
'person_id' => 'Person ID',
];
}
/**
* #return \yii\db\ActiveQuery
*/
public function getPeople()
{
return $this->hasOne(People::className(), ['id' => 'person_id']);
}
}
I have generated CRUD through Gii for both of these models.
What I would like to know is how to setup my people controller and people view so that this may work properly.
Just to recap, my index.php view will just show the list of people, if a record exists, you can view that specific record, if you view the record - which will be the view.php file, I want to show the attributes(These will be the default values) of that particular person where the id of the person is the same as the person_id in the attributes table
The user will then be able to update the attributes relating to that person.
Kind Regards.
Here an example :
public function actionCreate()
{
$user = new User;
$profile = new Profile;
if ($user->load(Yii::$app->request->post()) && $profile->load(Yii::$app->request->post()) && Model::validateMultiple([$user, $profile])) {
$user->save(false); // skip validation as model is already validated
$profile->user_id = $user->id; // no need for validation rule on user_id as you set it yourself
$profile-save(false);
return $this->redirect(['view', 'id' => $user->id]);
} else {
return $this->render('create', [
'user' => $user,
'profile' => $profile,
]);
}
}
More informations :
http://www.yiiframework.com/forum/index.php/topic/53935-solved-subforms/page__p__248184
http://www.yiiframework.com/doc-2.0/guide-input-tabular-input.html
To display related information in a view, you get the best performance with eager loading. I'll provide an example:
public function actionView($id)
{
$model = Person::find()
->where(['id' => $id])
->with('id0')
->one();
return $this->render('view', [
'model' => $model,
]);
}
Now i see that your relation in Person Model is called getId0, you can for readability change that to getAttribs(), and change to ->with('attribs') but that is just a digression :)
EDIT: as #soju commented, attributes is not possible to use as a relation name, and that is why gii has given it the name getId0. Attribs or something more informative can be helpful on readability.
If you want to show the results in a widget, like GridView or ListView, you can follow the guide here:
http://www.ramirezcobos.com/2014/04/16/displaying-sorting-and-filtering-model-relations-on-a-gridview-yii2/
EDIT2: as #soju commented, guide is possibly outdated. Read official documents aswell.
http://www.yiiframework.com/doc-2.0/guide-output-data-widgets.html#working-with-model-relations
If you want to create your own view, you can access the values with $model->id0->haircolor or, if you rename the relation, $model->attribs->haircolor just like you would any other attribute.
Remember: using GridView / ListView requires the table name from the db when displaying, like 'attributes.eyecolor', but the $model->id0 requires the relation name from the model, without the 'get' in front, and with lower case.

How to specify Books and Authors relationship in Eloquent insertion?

I was figuring out how to insert a hasMany entry using Laravel 4
Author.php module
class Author extends Eloquent\LaravelBook\Ardent\Ardent
{
public static $rules = array(
'title' => 'required',
'last_name' => 'required',
'first_name' => 'required',
);
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'authors';
public function abstraction()
{
return $this->belongsTo('Book','book_id');
}
/**
* Get the unique identifier for the user.
*
* #return mixed
*/
public function getRegId()
{
return $this->getKey();
}
}
Book.php Module
class Book extends Eloquent\LaravelBook\Ardent\Ardent
{
public static $rules = array(
'title' => 'required',
'authors' => 'required',
);
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'abstracts';
public function author()
{
return $this->hasMany('Author');
}
/**
* Get the unique identifier for the user.
*
* #return mixed
*/
public function getRegId()
{
return $this->getKey();
}
}
My controller
$r = new Book();
$r->title = Input::get('title');
$r->authors = Input::get('authors');
$r->affiliation = Input::get('affiliation');
$r->keywords = Input::get('keywords');
$r->summary = Input::get('summary');
$r->save();
$authors = new Author();
$authors_array = array(
array('name' => 'Arun1', 'affiliation' => 'aff_arun'),
array('name' => 'Arun2', 'affiliation' => 'aff_arun'),
array('name' => 'Arun3', 'affiliation' => 'aff_arun3'),
array('name' => 'Arun4', 'affiliation' => 'aff_arun'),
);
$authors->book()->associate($r);
$authors->save($authors_array);
I'm getting one null record on authors table which pointed to the book and other 3 record that I inserted here without any pointing to the book.
First off I believe this is wrong schema, since probably one author could write more than a single book.
So I suggest you define many-to-many (belongsToMany) relation for this instead.
Anyway here is solution to your problem:
// I wonder why you call the book $r ?
$r = new Book;
...
$r->save();
$authors_array = array(...);
foreach ($authors_array as $author)
{
$r->author()->save(new Author($author));
// you need $fillable/$guarded on Author model or it will throw MassAssignementException
}
Another suggestion: rename the relation to authors so it is meaningful and you won't catch yourself trying to treat it like a single model (while it always returns the Collection)

Categories