From db7c530570fab7d255a15a4708e60e4f1a1256e8 Mon Sep 17 00:00:00 2001 From: Claudio Dekker Date: Thu, 24 Oct 2024 19:55:10 +0200 Subject: [PATCH] Factories: Ensure afterMaking gets called prior to create() attributes being set --- .../Database/Eloquent/Factories/Factory.php | 97 ++++++++++++++++--- .../Database/DatabaseEloquentFactoryTest.php | 26 +++++ 2 files changed, 111 insertions(+), 12 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index f4b73b4720c7..29d389d8b809 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -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; @@ -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. * @@ -151,6 +159,7 @@ public function __construct($count = null, $this->connection = $connection; $this->recycle = $recycle ?? new Collection; $this->faker = $this->withFaker(); + $this->createAttributes = []; } /** @@ -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); @@ -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); } @@ -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) { @@ -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()); } @@ -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), ]), ]); } @@ -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, TModel|null): array)|array $state + * @return callable(array, TModel|null): array + */ + 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, TModel|null): array)|array $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); + } } diff --git a/tests/Database/DatabaseEloquentFactoryTest.php b/tests/Database/DatabaseEloquentFactoryTest.php index cc3c78504554..8ad503eec632 100644 --- a/tests/Database/DatabaseEloquentFactoryTest.php +++ b/tests/Database/DatabaseEloquentFactoryTest.php @@ -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)