diff --git a/config/maker.xml b/config/maker.xml
index a2dfa1d..da8b34c 100644
--- a/config/maker.xml
+++ b/config/maker.xml
@@ -15,6 +15,12 @@
+
+
+
+
+
+
%wouterj_eloquent.migration_path%
diff --git a/src/Factory/Factory.php b/src/Factory/Factory.php
new file mode 100644
index 0000000..1c9c0b5
--- /dev/null
+++ b/src/Factory/Factory.php
@@ -0,0 +1,28 @@
+
+ */
+abstract class Factory extends IlluminateFactory
+{
+ public function modelName(): string
+ {
+ /** @psalm-suppress RedundantPropertyInitializationCheck */
+ $resolver = static::$modelNameResolver ?? function (self $factory) {
+ $name = $factory::class;
+ if (str_ends_with($name, 'Factory')) {
+ $name = substr($name, 0, -7);
+ }
+
+ return str_replace('\\Factory\\', '\\Model\\', $name);
+ };
+
+ return $this->model ?? $resolver($this);
+ }
+}
diff --git a/src/Maker/MakeFactory.php b/src/Maker/MakeFactory.php
new file mode 100644
index 0000000..1f1093e
--- /dev/null
+++ b/src/Maker/MakeFactory.php
@@ -0,0 +1,115 @@
+
+ */
+class MakeFactory extends AbstractMaker
+{
+ private $fileManager;
+
+ public function __construct(FileManager $fileManager)
+ {
+ $this->fileManager = $fileManager;
+ }
+
+ public static function getCommandName(): string
+ {
+ return 'make:factory';
+ }
+
+ public static function getCommandDescription(): string
+ {
+ return 'Create a new Eloquent model factory';
+ }
+
+ public function configureCommand(Command $command, InputConfiguration $inputConfig): void
+ {
+ $command
+ ->setDescription(self::getCommandDescription())
+ ->addArgument('name', InputArgument::REQUIRED, 'The name of the factory')
+ ->addOption('model', 'm', InputOption::VALUE_REQUIRED, 'The name of the model')
+ ;
+ }
+
+ public function configureDependencies(DependencyBuilder $dependencies): void
+ {
+ }
+
+ public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
+ {
+ $factoryClassDetails = $generator->createClassNameDetails($input->getArgument('name'), 'Factory', 'Factory');
+ if (class_exists($factoryClassDetails->getFullName())) {
+ $io->error(sprintf('Factory "%s" already exists!', $factoryClassDetails->getFullName()));
+
+ return;
+ }
+
+ $factoryFqcn = $factoryClassDetails->getFullName();
+ $factory = $factoryClassDetails->getRelativeNameWithoutSuffix();
+
+ $modelFqcn = $generator->createClassNameDetails($input->getOption('model') ?? $this->guessModelName($generator, $factoryClassDetails), 'Model')->getFullName();
+ $model = Str::getShortClassName($modelFqcn);
+
+ $stubPath = dirname((new \ReflectionClass(FactoryMakeCommand::class))->getFileName()).'/stubs';
+ $stub = file_get_contents($stubPath.'/factory.stub');
+
+ $replace = [
+ 'NamespacedDummyModel' => $modelFqcn,
+ '{{ namespacedModel }}' => $modelFqcn,
+ '{{namespacedModel}}' => $modelFqcn,
+ 'DummyModel' => $model,
+ '{{ model }}' => $model,
+ '{{model}}' => $model,
+ '{{ factory }}' => $factory,
+ '{{factory}}' => $factory,
+ IlluminateFactory::class => Factory::class
+ ];
+
+ if ($namespace = Str::getNamespace($factoryFqcn)) {
+ $replace['{{ factoryNamespace }}'] = $namespace;
+ }
+
+ $stub = str_replace(array_keys($replace), array_values($replace), $stub);
+
+ $path = $this->fileManager->getRelativePathForFutureClass($factoryFqcn);
+ if (null === $path) {
+ throw new \LogicException(sprintf('Could not determine where to locate the new class "%s", maybe try with a full namespace like "\\My\\Full\\Namespace\\%s"', $factoryFqcn, $factoryClassDetails->getShortName()));
+ }
+
+ $this->fileManager->dumpFile($path, $stub);
+ }
+
+ private function guessModelName(Generator $generator, ClassNameDetails $factoryClassDetails): string
+ {
+ $name = $factoryClassDetails->getRelativeNameWithoutSuffix();
+
+ $modelClassDetails = $generator->createClassNameDetails($name, 'Model');
+
+ if (class_exists($modelClassDetails->getFullName())) {
+ return '\\'.$modelClassDetails->getFullName();
+ }
+
+
+ return '\\'.$generator->getRootNamespace().'\\Model';
+ }
+}
diff --git a/tests/Fixtures/factories/PersonFactory-8.php b/tests/Fixtures/factories/PersonFactory-8.php
new file mode 100644
index 0000000..6dbd10f
--- /dev/null
+++ b/tests/Fixtures/factories/PersonFactory-8.php
@@ -0,0 +1,20 @@
+
+ */
+class PersonFactory extends Factory
+{
+ /**
+ * Define the model's default state.
+ *
+ * @return array
+ */
+ public function definition()
+ {
+ return [
+ //
+ ];
+ }
+}
diff --git a/tests/Fixtures/factories/PersonFactory.php b/tests/Fixtures/factories/PersonFactory.php
new file mode 100644
index 0000000..d30f005
--- /dev/null
+++ b/tests/Fixtures/factories/PersonFactory.php
@@ -0,0 +1,23 @@
+
+ */
+class PersonFactory extends Factory
+{
+ /**
+ * Define the model's default state.
+ *
+ * @return array
+ */
+ public function definition(): array
+ {
+ return [
+ //
+ ];
+ }
+}
diff --git a/tests/Fixtures/factories/PostFactory-8.php b/tests/Fixtures/factories/PostFactory-8.php
new file mode 100644
index 0000000..41d19eb
--- /dev/null
+++ b/tests/Fixtures/factories/PostFactory-8.php
@@ -0,0 +1,20 @@
+
+ */
+class PostFactory extends Factory
+{
+ /**
+ * Define the model's default state.
+ *
+ * @return array
+ */
+ public function definition()
+ {
+ return [
+ //
+ ];
+ }
+}
diff --git a/tests/Fixtures/factories/PostFactory.php b/tests/Fixtures/factories/PostFactory.php
new file mode 100644
index 0000000..953b2d4
--- /dev/null
+++ b/tests/Fixtures/factories/PostFactory.php
@@ -0,0 +1,23 @@
+
+ */
+class PostFactory extends Factory
+{
+ /**
+ * Define the model's default state.
+ *
+ * @return array
+ */
+ public function definition(): array
+ {
+ return [
+ //
+ ];
+ }
+}
diff --git a/tests/Fixtures/factories/TalkFactory-8.php b/tests/Fixtures/factories/TalkFactory-8.php
new file mode 100644
index 0000000..17b4bff
--- /dev/null
+++ b/tests/Fixtures/factories/TalkFactory-8.php
@@ -0,0 +1,20 @@
+
+ */
+class TalkFactory extends Factory
+{
+ /**
+ * Define the model's default state.
+ *
+ * @return array
+ */
+ public function definition()
+ {
+ return [
+ //
+ ];
+ }
+}
diff --git a/tests/Fixtures/factories/TalkFactory.php b/tests/Fixtures/factories/TalkFactory.php
new file mode 100644
index 0000000..c011616
--- /dev/null
+++ b/tests/Fixtures/factories/TalkFactory.php
@@ -0,0 +1,23 @@
+
+ */
+class TalkFactory extends Factory
+{
+ /**
+ * Define the model's default state.
+ *
+ * @return array
+ */
+ public function definition(): array
+ {
+ return [
+ //
+ ];
+ }
+}
diff --git a/tests/Maker/MakeFactoryTest.php b/tests/Maker/MakeFactoryTest.php
new file mode 100644
index 0000000..e89cf0c
--- /dev/null
+++ b/tests/Maker/MakeFactoryTest.php
@@ -0,0 +1,95 @@
+ 'make:factory'];
+ private $fileManager;
+ private $generator;
+
+ protected function setUp(): void
+ {
+ $this->fileManager = \Mockery::spy(FileManager::class);
+ $this->maker = new MakeFactory($this->fileManager);
+ $this->generator = new Generator($this->fileManager, 'App');
+ }
+
+ /**
+ * @test
+ * @dataProvider providePostFactoryNames
+ */
+ public function it_creates_factories($name)
+ {
+ $this->expectFactory('PostFactory');
+
+ $this->callGenerate(['name' => $name]);
+ }
+
+ public function providePostFactoryNames()
+ {
+ yield ['Post'];
+ yield ['PostFactory'];
+ yield ['\App\Factory\PostFactory'];
+ }
+
+ /**
+ * @test
+ */
+ public function it_accepts_model_fqcn()
+ {
+ $this->expectFactory('PersonFactory');
+
+ $this->callGenerate(['name' => 'PersonFactory', '--model' => 'Person']);
+ }
+
+ /**
+ * @test
+ * @dataProvider provideFactoryNames
+ */
+ public function it_guesses_model_fqcn($name)
+ {
+ if (!class_exists('App\Model\Talk')) {
+ eval('namespace App\Model { class Talk {} }');
+ }
+
+ $this->expectFactory('TalkFactory');
+
+ $this->callGenerate(['name' => $name]);
+ }
+
+ public function provideFactoryNames()
+ {
+ yield ['Talk'];
+ yield ['TalkFactory'];
+ yield ['\App\Factory\TalkFactory'];
+ }
+
+ private function expectFactory(string $name)
+ {
+ $fixturePath = __DIR__.'/../Fixtures/factories/'.$name;
+ // BC with Laravel <10
+ if (!trait_exists(WithoutModelEvents::class)) {
+ $fixturePath .= '-8';
+ } elseif (!class_exists(Json::class)) {
+ $fixturePath .= '-9';
+ }
+ $fixturePath .= '.php';
+ $normalizedExpected = preg_replace('/\R/', "\n", file_get_contents($fixturePath));
+
+ $path = '/app/src/Factory/'.$name.'.php';
+ $this->fileManager->allows()->getRelativePathForFutureClass()->with('App\\Factory\\'.$name)->andReturn($path);
+ $this->fileManager->expects()->dumpFile()->once()->with($path, $normalizedExpected);
+ }
+}