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