-
Notifications
You must be signed in to change notification settings - Fork 20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support closures for model data #56
Support closures for model data #56
Conversation
private static function isCallable($field): bool | ||
{ | ||
return collect($defaultModelFields) | ||
->map(function ($item) { | ||
if ($this->isFactory($item)) { | ||
return $item->create()->getKey(); | ||
} | ||
|
||
return $item; | ||
}) | ||
->toArray(); | ||
return is_callable($field) && ! is_string($field) && ! is_array($field); | ||
} | ||
|
||
private function transformModelFields(array $defaultModelFields): array | ||
{ | ||
foreach ($defaultModelFields as &$field) { | ||
$field = $this->transformField($field, $defaultModelFields); | ||
} | ||
|
||
return $defaultModelFields; | ||
} | ||
|
||
private function transformField($field, array $defaultModelFields) | ||
{ | ||
if ($this->isFactory($field)) { | ||
return $field->create()->getKey(); | ||
} | ||
|
||
if ($this->isCallable($field)) { | ||
return $field($defaultModelFields); | ||
} | ||
|
||
return $field; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This portion was heavily inspired by how Laravel's FactoryBuilder@expandAttributes
method works.
The main takeaway here is that we use foreach with a variable passed by reference so with each iteration, the individual field transforms into the closure's return value, thus making it usable in the next factory fields. With map, the fields aren't affected until it loops all the way through.
I can provide screenshots if i didn't explain this part well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Except I'm not sure what you meant with map?
For me the tests were passing as well with this:
return collect($defaultModelFields)
->map(fn ($field) => $this->transformField($field, $defaultModelFields))
->toArray();
Did I miss your point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, i think my test wasnt testing the actual problem with using map. I have updated the case in 627161f
The problem is when i define an array of defaults, for example:
public function getDefaults(Generator $faker): array
{
return [
'first_field' => fn (array $factory) => 'some string',
'second_field' => fn(array $factory) => someFunctionThatRequiresString($factory['first_field']),
];
}
In the array above each key's value is a function. When we loop through these one at a time and transform them to the callbacks return value, the first_field
key will turn into a string.
When we get to the second field, it's relying on $factory['first_field']
to already be a string, not a closure.
When we use ->map
to iterate, we get to the first_field
, we transform it into a string, but don't update it yet. Instead, it holds in a different memory location and will switch at the end. Then, when we get to the second_field
key, it throws an error because someFunctionThatRequiresString
requires a string, but a closure is passed because the first field hasn't been transformed yet.
using foreach
with passing $field
by reference via &$field
, it allows the value to transform on each iteration, rather than waiting to switch the entire array at once to the new array of values.
Here is a printout of what the $defaultModelFields
looks like on each iteration using map and foreach:
Using Map
// first iteration
array:2 [
"name" => Closure(array $ingredient) {#1354
class: "Christophrumpel\LaravelFactoriesReloaded\Tests\FactoryTest"
this: Christophrumpel\LaravelFactoriesReloaded\Tests\FactoryTest {#759 …}
}
"description" => Closure(array $ingredient) {#1355
class: "Christophrumpel\LaravelFactoriesReloaded\Tests\FactoryTest"
this: Christophrumpel\LaravelFactoriesReloaded\Tests\FactoryTest {#759 …}
}
]
// second iteration
array:2 [
"name" => Closure(array $ingredient) {#1354
class: "Christophrumpel\LaravelFactoriesReloaded\Tests\FactoryTest"
this: Christophrumpel\LaravelFactoriesReloaded\Tests\FactoryTest {#759 …}
}
"description" => Closure(array $ingredient) {#1355
class: "Christophrumpel\LaravelFactoriesReloaded\Tests\FactoryTest"
this: Christophrumpel\LaravelFactoriesReloaded\Tests\FactoryTest {#759 …}
}
]
Using Foreach
// first iteration
array:2 [
"name" => & Closure(array $ingredient) {#1354
class: "Christophrumpel\LaravelFactoriesReloaded\Tests\FactoryTest"
this: Christophrumpel\LaravelFactoriesReloaded\Tests\FactoryTest {#759 …}
}
"description" => Closure(array $ingredient) {#1355
class: "Christophrumpel\LaravelFactoriesReloaded\Tests\FactoryTest"
this: Christophrumpel\LaravelFactoriesReloaded\Tests\FactoryTest {#759 …}
}
]
// second iteration
array:2 [
"name" => "Basil"
"description" => & Closure(array $ingredient) {#1355
class: "Christophrumpel\LaravelFactoriesReloaded\Tests\FactoryTest"
this: Christophrumpel\LaravelFactoriesReloaded\Tests\FactoryTest {#759 …}
}
]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah got it. Thanks for the details. Then I'm fine with the current implementation 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool. I prefer moving things to collections as well, so i get the hesitation. I couldnt find a way to update the variables by reference via Collection Methods, so i opted for this. I believe this is how laravel handles it behind the scenes as well.
return is_callable($field) && ! is_string($field) && ! is_array($field); | ||
} | ||
|
||
private function transformModelFields(array $defaultModelFields): array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also renamed this because i felt it wasn't just transforming factories anymore, but not super opinionated on this, so if you feel strongly about the name, i can change it back.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I also think the name is now better 👍
use ExampleApp\Models\Group; | ||
use ExampleApp\Models\Ingredient; | ||
use ExampleApp\Models\Recipe; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think my IDE just auto-alphabetized these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok no worries. I will see if I can automate that with the Github Actions as well. I just want them to be consistent, not matter which way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, im not opinionated on this either tbh. Whenever i auto-import something, it just re-alphabetizes the list. I can manually put it back if you like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's ok, just leave it 👍
Hey, thanks a lot for this PR. Just came back from holidays and need to give a closer look 👍 |
how dare you take time off from open source 😜 .. ha, of course, take your time. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just went through. Looks good to me. Just some small comments.
Thanks a lot for this PR and I'm happy that this package could help you with your projects.
return is_callable($field) && ! is_string($field) && ! is_array($field); | ||
} | ||
|
||
private function transformModelFields(array $defaultModelFields): array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I also think the name is now better 👍
private static function isCallable($field): bool | ||
{ | ||
return collect($defaultModelFields) | ||
->map(function ($item) { | ||
if ($this->isFactory($item)) { | ||
return $item->create()->getKey(); | ||
} | ||
|
||
return $item; | ||
}) | ||
->toArray(); | ||
return is_callable($field) && ! is_string($field) && ! is_array($field); | ||
} | ||
|
||
private function transformModelFields(array $defaultModelFields): array | ||
{ | ||
foreach ($defaultModelFields as &$field) { | ||
$field = $this->transformField($field, $defaultModelFields); | ||
} | ||
|
||
return $defaultModelFields; | ||
} | ||
|
||
private function transformField($field, array $defaultModelFields) | ||
{ | ||
if ($this->isFactory($field)) { | ||
return $field->create()->getKey(); | ||
} | ||
|
||
if ($this->isCallable($field)) { | ||
return $field($defaultModelFields); | ||
} | ||
|
||
return $field; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Except I'm not sure what you meant with map?
For me the tests were passing as well with this:
return collect($defaultModelFields)
->map(fn ($field) => $this->transformField($field, $defaultModelFields))
->toArray();
Did I miss your point?
use ExampleApp\Models\Group; | ||
use ExampleApp\Models\Ingredient; | ||
use ExampleApp\Models\Recipe; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok no worries. I will see if I can automate that with the Github Actions as well. I just want them to be consistent, not matter which way.
tests/FactoryTest.php
Outdated
@@ -192,6 +193,31 @@ public function it_lets_you_use_faker_for_defining_data(): void | |||
$this->assertIsString($group->mobile); | |||
} | |||
|
|||
/** @test * */ | |||
public function it_lets_you_use_a_closure_for_defining_data(): void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please change it to it_lets_you_use_a_closure_for_defining_default_data
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in 345e064
tests/FactoryTest.php
Outdated
} | ||
|
||
/** @test * */ | ||
public function it_lets_you_use_a_closure_for_overriding_data(): void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And here please it_lets_you_use_a_closure_for_overriding_default_data
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in 345e064
public function it_lets_you_use_a_closure_for_overriding_default_data(): void | ||
{ | ||
$ingredient = IngredientFactoryUsingClosure::new()->create([ | ||
'name' => fn (array $ingredient) => 'Basil', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated the test to define two closures. This will accurately test that the second closure can use the value from the first closure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome. Looks good to me! I think we are ready to merge, right?
Yep, let’s do it |
Wohoo. Thanks a lot for the PR. I will update the docs and release a new version today 👍 |
What & Why
Currently in the Laravel factories setup, we are able to use a function when defining a field if we rely on data being dynamically set earlier in the factory.
https://laravel.com/docs/7.x/database-testing#relationships
In the library as it stands today, it throws an error when trying to use a closure.
I love this library because I was in the process of transitioning my factories into classes and this library made it so much easier, but I rely on closures a lot for dynamically generating content that is dependent on the fields that are created earlier. I think others who also have already been using closures might find this update useful.
Solution
This PR aims to allow closures to be used when setting data in the
getDefaults
method as well as using a closure to override onmake
orcreate
.