From 2f1f3301bdb4d6ac45f980702e2d2bc544e2d104 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Mon, 8 Jul 2024 10:08:27 -0500 Subject: [PATCH] feat: improve Factory generics (#52005) --- .../Database/Eloquent/Factories/Factory.php | 58 ++++--- .../Eloquent/Factories/HasFactory.php | 19 ++- .../Database/Fixtures/Models/Money/Price.php | 6 +- types/Autoload.php | 20 +++ types/Database/Eloquent/Factories/Factory.php | 153 ++++++------------ types/Database/Eloquent/Model.php | 14 +- 6 files changed, 125 insertions(+), 145 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index e8c25101361b..fd9648aa1f4b 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -32,7 +32,7 @@ abstract class Factory /** * The name of the factory's corresponding model. * - * @var class-string<\Illuminate\Database\Eloquent\Model|TModel> + * @var class-string */ protected $model; @@ -109,7 +109,7 @@ abstract class Factory /** * The default model name resolver. * - * @var callable + * @var callable(self): class-string */ protected static $modelNameResolver; @@ -214,7 +214,7 @@ public function raw($attributes = [], ?Model $parent = null) * Create a single model and persist it to the database. * * @param (callable(array): array)|array $attributes - * @return \Illuminate\Database\Eloquent\Model|TModel + * @return TModel */ public function createOne($attributes = []) { @@ -225,7 +225,7 @@ public function createOne($attributes = []) * Create a single model and persist it to the database without dispatching any model events. * * @param (callable(array): array)|array $attributes - * @return \Illuminate\Database\Eloquent\Model|TModel + * @return TModel */ public function createOneQuietly($attributes = []) { @@ -236,7 +236,7 @@ public function createOneQuietly($attributes = []) * Create a collection of models and persist them to the database. * * @param int|null|iterable> $records - * @return \Illuminate\Database\Eloquent\Collection + * @return \Illuminate\Database\Eloquent\Collection */ public function createMany(int|iterable|null $records = null) { @@ -259,7 +259,7 @@ public function createMany(int|iterable|null $records = null) * Create a collection of models and persist them to the database without dispatching any model events. * * @param int|null|iterable> $records - * @return \Illuminate\Database\Eloquent\Collection + * @return \Illuminate\Database\Eloquent\Collection */ public function createManyQuietly(int|iterable|null $records = null) { @@ -273,7 +273,7 @@ public function createManyQuietly(int|iterable|null $records = null) * * @param (callable(array): array)|array $attributes * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|TModel + * @return \Illuminate\Database\Eloquent\Collection|TModel */ public function create($attributes = [], ?Model $parent = null) { @@ -301,7 +301,7 @@ public function create($attributes = [], ?Model $parent = null) * * @param (callable(array): array)|array $attributes * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|TModel + * @return \Illuminate\Database\Eloquent\Collection|TModel */ public function createQuietly($attributes = [], ?Model $parent = null) { @@ -315,7 +315,7 @@ public function createQuietly($attributes = [], ?Model $parent = null) * * @param array $attributes * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Closure(): (\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|TModel) + * @return \Closure(): (\Illuminate\Database\Eloquent\Collection|TModel) */ public function lazy(array $attributes = [], ?Model $parent = null) { @@ -325,7 +325,7 @@ public function lazy(array $attributes = [], ?Model $parent = null) /** * Set the connection name on the results and store them. * - * @param \Illuminate\Support\Collection $results + * @param \Illuminate\Support\Collection $results * @return void */ protected function store(Collection $results) @@ -366,7 +366,7 @@ protected function createChildren(Model $model) * Make a single instance of the model. * * @param (callable(array): array)|array $attributes - * @return \Illuminate\Database\Eloquent\Model|TModel + * @return TModel */ public function makeOne($attributes = []) { @@ -378,7 +378,7 @@ public function makeOne($attributes = []) * * @param (callable(array): array)|array $attributes * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|TModel + * @return \Illuminate\Database\Eloquent\Collection|TModel */ public function make($attributes = [], ?Model $parent = null) { @@ -504,7 +504,7 @@ protected function expandAttributes(array $definition) /** * Add a new state transformation to the model definition. * - * @param (callable(array, \Illuminate\Database\Eloquent\Model|null): array)|array $state + * @param (callable(array, TModel|null): array)|array $state * @return static */ public function state($state) @@ -654,8 +654,10 @@ public function recycle($model) /** * Retrieve a random model of a given type from previously provided models to recycle. * - * @param string $modelClassName - * @return \Illuminate\Database\Eloquent\Model|null + * @template TClass of \Illuminate\Database\Eloquent\Model + * + * @param class-string $modelClassName + * @return TClass|null */ public function getRandomRecycledModel($modelClassName) { @@ -665,7 +667,7 @@ public function getRandomRecycledModel($modelClassName) /** * Add a new "after making" callback to the model definition. * - * @param \Closure(\Illuminate\Database\Eloquent\Model|TModel): mixed $callback + * @param \Closure(TModel): mixed $callback * @return static */ public function afterMaking(Closure $callback) @@ -676,7 +678,7 @@ public function afterMaking(Closure $callback) /** * Add a new "after creating" callback to the model definition. * - * @param \Closure(\Illuminate\Database\Eloquent\Model|TModel): mixed $callback + * @param \Closure(TModel): mixed $callback * @return static */ public function afterCreating(Closure $callback) @@ -761,7 +763,7 @@ protected function newInstance(array $arguments = []) * Get a new model instance. * * @param array $attributes - * @return \Illuminate\Database\Eloquent\Model|TModel + * @return TModel */ public function newModel(array $attributes = []) { @@ -773,7 +775,7 @@ public function newModel(array $attributes = []) /** * Get the name of the model that is generated by the factory. * - * @return class-string<\Illuminate\Database\Eloquent\Model|TModel> + * @return class-string */ public function modelName() { @@ -797,7 +799,7 @@ public function modelName() /** * Specify the callback that should be invoked to guess model names based on factory names. * - * @param callable(self): class-string<\Illuminate\Database\Eloquent\Model|TModel> $callback + * @param callable(self): class-string $callback * @return void */ public static function guessModelNamesUsing(callable $callback) @@ -819,8 +821,10 @@ public static function useNamespace(string $namespace) /** * Get a new factory instance for the given model name. * - * @param class-string<\Illuminate\Database\Eloquent\Model> $modelName - * @return \Illuminate\Database\Eloquent\Factories\Factory + * @template TClass of \Illuminate\Database\Eloquent\Model + * + * @param class-string $modelName + * @return \Illuminate\Database\Eloquent\Factories\Factory */ public static function factoryForModel(string $modelName) { @@ -853,8 +857,10 @@ protected function withFaker() /** * Get the factory name for the given model name. * - * @param class-string<\Illuminate\Database\Eloquent\Model> $modelName - * @return class-string<\Illuminate\Database\Eloquent\Factories\Factory> + * @template TClass of \Illuminate\Database\Eloquent\Model + * + * @param class-string $modelName + * @return class-string<\Illuminate\Database\Eloquent\Factories\Factory> */ public static function resolveFactoryName(string $modelName) { @@ -880,8 +886,8 @@ protected static function appNamespace() { try { return Container::getInstance() - ->make(Application::class) - ->getNamespace(); + ->make(Application::class) + ->getNamespace(); } catch (Throwable) { return 'App\\'; } diff --git a/src/Illuminate/Database/Eloquent/Factories/HasFactory.php b/src/Illuminate/Database/Eloquent/Factories/HasFactory.php index f10281d14100..afd20e6ed73e 100644 --- a/src/Illuminate/Database/Eloquent/Factories/HasFactory.php +++ b/src/Illuminate/Database/Eloquent/Factories/HasFactory.php @@ -2,18 +2,21 @@ namespace Illuminate\Database\Eloquent\Factories; +/** + * @template TFactory of \Illuminate\Database\Eloquent\Factories\Factory + */ trait HasFactory { /** * Get a new factory instance for the model. * - * @param callable|array|int|null $count - * @param callable|array $state - * @return \Illuminate\Database\Eloquent\Factories\Factory + * @param (callable(array, static|null): array)|array|int|null $count + * @param (callable(array, static|null): array)|array $state + * @return TFactory */ public static function factory($count = null, $state = []) { - $factory = static::newFactory() ?: Factory::factoryForModel(get_called_class()); + $factory = static::newFactory() ?? Factory::factoryForModel(get_called_class()); return $factory ->count(is_numeric($count) ? $count : null) @@ -23,10 +26,14 @@ public static function factory($count = null, $state = []) /** * Create a new factory instance for the model. * - * @return \Illuminate\Database\Eloquent\Factories\Factory + * @return TFactory|null */ protected static function newFactory() { - // + if (isset(static::$factory)) { + return static::$factory::new(); + } + + return null; } } diff --git a/tests/Database/Fixtures/Models/Money/Price.php b/tests/Database/Fixtures/Models/Money/Price.php index 7fd74460736d..f71f8afbc11f 100644 --- a/tests/Database/Fixtures/Models/Money/Price.php +++ b/tests/Database/Fixtures/Models/Money/Price.php @@ -8,12 +8,10 @@ class Price extends Model { + /** @use HasFactory */ use HasFactory; protected $table = 'prices'; - public static function factory() - { - return PriceFactory::new(); - } + protected static string $factory = PriceFactory::class; } diff --git a/types/Autoload.php b/types/Autoload.php index 23e08d6064f4..3df6d82b0780 100644 --- a/types/Autoload.php +++ b/types/Autoload.php @@ -1,7 +1,9 @@ */ use HasFactory; use MassPrunable; use SoftDeletes; + + protected static string $factory = UserFactory::class; +} + +/** @extends Factory */ +class UserFactory extends Factory +{ + protected $model = User::class; + + public function definition(): array + { + return []; + } +} + +class Post extends Model +{ } enum UserType diff --git a/types/Database/Eloquent/Factories/Factory.php b/types/Database/Eloquent/Factories/Factory.php index 433dee6fb204..0d59c01adc81 100644 --- a/types/Database/Eloquent/Factories/Factory.php +++ b/types/Database/Eloquent/Factories/Factory.php @@ -4,28 +4,15 @@ use function PHPStan\Testing\assertType; -/** - * @extends Illuminate\Database\Eloquent\Factories\Factory - */ +/** @extends Illuminate\Database\Eloquent\Factories\Factory */ class UserFactory extends Factory { - /** - * The name of the factory's corresponding model. - * - * @var class-string - */ protected $model = User::class; - /** - * Define the model's default state. - * - * @return array - */ - public function definition() + /** @return array */ + public function definition(): array { - return [ - // - ]; + return []; } } @@ -51,100 +38,60 @@ public function definition() return ['string' => 'string']; })); -// assertType('User', $factory->createOne()); -// assertType('User', $factory->createOne(['string' => 'string'])); -assertType('Illuminate\Database\Eloquent\Model', $factory->createOne()); -assertType('Illuminate\Database\Eloquent\Model', $factory->createOne(['string' => 'string'])); -assertType('Illuminate\Database\Eloquent\Model', $factory->createOne(function ($attributes) { +assertType('User', $factory->createOne()); +assertType('User', $factory->createOne(['string' => 'string'])); +assertType('User', $factory->createOne(function ($attributes) { assertType('array', $attributes); return ['string' => 'string']; })); -// assertType('User', $factory->createOneQuietly()); -// assertType('User', $factory->createOneQuietly(['string' => 'string'])); -assertType('Illuminate\Database\Eloquent\Model', $factory->createOneQuietly()); -assertType('Illuminate\Database\Eloquent\Model', $factory->createOneQuietly(['string' => 'string'])); -assertType('Illuminate\Database\Eloquent\Model', $factory->createOneQuietly(function ($attributes) { +assertType('User', $factory->createOneQuietly()); +assertType('User', $factory->createOneQuietly(['string' => 'string'])); +assertType('User', $factory->createOneQuietly(function ($attributes) { assertType('array', $attributes); return ['string' => 'string']; })); -// assertType('Illuminate\Database\Eloquent\Collection', $factory->createMany([['string' => 'string']])); -assertType('Illuminate\Database\Eloquent\Collection', $factory->createMany( - [['string' => 'string']] -)); -assertType('Illuminate\Database\Eloquent\Collection', $factory->createMany(3)); -assertType('Illuminate\Database\Eloquent\Collection', $factory->createMany()); - -// assertType('Illuminate\Database\Eloquent\Collection', $factory->createManyQuietly([['string' => 'string']])); -assertType('Illuminate\Database\Eloquent\Collection', $factory->createManyQuietly( - [['string' => 'string']] -)); -assertType('Illuminate\Database\Eloquent\Collection', $factory->createManyQuietly(3)); -assertType('Illuminate\Database\Eloquent\Collection', $factory->createManyQuietly()); - -// assertType('Illuminate\Database\Eloquent\Collection|User', $factory->create()); -// assertType('Illuminate\Database\Eloquent\Collection|User', $factory->create([ -// 'string' => 'string', -// ])); -assertType('Illuminate\Database\Eloquent\Collection|Illuminate\Database\Eloquent\Model', $factory->create()); -assertType('Illuminate\Database\Eloquent\Collection|Illuminate\Database\Eloquent\Model', $factory->create([ - 'string' => 'string', -])); -assertType('Illuminate\Database\Eloquent\Collection|Illuminate\Database\Eloquent\Model', $factory->create(function ($attributes) { +assertType('Illuminate\Database\Eloquent\Collection', $factory->createMany([['string' => 'string']])); +assertType('Illuminate\Database\Eloquent\Collection', $factory->createMany(3)); +assertType('Illuminate\Database\Eloquent\Collection', $factory->createMany()); + +assertType('Illuminate\Database\Eloquent\Collection', $factory->createManyQuietly([['string' => 'string']])); +assertType('Illuminate\Database\Eloquent\Collection', $factory->createManyQuietly(3)); +assertType('Illuminate\Database\Eloquent\Collection', $factory->createManyQuietly()); + +assertType('Illuminate\Database\Eloquent\Collection|User', $factory->create()); +assertType('Illuminate\Database\Eloquent\Collection|User', $factory->create(['string' => 'string'])); +assertType('Illuminate\Database\Eloquent\Collection|User', $factory->create(function ($attributes) { assertType('array', $attributes); return ['string' => 'string']; })); -// assertType('Illuminate\Database\Eloquent\Collection|User', $factory->createQuietly()); -// assertType('Illuminate\Database\Eloquent\Collection|User', $factory->createQuietly([ -// 'string' => 'string', -// ])); -assertType('Illuminate\Database\Eloquent\Collection|Illuminate\Database\Eloquent\Model', $factory->createQuietly()); -assertType('Illuminate\Database\Eloquent\Collection|Illuminate\Database\Eloquent\Model', $factory->createQuietly([ - 'string' => 'string', -])); -assertType('Illuminate\Database\Eloquent\Collection|Illuminate\Database\Eloquent\Model', $factory->createQuietly(function ($attributes) { +assertType('Illuminate\Database\Eloquent\Collection|User', $factory->createQuietly()); +assertType('Illuminate\Database\Eloquent\Collection|User', $factory->createQuietly(['string' => 'string'])); +assertType('Illuminate\Database\Eloquent\Collection|User', $factory->createQuietly(function ($attributes) { assertType('array', $attributes); return ['string' => 'string']; })); -// assertType('Closure(): Illuminate\Database\Eloquent\Collection|User', $factory->lazy()); -// assertType('Closure(): Illuminate\Database\Eloquent\Collection|User', $factory->lazy([ -// 'string' => 'string', -// ])); -assertType('Closure(): (Illuminate\Database\Eloquent\Collection|Illuminate\Database\Eloquent\Model)', $factory->lazy()); -assertType('Closure(): (Illuminate\Database\Eloquent\Collection|Illuminate\Database\Eloquent\Model)', $factory->lazy([ - 'string' => 'string', -])); - -// assertType('User', $factory->makeOne()); -// assertType('User', $factory->makeOne([ -// 'string' => 'string', -// ])); -assertType('Illuminate\Database\Eloquent\Model', $factory->makeOne()); -assertType('Illuminate\Database\Eloquent\Model', $factory->makeOne([ - 'string' => 'string', -])); -assertType('Illuminate\Database\Eloquent\Model', $factory->makeOne(function ($attributes) { +assertType('Closure(): (Illuminate\Database\Eloquent\Collection|User)', $factory->lazy()); +assertType('Closure(): (Illuminate\Database\Eloquent\Collection|User)', $factory->lazy(['string' => 'string'])); + +assertType('User', $factory->makeOne()); +assertType('User', $factory->makeOne(['string' => 'string'])); +assertType('User', $factory->makeOne(function ($attributes) { assertType('array', $attributes); return ['string' => 'string']; })); -// assertType('Illuminate\Database\Eloquent\Collection|User', $factory->make()); -// assertType('Illuminate\Database\Eloquent\Collection|User', $factory->make([ -// 'string' => 'string', -// ])); -assertType('Illuminate\Database\Eloquent\Collection|Illuminate\Database\Eloquent\Model', $factory->make()); -assertType('Illuminate\Database\Eloquent\Collection|Illuminate\Database\Eloquent\Model', $factory->make([ - 'string' => 'string', -])); -assertType('Illuminate\Database\Eloquent\Collection|Illuminate\Database\Eloquent\Model', $factory->make(function ($attributes) { +assertType('Illuminate\Database\Eloquent\Collection|User', $factory->make()); +assertType('Illuminate\Database\Eloquent\Collection|User', $factory->make(['string' => 'string'])); +assertType('Illuminate\Database\Eloquent\Collection|User', $factory->make(function ($attributes) { assertType('array', $attributes); return ['string' => 'string']; @@ -158,7 +105,7 @@ public function definition() })); assertType('UserFactory', $factory->state(function ($attributes, $model) { assertType('array', $attributes); - assertType('Illuminate\Database\Eloquent\Model|null', $model); + assertType('User|null', $model); return ['string' => 'string']; })); @@ -176,23 +123,15 @@ public function definition() assertType('UserFactory', $factory->for($factory)); assertType('UserFactory', $factory->for($factory->createOne())); -// assertType('UserFactory', $factory->afterMaking(function ($user) { -// assertType('User', $user); -// })); -assertType('UserFactory', $factory->afterMaking(function ($user) { - assertType('Illuminate\Database\Eloquent\Model', $user); -})); assertType('UserFactory', $factory->afterMaking(function ($user) { + assertType('User', $user); + return 'string'; })); -// assertType('UserFactory', $factory->afterCreating(function ($user) { -// assertType('User', $user); -// })); -assertType('UserFactory', $factory->afterCreating(function ($user) { - assertType('Illuminate\Database\Eloquent\Model', $user); -})); assertType('UserFactory', $factory->afterCreating(function ($user) { + assertType('User', $user); + return 'string'; })); @@ -200,13 +139,12 @@ public function definition() assertType('UserFactory', $factory->connection('string')); -// assertType('User', $factory->newModel()); -// assertType('User', $factory->newModel(['string' => 'string'])); -assertType('Illuminate\Database\Eloquent\Model', $factory->newModel()); -assertType('Illuminate\Database\Eloquent\Model', $factory->newModel(['string' => 'string'])); +assertType('User', $factory->newModel()); +assertType('User', $factory->newModel(['string' => 'string'])); -// assertType('class-string', $factory->modelName()); -assertType('class-string', $factory->modelName()); +assertType('class-string', $factory->modelName()); + +assertType('Post|null', $factory->getRandomRecycledModel(Post::class)); Factory::guessModelNamesUsing(function (Factory $factory) { return match (true) { @@ -217,9 +155,8 @@ public function definition() $factory->useNamespace('string'); -assertType(Factory::class, $factory::factoryForModel(User::class)); - -assertType('class-string', $factory->resolveFactoryName(User::class)); +assertType('Illuminate\Database\Eloquent\Factories\Factory', $factory::factoryForModel(User::class)); +assertType('class-string>', $factory->resolveFactoryName(User::class)); Factory::guessFactoryNamesUsing(function (string $modelName) { return match ($modelName) { diff --git a/types/Database/Eloquent/Model.php b/types/Database/Eloquent/Model.php index 8ba71877fe6a..29c7d477e266 100644 --- a/types/Database/Eloquent/Model.php +++ b/types/Database/Eloquent/Model.php @@ -9,7 +9,19 @@ function test(User $user): void { - assertType('Illuminate\Database\Eloquent\Factories\Factory', User::factory()); + assertType('UserFactory', User::factory(function ($attributes, $model) { + assertType('array', $attributes); + assertType('User|null', $model); + + return ['string' => 'string']; + })); + assertType('UserFactory', User::factory(42, function ($attributes, $model) { + assertType('array', $attributes); + assertType('User|null', $model); + + return ['string' => 'string']; + })); + assertType('Illuminate\Database\Eloquent\Builder', User::query()); assertType('Illuminate\Database\Eloquent\Builder', $user->newQuery()); assertType('Illuminate\Database\Eloquent\Builder', $user->withTrashed());