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));