From 9afe5089e570f53b83598a9f0cc45857282af68b Mon Sep 17 00:00:00 2001 From: Nuno Maduro <enunomaduro@gmail.com> Date: Tue, 12 Oct 2021 13:45:11 +0100 Subject: [PATCH] Makes database factories generic --- .../Console/Factories/stubs/factory.stub | 3 + .../Database/Eloquent/Factories/Factory.php | 75 ++++---- types/Database/Eloquent/Factories/Factory.php | 171 ++++++++++++++++++ 3 files changed, 213 insertions(+), 36 deletions(-) create mode 100644 types/Database/Eloquent/Factories/Factory.php diff --git a/src/Illuminate/Database/Console/Factories/stubs/factory.stub b/src/Illuminate/Database/Console/Factories/stubs/factory.stub index f7a898c9f1fe..2b6d5cd183f4 100644 --- a/src/Illuminate/Database/Console/Factories/stubs/factory.stub +++ b/src/Illuminate/Database/Console/Factories/stubs/factory.stub @@ -5,6 +5,9 @@ namespace {{ factoryNamespace }}; use Illuminate\Database\Eloquent\Factories\Factory; use {{ namespacedModel }}; +/** + * @extends \Illuminate\Database\Eloquent\Factories\Factory<\{{ namespacedModel }}> + */ class {{ factory }}Factory extends Factory { /** diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index 11cb5adc61db..3e6faea541da 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -14,6 +14,9 @@ use Illuminate\Support\Traits\Macroable; use Throwable; +/** + * @template TModel of \Illuminate\Database\Eloquent\Model + */ abstract class Factory { use ForwardsCalls, Macroable { @@ -23,7 +26,7 @@ abstract class Factory /** * The name of the factory's corresponding model. * - * @var string + * @var class-string<\Illuminate\Database\Eloquent\Model|TModel> */ protected $model; @@ -137,14 +140,14 @@ public function __construct($count = null, /** * Define the model's default state. * - * @return array + * @return array<string, mixed> */ abstract public function definition(); /** * Get a new factory instance for the given attributes. * - * @param callable|array $attributes + * @param (callable(): array<string, mixed>)|array<string, mixed> $attributes * @return static */ public static function new($attributes = []) @@ -176,9 +179,9 @@ public function configure() /** * Get the raw attributes generated by the factory. * - * @param array $attributes + * @param array<string, mixed> $attributes * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return array + * @return array<int|string, mixed> */ public function raw($attributes = [], ?Model $parent = null) { @@ -194,8 +197,8 @@ public function raw($attributes = [], ?Model $parent = null) /** * Create a single model and persist it to the database. * - * @param array $attributes - * @return \Illuminate\Database\Eloquent\Model + * @param array<string, mixed> $attributes + * @return \Illuminate\Database\Eloquent\Model|TModel */ public function createOne($attributes = []) { @@ -205,8 +208,8 @@ public function createOne($attributes = []) /** * Create a single model and persist it to the database. * - * @param array $attributes - * @return \Illuminate\Database\Eloquent\Model + * @param array<string, mixed> $attributes + * @return \Illuminate\Database\Eloquent\Model|TModel */ public function createOneQuietly($attributes = []) { @@ -216,8 +219,8 @@ public function createOneQuietly($attributes = []) /** * Create a collection of models and persist them to the database. * - * @param iterable $records - * @return \Illuminate\Database\Eloquent\Collection + * @param iterable<int, array<string, mixed>> $records + * @return \Illuminate\Database\Eloquent\Collection<int, \Illuminate\Database\Eloquent\Model|TModel> */ public function createMany(iterable $records) { @@ -231,8 +234,8 @@ public function createMany(iterable $records) /** * Create a collection of models and persist them to the database. * - * @param iterable $records - * @return \Illuminate\Database\Eloquent\Collection + * @param iterable<int, array<string, mixed>> $records + * @return \Illuminate\Database\Eloquent\Collection<int, \Illuminate\Database\Eloquent\Model|TModel> */ public function createManyQuietly(iterable $records) { @@ -244,9 +247,9 @@ public function createManyQuietly(iterable $records) /** * Create a collection of models and persist them to the database. * - * @param array $attributes + * @param array<string, mixed> $attributes * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model + * @return \Illuminate\Database\Eloquent\Collection<int, \Illuminate\Database\Eloquent\Model|TModel>|\Illuminate\Database\Eloquent\Model|TModel */ public function create($attributes = [], ?Model $parent = null) { @@ -272,9 +275,9 @@ public function create($attributes = [], ?Model $parent = null) /** * Create a collection of models and persist them to the database. * - * @param array $attributes + * @param array<string, mixed> $attributes * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model + * @return \Illuminate\Database\Eloquent\Collection<int, \Illuminate\Database\Eloquent\Model|TModel>|\Illuminate\Database\Eloquent\Model|TModel */ public function createQuietly($attributes = [], ?Model $parent = null) { @@ -286,9 +289,9 @@ public function createQuietly($attributes = [], ?Model $parent = null) /** * Create a callback that persists a model in the database when invoked. * - * @param array $attributes + * @param array<string, mixed> $attributes * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Closure + * @return \Closure(): (\Illuminate\Database\Eloquent\Collection<int, \Illuminate\Database\Eloquent\Model|TModel>|\Illuminate\Database\Eloquent\Model|TModel) */ public function lazy(array $attributes = [], ?Model $parent = null) { @@ -334,8 +337,8 @@ protected function createChildren(Model $model) /** * Make a single instance of the model. * - * @param callable|array $attributes - * @return \Illuminate\Database\Eloquent\Model + * @param (callable(): array<string, mixed>)|array<string, mixed> $attributes + * @return \Illuminate\Database\Eloquent\Model|TModel */ public function makeOne($attributes = []) { @@ -345,9 +348,9 @@ public function makeOne($attributes = []) /** * Create a collection of models. * - * @param array $attributes + * @param array<string, mixed> $attributes * @param \Illuminate\Database\Eloquent\Model|null $parent - * @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model + * @return \Illuminate\Database\Eloquent\Collection<int, \Illuminate\Database\Eloquent\Model|TModel>|\Illuminate\Database\Eloquent\Model|TModel */ public function make($attributes = [], ?Model $parent = null) { @@ -465,7 +468,7 @@ protected function expandAttributes(array $definition) /** * Add a new state transformation to the model definition. * - * @param callable|array $state + * @param (callable(): array<string, mixed>)|array<string, mixed> $state * @return static */ public function state($state) @@ -523,7 +526,7 @@ protected function guessRelationship(string $related) * Define an attached relationship for the model. * * @param \Illuminate\Database\Eloquent\Factories\Factory|\Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model $factory - * @param callable|array $pivot + * @param (callable(): array<string, mixed>)|array<string, mixed> $pivot * @param string|null $relationship * @return static */ @@ -562,7 +565,7 @@ public function for($factory, $relationship = null) /** * Add a new "after making" callback to the model definition. * - * @param \Closure $callback + * @param \Closure(\Illuminate\Database\Eloquent\Model|TModel): mixed $callback * @return static */ public function afterMaking(Closure $callback) @@ -573,7 +576,7 @@ public function afterMaking(Closure $callback) /** * Add a new "after creating" callback to the model definition. * - * @param \Closure $callback + * @param \Closure(\Illuminate\Database\Eloquent\Model|TModel): mixed $callback * @return static */ public function afterCreating(Closure $callback) @@ -656,8 +659,8 @@ protected function newInstance(array $arguments = []) /** * Get a new model instance. * - * @param array $attributes - * @return \Illuminate\Database\Eloquent\Model + * @param array<string, mixed> $attributes + * @return \Illuminate\Database\Eloquent\Model|TModel */ public function newModel(array $attributes = []) { @@ -669,7 +672,7 @@ public function newModel(array $attributes = []) /** * Get the name of the model that is generated by the factory. * - * @return string + * @return class-string<\Illuminate\Database\Eloquent\Model|TModel> */ public function modelName() { @@ -689,7 +692,7 @@ public function modelName() /** * Specify the callback that should be invoked to guess model names based on factory names. * - * @param callable $callback + * @param callable(): class-string<\Illuminate\Database\Eloquent\Model|TModel> $callback * @return void */ public static function guessModelNamesUsing(callable $callback) @@ -711,8 +714,8 @@ public static function useNamespace(string $namespace) /** * Get a new factory instance for the given model name. * - * @param string $modelName - * @return static + * @param class-string<\Illuminate\Database\Eloquent\Model> $modelName + * @return \Illuminate\Database\Eloquent\Factories\Factory */ public static function factoryForModel(string $modelName) { @@ -724,7 +727,7 @@ public static function factoryForModel(string $modelName) /** * Specify the callback that should be invoked to guess factory names based on dynamic relationship names. * - * @param callable $callback + * @param callable(): class-string<\Illuminate\Database\Eloquent\Model|TModel> $callback * @return void */ public static function guessFactoryNamesUsing(callable $callback) @@ -745,8 +748,8 @@ protected function withFaker() /** * Get the factory name for the given model name. * - * @param string $modelName - * @return string + * @param class-string<\Illuminate\Database\Eloquent\Model> $modelName + * @return class-string<\Illuminate\Database\Eloquent\Factories\Factory> */ public static function resolveFactoryName(string $modelName) { diff --git a/types/Database/Eloquent/Factories/Factory.php b/types/Database/Eloquent/Factories/Factory.php new file mode 100644 index 000000000000..902d4d393b8f --- /dev/null +++ b/types/Database/Eloquent/Factories/Factory.php @@ -0,0 +1,171 @@ +<?php + +use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Foundation\Auth\User as Authenticatable; +use function PHPStan\Testing\assertType; + +class User extends Authenticatable +{ +} + +/** + * @extends Illuminate\Database\Eloquent\Factories\Factory<User> + */ +class UserFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = User::class; + + /** + * Define the model's default state. + * + * @return array<string, mixed> + */ + public function definition() + { + return [ + // + ]; + } +} + +$factory = UserFactory::new(); +assertType('UserFactory', $factory); + +assertType('array<string, mixed>', $factory->definition()); + +assertType('UserFactory', $factory::times(10)); + +assertType('UserFactory', $factory->configure()); + +assertType('array<int|string, mixed>', $factory->raw()); +assertType('array<int|string, mixed>', $factory->raw(['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('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\Collection<int, User>', $factory->createMany([['string' => 'string']])); +assertType('Illuminate\Database\Eloquent\Collection<int, Illuminate\Database\Eloquent\Model>', $factory->createMany( + [['string' => 'string']] +)); + +// assertType('Illuminate\Database\Eloquent\Collection<int, User>', $factory->createManyQuietly([['string' => 'string']])); +assertType('Illuminate\Database\Eloquent\Collection<int, Illuminate\Database\Eloquent\Model>', $factory->createManyQuietly( + [['string' => 'string']] +)); + +// assertType('Illuminate\Database\Eloquent\Collection<int, User>|User', $factory->create()); +// assertType('Illuminate\Database\Eloquent\Collection<int, User>|User', $factory->create([ +// 'string' => 'string', +// ])); +assertType('Illuminate\Database\Eloquent\Collection<int, Illuminate\Database\Eloquent\Model>|Illuminate\Database\Eloquent\Model', $factory->create()); +assertType('Illuminate\Database\Eloquent\Collection<int, Illuminate\Database\Eloquent\Model>|Illuminate\Database\Eloquent\Model', $factory->create([ + 'string' => 'string', +])); + +// assertType('Illuminate\Database\Eloquent\Collection<int, User>|User', $factory->createQuietly()); +// assertType('Illuminate\Database\Eloquent\Collection<int, User>|User', $factory->createQuietly([ +// 'string' => 'string', +// ])); +assertType('Illuminate\Database\Eloquent\Collection<int, Illuminate\Database\Eloquent\Model>|Illuminate\Database\Eloquent\Model', $factory->createQuietly()); +assertType('Illuminate\Database\Eloquent\Collection<int, Illuminate\Database\Eloquent\Model>|Illuminate\Database\Eloquent\Model', $factory->createQuietly([ + 'string' => 'string', +])); + +// assertType('Closure(): Illuminate\Database\Eloquent\Collection<int, User>|User', $factory->lazy()); +// assertType('Closure(): Illuminate\Database\Eloquent\Collection<int, User>|User', $factory->lazy([ +// 'string' => 'string', +// ])); +assertType('Closure(): Illuminate\Database\Eloquent\Collection<int, Illuminate\Database\Eloquent\Model>|Illuminate\Database\Eloquent\Model', $factory->lazy()); +assertType('Closure(): Illuminate\Database\Eloquent\Collection<int, Illuminate\Database\Eloquent\Model>|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\Collection<int, User>|User', $factory->make()); +// assertType('Illuminate\Database\Eloquent\Collection<int, User>|User', $factory->make([ +// 'string' => 'string', +// ])); +assertType('Illuminate\Database\Eloquent\Collection<int, Illuminate\Database\Eloquent\Model>|Illuminate\Database\Eloquent\Model', $factory->make()); +assertType('Illuminate\Database\Eloquent\Collection<int, Illuminate\Database\Eloquent\Model>|Illuminate\Database\Eloquent\Model', $factory->make([ + 'string' => 'string', +])); + +assertType('UserFactory', $factory->state(['string' => 'string'])); +assertType('UserFactory', $factory->state(function () { + return ['string' => 'string']; +})); + +assertType('UserFactory', $factory->sequence([['string' => 'string']])); + +assertType('UserFactory', $factory->has($factory)); + +assertType('UserFactory', $factory->hasAttached($factory, ['string' => 'string'])); +assertType('UserFactory', $factory->hasAttached($factory->createOne(), ['string' => 'string'])); +assertType('UserFactory', $factory->hasAttached($factory->createOne(), function () { + return ['string' => 'string']; +})); + +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) { + 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) { + return 'string'; +})); + +assertType('UserFactory', $factory->count(10)); + +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('class-string<User>', $factory->modelName()); +assertType('class-string<Illuminate\Database\Eloquent\Model>', $factory->modelName()); + +$factory->guessModelNamesUsing(function () { + return User::class; +}); + +$factory->useNamespace('string'); + +assertType(Factory::class, $factory::factoryForModel(User::class)); + +assertType('class-string<Illuminate\Database\Eloquent\Factories\Factory>', $factory->resolveFactoryName(User::class));