Laravel 8.x, make fake model reationships? - php

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?

Related

How to fake valid json in Laravel?

In a Laravel project I have to store some data in json. For phpUnit tests I use a factory with faker. I try to fake a json structure for the tests, but it always fail on validation. Is there any proper way to create a json in factory that passes the validation for json?
I tried a simple json array, and the json array with json_encode, both of them failed at validation, and gives errors.
With simple json like: 'settings' => ['areas' => ['full', 'city']]
the error is:
Property [settings] is not of expected type [json].
Failed asserting that an array contains 'array'.
With json_encode like: 'settings' => json_encode(['areas' => ['full', 'city']])
the error is:
Property [settings] is not of expected type [json].
Failed asserting that an array contains 'string'.
My model:
class Example extends Model
{
protected $fillable = [
'name',
'settings'
];
public static $rules = [
'name' => 'required|string|max:255',
'settings' => 'nullable|json'
];
protected $casts =
'settings' => 'array'
];
}
My factory:
<?php
class ExampleFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected $model = Example::class;
/**
* Define the model's default state.
*
* #return array
*/
public function definition()
{
return [
'name' => $this->faker->words(3, 7),
'settings' => json_encode(['areas' => ['full', 'city']]) // or what?
];
}
}
In my test file:
/** #test */
public function shouldStore(): void
{
$item = $this->model::factory()->make();
$data = $item->toArray();
$this->post(action([$this->controller, 'store']), $data)
->assertOk();
}
Your issue is that you are casting the property to array, but you are storing a string, you should be passing an array.
Do this:
class ExampleFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected $model = Example::class;
/**
* Define the model's default state.
*
* #return array
*/
public function definition()
{
return [
'name' => $this->faker->words(3, 7),
'settings' => ['areas' => ['full', 'city']],
];
}
}
And the rule (no idea where are you using that) should be like this:
'settings' => 'nullable|array'
Your model works just fine with an array because the cast takes care of the conversion for you. However when posting to the controller you need to manually cast the data to JSON:
public function shouldStore(): void
{
$item = $this->model::factory()->make();
$data = $item->toArray();
$data['settings'] = json_encode($data['settings']);
$this->post(action([$this->controller, 'store']), $data)
->assertOk();
}
However this does mean that you may need to json_decode the data in the controller before creating the model.
Alternatevely you can do what #matiaslauriti is suggesting and post the data as an array to begin with
You need to cast the property to array:
SomeModel extends Model
{
protected $casts = [
'settings' => 'array',
];
}

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 - Factories & Foreign Keys

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.

How to set sorting in default repository function in Typo3?

I have written like this
/**
* itemRepository
*
* #var \KRT\KrtEmployee\Domain\Repository\ItemRepository
* #inject
*/
protected $itemRepository = null;
/**
* action list
*
* #return void
*/
public function listAction()
{
$arguments =$this->request->getArguments();
$employees = $this->itemRepository->findAll();
$this->view->assign('employees',$employees);
}
In my $employees result I have
Employee ID (Default uid)
Name
Designation
Department
Salary
Now, How can I Sort the result array in ascending order based on
Name
Department and Salary
Is there any default function to sort inside the repository queries?
Every repository has a $defaultOrderings property where you can specify the default orderings applied to all query methods. In your case it could look like this:
protected $defaultOrderings = [
'name' => QueryInterface::ORDER_ASCENDING,
'department.name' => QueryInterface::ORDER_ASCENDING,
'salary' => QueryInterface::ORDER_ASCENDING,
];
As you can see with department.name you can also sort by properties of relations. Notice that this only works for 1:1 and n:1 relations.
In case of custom query methods in your repository you can manually set the orderings directly on the query:
$query->setOrderings([
'name' => QueryInterface::ORDER_ASCENDING,
'department.name' => QueryInterface::ORDER_ASCENDING,
'salary' => QueryInterface::ORDER_ASCENDING,
]);
You have multiple options depending on what you would like to achieve:
Setting a default order for the entire repository
Add the following to your repository class
protected $defaultOrderings =
array(
'department' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING,
'salary' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING
);
This will apply to all queries made in this repository.
see: https://wiki.typo3.org/Default_Orderings_and_Query_Settings_in_Repository
Setting an order for a single query
Add this to a query you define in your repository
$query->setOrderings(
array(
'department' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING,
'salary' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING
)
);
In this way you could (and would have to) implement a different access method for each sort order you would like to have returned.
see: https://docs.typo3.org/typo3cms/ExtbaseFluidBook/6-Persistence/3-implement-individual-database-queries.html
Sort the result
You can always use PHP sorting methods to sort the query result (possibly converting it to an array with ->toArray() first.
In general and complete:
<?php
namespace <yourVendor>\<yourExtensionkey>\Domain\Repository;
use TYPO3\CMS\Extbase\Persistence\Repository;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
class <yourDomain>Repository extends \TYPO3\CMS\Extbase\Persistence\Repository {
protected $defaultOrderings = [
'<yourProperty1>' => QueryInterface::ORDER_ASCENDING,
'<yourProperty2>' => QueryInterface::ORDER_DESCENDING
];
}
Further explanation in docs.typo3.org

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
}

Categories