Laravel 8: Passing Factory properties into children relationships - laravel

we are currently working on a laravel 8 application. We are trying to create factories to create some dummy data for manual / developer based application testing.
The current code of my main Database-Seeder is below:
class DatabaseSeeder extends Seeder
{
public function run()
{
$this->call([
UserTableSeeder::class,
]);
\App\Models\User::factory(10)->create();
\App\Models\Activity::factory(5)->create();
/* 1. try
$tenFact = \App\Models\Tenant::factory(2)->has(
\App\Models\Project::factory(2)->state(
function (array $attributes, \App\Models\Tenant $tenant) {
return ['tenant_id' => $attributes['id']];
}
)->hasTasks(5)->hasLocation()
)->hasContracts(3)->create();
*/
/* Currently being used: */
\App\Models\Tenant::factory(10)->has(
\App\Models\Project::factory(5)->hasTasks(5)->hasLocation()
)->hasContracts(3)->create();
}
ProjectFactory.php:
class ProjectFactory extends Factory
{
protected $model = Project::class;
public function definition()
{
return [
'name' => 'Projekt: '. $this->faker->name,
'budget' => $this->faker->randomDigitNotNull*1000,
'progress' => $this->faker->randomDigitNotNull*10,
'budget_used' => $this->faker->randomDigitNotNull*50,
//'tenant_id' => Tenant::factory(),
'location_id' => Location::factory()->hasTenant(1),
];
}
}
LocationFactory.php:
class LocationFactory extends Factory
{
protected $model = Location::class;
public function definition()
{
return [
'name' => 'Standort: ' . $this->faker->company,
'street' => $this->faker->streetName,
'house_number' => $this->faker->buildingNumber,
'house_addition' => $this->faker->secondaryAddress,
'zip' => $this->faker->postcode,
'city' => $this->faker->city,
'tenant_id' => Tenant::factory(),
];
}
}
Our relationships look like this:
Tenant
|-- Project (has: tenant_id, but also has location_id)
| | -- Task (has: project_id)
|-- Locations (has: tenant_id)
|-- Contracts (has: tenant_id)
When creating datasets with the above named Tenant-Factory the following happens:
Tenant->id is being passed to Project(tenant_id)
but: Tenant->id is not being passend to Location (which depends on the tenants id but is also used for Project).
How can we pass the id of \App\Models\Tenant::factory(10) to Project::factory(5)->hasTasks(5)->hasLocation()?
Additionally we do have the problem, that even though we request 10 tenants, we will get around 60, because Location/Project create new objects when they should be using existing ones.

I gave up using the chained usage of the Tenant-Factory - I finally used some for-Loop that connected the related objects to each user by using laravels for() and state() methods:
for ($i=0; $i < 10 ; $i++) {
$tenant = \App\Models\Tenant::factory()->hasContracts(3)->create();
for ($j=0; $j < 5; $j++) {
$location = \App\Models\Location::factory(1)->for($tenant)->create();
$project = \App\Models\Project::factory(1)->state([
'location_id' => $location->first()['id'],
'tenant_id' => $tenant['id']])->hasTasks(5)->create();
}
}

class ProjectFactory extends Factory
{
$location_ids = App\Models\Location::pluck('id')->toArray();
protected $model = Project::class;
public function definition()
{
return [
'name' => 'Projekt: '. $this->faker->name,
'budget' => $this->faker->randomDigitNotNull*1000,
'progress' => $this->faker->randomDigitNotNull*10,
'budget_used' => $this->faker->randomDigitNotNull*50,
//'tenant_id' => Tenant::factory(),
'location_id'=> $faker->randomElement($location_ids),
];
}
}
class LocationFactory extends Factory
{
$tenant_ids = App\Models\Tenant::pluck('id')->toArray();
protected $model = Location::class;
public function definition()
{
return [
'name' => 'Standort: ' . $this->faker->company,
'street' => $this->faker->streetName,
'house_number' => $this->faker->buildingNumber,
'house_addition' => $this->faker->secondaryAddress,
'zip' => $this->faker->postcode,
'city' => $this->faker->city,
'tenant_id'=> $faker->randomElement($tenant_ids),
];
}
}

Related

How to use Laravel 9 Faker to select from a predefined array of UUIDs?

How can I use Laravel Faker to select from a predefined array of UUIDs?
class BookShelfFactory extends Factory
{
protected $model = BookShelf::class;
public function definition()
{
$uuids = array(
'9f86affa-66fc-11ed-9022-0242ac120002',
'e69e6546-d0a2-49e1-b4fe-2fe31de20252',
'c48c6318-dbd1-4881-ab77-ffee4e1fa3b1',
'f3031799-6cae-4edf-87a1-330f3e5d2a65',
'e8d7f5cc-71f5-4e05-848a-1cdab53c15de'
);
return [
'name' => $this->faker->text(50),
//'uuid' => $this->faker->uuid,
'uuid'=> $this->faker->sequence($uuids),
'author_id' => \App\Models\BookAuthor::factory(),
'created_by' => \App\Models\User::factory(),
];
}
}
Above code results in error: Unknown format "sequence

How to test singleton(a class only new once) on laravel

This is Residence Facade:
class Residence extends Facade
{
protected static function getFacadeAccessor()
{
return 'residence.manager';
}
}
Here is the provider:
class ResidenceServiceProvider extends ServiceProvider implements DeferrableProvider
{
public $singletons = [
'residence.manager' => ResidenceManager::class,
'residence.japan' => ResidenceJapan::class,
'residence.us' => ResidenceUS::class,
'residence.eu' => ResidenceEU::class,
];
public function provides()
{
return [
'residence.manager',
'residence.japan',
'residence.us',
'residence.eu',
];
}
The ResidenceManager Class:
class ResidenceManager
{
protected $app;
public function __construct()
{
$this->app = app();
}
public function make($data)
{
$residenceService = match ($data['location']) {
'japan' => $this->app['residence.japan'],
'us' => $this->app['residence.us'],
'eu' => $this->app['residence.eu'],
default => false
};
if (!$residenceService ) {
return false;
}
return $residenceService->make($data);
}
}
I try to test if ResidenceJapan::class, ResidenceUS::class, and ResidenceEU::class only new once.
I use spy, bu it give me
Mockery\Exception\InvalidCountException
Method make(<Any Arguments>) from Mockery_0_App_Services_Residence_ResidenceJapan should be called
exactly 1 times but called 3 times.
/**
* #dataProvider residenceSeedProvider
*/
public function test_residence_class_only_new_once($data, $expected)
{
$residence= match ($data['location']) {
'japan' =>
[
'bind' => 'residence.japan',
'class' => ResidenceJapan::class
],
'us' =>
[
'bind' => 'residence.us',
'class' => ResidenceUS::class
],
'eu' => [
'bind' => 'residence.eu',
'class' => ResidenceEU::class
],
default => 'unknown',
};
$spy=$this->spy($residence['class']);
$this->app->instance(
$residence['bind'],
$spy
);
$i = 3;
while ($i > 0) {
//$this->assertNotNull(Residence::make($data));
Residence::make($data);// don't know why the Residence::make($data) return null
$i--;
}
$spy->shouldHaveReceived('make')->once();
}
So I use mock, but it give me
Error
Call to undefined method App\Services\Residence\ResidenceJapan::shouldHaveReceived()
/**
* #dataProvider residenceSeedProvider
*/
public function test_residence_class_only_new_once($data, $expected)
{
$residence= match ($data['location']) {
'japan' =>
[
'bind' => 'residence.japan',
'class' => ResidenceJapan::class
],
'us' =>
[
'bind' => 'residence.us',
'class' => ResidenceUS::class
],
'eu' => [
'bind' => 'residence.eu',
'class' => ResidenceEU::class
],
default => 'unknown',
};
$mock = Mockery::mock($residence['class'], function (MockInterface $mock) use ($expected) {
$mock->shouldReceive('make')->once()->andReturn($expected);
});
$this->app->instance(
$residence['bind'],
$mock
);
$i = 3;
while ($i > 0) {
$this->assertNotNull(Residence::make($data)); // the Residence::make($data) return what I am expected
$i--;
}
$residence['class']::shouldHaveReceived('make')->once();
}
How do I test if ResidenceJapan::class, ResidenceUS::class, and ResidenceEU::class only new once, no matter how many times the Facade Residence::make($data) calls?

Laravel-scout : ElasticSearch with Translatable Entities (astrotomic/laravel-translatable)

I'm trying to use "babenkoivan/scout-elasticsearch-driver" with "astrotomic/laravel-translatable", but i don't understand how I could index the translated words.
My Model looks like :
namespace App\Models;
use Astrotomic\Translatable\Translatable;
use App\Models\Search\ShowIndexConfigurator;
use ScoutElastic\Searchable;
...
class Show extends BaseModel
{
...
use Translatable;
use Searchable;
protected $indexConfigurator = ShowIndexConfigurator::class;
protected $searchRules = [
//
];
protected $mapping = [
'properties' => [
// How to index localized translations ???
'title' => [
'type' => 'string'
],
]
];
....
public $translatedAttributes = [
...,
'title'
...
];
Best regards
I found a solution with the override of the method
public function toSearchableArray() with something like:
public function toSearchableArray(): array
{
$document = [];
if ($this->published) {
$document = [
//...
];
foreach ($this->translations()->get() as $translation)
{
if (!$translation->active) {
continue;
}
$locale = $translation->locale;
$document['title_' . $locale] = $translation->title;
$document['url_' . $locale] = $this->getLink($locale);
$document['sub_title_' . $locale] = $translation->sub_title;
$document['keywords_' . $locale] = "";
}
}
return $document;
}
The purpose of $mapping=[] is only to define the structure of data. Something like that is expected:
protected $mapping = [
'properties' => [
'title_en' => [
'type' => 'string'
],
'title_fr' => [
'type' => 'string'
],
...
]
];

Extend Laravel package

I've searched around and couldn't find a definitive answer for this...
I have a package DevDojo Chatter and would like to extend it using my application. I understand I'd have to override the functions so that a composer update doesn't overwrite my changes.
How do I go about doing this?
UPDATE
public function store(Request $request)
{
$request->request->add(['body_content' => strip_tags($request->body)]);
$validator = Validator::make($request->all(), [
'title' => 'required|min:5|max:255',
'body_content' => 'required|min:10',
'chatter_category_id' => 'required',
]);
Event::fire(new ChatterBeforeNewDiscussion($request, $validator));
if (function_exists('chatter_before_new_discussion')) {
chatter_before_new_discussion($request, $validator);
}
if ($validator->fails()) {
return back()->withErrors($validator)->withInput();
}
$user_id = Auth::user()->id;
if (config('chatter.security.limit_time_between_posts')) {
if ($this->notEnoughTimeBetweenDiscussion()) {
$minute_copy = (config('chatter.security.time_between_posts') == 1) ? ' minute' : ' minutes';
$chatter_alert = [
'chatter_alert_type' => 'danger',
'chatter_alert' => 'In order to prevent spam, please allow at least '.config('chatter.security.time_between_posts').$minute_copy.' in between submitting content.',
];
return redirect('/'.config('chatter.routes.home'))->with($chatter_alert)->withInput();
}
}
// *** Let's gaurantee that we always have a generic slug *** //
$slug = str_slug($request->title, '-');
$discussion_exists = Models::discussion()->where('slug', '=', $slug)->first();
$incrementer = 1;
$new_slug = $slug;
while (isset($discussion_exists->id)) {
$new_slug = $slug.'-'.$incrementer;
$discussion_exists = Models::discussion()->where('slug', '=', $new_slug)->first();
$incrementer += 1;
}
if ($slug != $new_slug) {
$slug = $new_slug;
}
$new_discussion = [
'title' => $request->title,
'chatter_category_id' => $request->chatter_category_id,
'user_id' => $user_id,
'slug' => $slug,
'color' => $request->color,
];
$category = Models::category()->find($request->chatter_category_id);
if (!isset($category->slug)) {
$category = Models::category()->first();
}
$discussion = Models::discussion()->create($new_discussion);
$new_post = [
'chatter_discussion_id' => $discussion->id,
'user_id' => $user_id,
'body' => $request->body,
];
if (config('chatter.editor') == 'simplemde'):
$new_post['markdown'] = 1;
endif;
// add the user to automatically be notified when new posts are submitted
$discussion->users()->attach($user_id);
$post = Models::post()->create($new_post);
if ($post->id) {
Event::fire(new ChatterAfterNewDiscussion($request));
if (function_exists('chatter_after_new_discussion')) {
chatter_after_new_discussion($request);
}
if($discussion->status === 1) {
$chatter_alert = [
'chatter_alert_type' => 'success',
'chatter_alert' => 'Successfully created a new '.config('chatter.titles.discussion').'.',
];
return redirect('/'.config('chatter.routes.home').'/'.config('chatter.routes.discussion').'/'.$category->slug.'/'.$slug)->with($chatter_alert);
} else {
$chatter_alert = [
'chatter_alert_type' => 'info',
'chatter_alert' => 'You post has been submitted for approval.',
];
return redirect()->back()->with($chatter_alert);
}
} else {
$chatter_alert = [
'chatter_alert_type' => 'danger',
'chatter_alert' => 'Whoops :( There seems to be a problem creating your '.config('chatter.titles.discussion').'.',
];
return redirect('/'.config('chatter.routes.home').'/'.config('chatter.routes.discussion').'/'.$category->slug.'/'.$slug)->with($chatter_alert);
}
}
There's a store function within the vendor package that i'd like to modify/override. I want to be able to modify some of the function or perhaps part of it if needed. Please someone point me in the right direction.
If you mean modify class implementation in your application you can change the way class is resolved:
app()->bind(PackageClass:class, YourCustomClass::class);
and now you can create this custom class like so:
class YourCustomClass extends PackageClass
{
public function packageClassYouWantToChange()
{
// here you can modify behavior
}
}
I would advise you to read more about binding.
Of course a lot depends on how class is created, if it is created using new operator you might need to change multiple classes but if it's injected it should be enough to change this single class.

using first() in laravel fractal

I want to transform on the first item of Contacts and here is my code
public function includeContact(Customer $customer)
{
return $this->item($customer->contacts()->first(), new ContactTransformer);
}
but it's not working and I get this error :
Type error: Argument 1 passed to
App\Transformers\ContactTransformer::transform() must be an instance
of App\Models\Contact, null given, called in
*\vendor\league\fractal\src\Scope.php
on line 407
Edited
Here is ContactTransformer
namespace App\Transformers;
use App\Models\Contact;
use League\Fractal\TransformerAbstract;
class ContactTransformer extends TransformerAbstract
{
public function transform(Contact $contact)
{
return [
'value' => $contact->value,
'type' => $contact->communication->title,
'icon' => $contact->communication->icon
];
}
}
Here is CustomerTransformer
class CustomerTransformer extends TransformerAbstract
{
protected $availableIncludes = ['contacts', 'contact'];
public function transform(Customer $customer)
{
return [
'id' => $customer->id,
'name'=>$customer->name,
'status' => $customer->status,
'tags' => $customer->tags->pluck('name'),
'created_at' => Verta::instance($customer->created_at)->format('Y/n/j'),
];
}
public function includeContacts(Customer $customer)
{
return $this->collection($customer->contacts, new ContactTransformer);
}
public function includeContact(Customer $customer)
{
return $this->collection($customer->contacts, new ContactTransformer);
}
}
It's because some of your customers don't have contacts. Looks at your ContactTransformer class on transform() method, it should receive instance of Contact. If you give that method null, of course it'll fail.
Then, you need to have like so,
class ContactTransformer extends TransformerAbstract
{
public function transform(Contact $contact = null)
{
if (is_null($contact)) return null;
return [
'value' => $contact->value,
'type' => $contact->communication->title,
'icon' => $contact->communication->icon
];
}
}

Resources