Skip to content

Commit

Permalink
Factories: Ensure afterMaking gets called prior to create() attribute…
Browse files Browse the repository at this point in the history
…s being set
  • Loading branch information
claudiodekker committed Oct 25, 2024
1 parent 5527e72 commit db7c530
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 12 deletions.
97 changes: 85 additions & 12 deletions src/Illuminate/Database/Eloquent/Factories/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Enumerable;
Expand Down Expand Up @@ -50,6 +51,13 @@ abstract class Factory
*/
protected $states;

/**
* The "create" attributes that will be applied to the model during creation.
*
* @var array
*/
protected $createAttributes;

/**
* The parent relationships that will be applied to the model.
*
Expand Down Expand Up @@ -151,6 +159,7 @@ public function __construct($count = null,
$this->connection = $connection;
$this->recycle = $recycle ?? new Collection;
$this->faker = $this->withFaker();
$this->createAttributes = [];
}

/**
Expand Down Expand Up @@ -278,16 +287,20 @@ public function createManyQuietly(int|iterable|null $records = null)
public function create($attributes = [], ?Model $parent = null)
{
if (! empty($attributes)) {
return $this->state($attributes)->create([], $parent);
return $this->captureCreateAttributeKeysState($attributes)->create([], $parent);
}

$results = $this->make($attributes, $parent);
$results = $this->make([], $parent);

if ($results instanceof Model) {
$results->forceFill($this->createAttributes);

$this->store(collect([$results]));

$this->callAfterCreating(collect([$results]), $parent);
} else {
$results->each(fn (Model $model) => $model->forceFill($this->createAttributes));

$this->store($results);

$this->callAfterCreating($results, $parent);
Expand Down Expand Up @@ -414,7 +427,11 @@ public function make($attributes = [], ?Model $parent = null)
protected function makeInstance(?Model $parent)
{
return Model::unguarded(function () use ($parent) {
return tap($this->newModel($this->getExpandedAttributes($parent)), function ($instance) {
$attributes = $this->getExpandedAttributes($parent);

$this->captureCreateAttributes($attributes);

return tap($this->newModel($attributes), function ($instance) {
if (isset($this->connection)) {
$instance->setConnection($this->connection);
}
Expand All @@ -426,7 +443,7 @@ protected function makeInstance(?Model $parent)
* Get a raw attributes array for the model.
*
* @param \Illuminate\Database\Eloquent\Model|null $parent
* @return mixed
* @return array
*/
protected function getExpandedAttributes(?Model $parent)
{
Expand All @@ -446,11 +463,7 @@ protected function getRawAttributes(?Model $parent)
return $this->parentResolvers();
}], $states->all()));
})->reduce(function ($carry, $state) use ($parent) {
if ($state instanceof Closure) {
$state = $state->bindTo($this);
}

return array_merge($carry, $state($carry, $parent));
return array_merge($carry, $this->resolveStateAttributes($state, $carry, $parent));
}, $this->definition());
}

Expand Down Expand Up @@ -511,9 +524,7 @@ public function state($state)
{
return $this->newInstance([
'states' => $this->states->concat([
is_callable($state) ? $state : function () use ($state) {
return $state;
},
$this->wrapStateTransformer($state),
]),
]);
}
Expand Down Expand Up @@ -947,4 +958,66 @@ public function __call($method, $parameters)
);
}
}

/**
* If the given state transformer is not a callable, wrap it in one.
*
* @param (callable(array<string, mixed>, TModel|null): array<string, mixed>)|array<string, mixed> $state
* @return callable(array<string, mixed>, TModel|null): array<string, mixed>
*/
protected function wrapStateTransformer(array|callable $state): callable
{
return is_callable($state) ? $state : function () use ($state) {
return $state;
};
}

/**
* Resolve the attributes of a state transformation.
*
* @param callable $resolver
* @param array $state
* @param \Illuminate\Database\Eloquent\Model|null $parent
* @return array
*/
protected function resolveStateAttributes(callable $resolver, array $state, ?Model $parent): array
{
if ($resolver instanceof Closure) {
$resolver = $resolver->bindTo($this);
}

return $resolver($state, $parent);
}

/**
* State transformer that captures the keys of the `create` attributes.
*
* @param (callable(array<string, mixed>, TModel|null): array<string, mixed>)|array<string, mixed> $state
* @return static
*/
protected function captureCreateAttributeKeysState($state)
{
$callableState = $this->wrapStateTransformer($state);

return $this->state(function ($carry, $parent) use ($callableState) {
return tap($this->resolveStateAttributes($callableState, $carry, $parent), function ($newState) {
$this->createAttributes = array_fill_keys(array_keys($newState), null);
});
});
}

/**
* Capture the current state of the attributes that were passed to the `create` method (if any).
*
* @param array $attributes
* @return void
*/
protected function captureCreateAttributes(array $attributes)
{
if (empty($createAttributeKeys = array_keys($this->createAttributes))) {
return;
}

$this->createAttributes = Arr::only($attributes, $createAttributeKeys);
}
}
26 changes: 26 additions & 0 deletions tests/Database/DatabaseEloquentFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,32 @@ public function test_after_creating_and_making_callbacks_are_called()
unset($_SERVER['__test.user.making'], $_SERVER['__test.user.creating']);
}

public function test_after_making_callback_is_called_prior_to_create_attributes_being_set()
{
$user = FactoryTestUserFactory::new()
->afterMaking(function ($user) {
$user->name = 'Taylor Otwell';
})
->create(['name' => 'Claudio Dekker']);

$this->assertSame('Claudio Dekker', $user->name);
}

public function test_create_attributes_override_current_state_definition()
{
$user = FactoryTestUserFactory::new()->create();
$this->assertCount(1, FactoryTestUser::all());

FactoryTestPostFactory::new()->create([]);
$this->assertCount(2, FactoryTestUser::all());

FactoryTestPostFactory::new()->create(['user_id' => $user->id]);
$this->assertCount(2, FactoryTestUser::all());

FactoryTestPostFactory::new()->create(['user_id' => FactoryTestUserFactory::new()]);
$this->assertCount(3, FactoryTestUser::all());
}

public function test_has_many_relationship()
{
$users = FactoryTestUserFactory::times(10)
Expand Down

0 comments on commit db7c530

Please sign in to comment.