diff --git a/config/entity-generator.php b/config/entity-generator.php index bff879b1..6d498c76 100644 --- a/config/entity-generator.php +++ b/config/entity-generator.php @@ -16,7 +16,7 @@ 'repositories' => 'app/Repositories', 'tests' => 'tests', 'routes' => 'routes/api.php', - 'factory' => 'database/factories/ModelFactory.php', + 'factories' => 'database/factories', 'translations' => 'resources/lang/en/validation.php', 'resources' => 'app/Http/Resources', 'nova' => 'app/Nova', @@ -32,8 +32,6 @@ 'routes' => 'entity-generator::routes', 'use_routes' => 'entity-generator::use_routes', 'factory' => 'entity-generator::factory', - 'legacy_factory' => 'entity-generator::legacy_factory', - 'legacy_empty_factory' => 'entity-generator::legacy_empty_factory', 'seeder' => 'entity-generator::seeder', 'legacy_seeder' => 'entity-generator::legacy_seeder', 'database_empty_seeder' => 'entity-generator::database_empty_seeder', diff --git a/src/Commands/MakeEntityCommand.php b/src/Commands/MakeEntityCommand.php index 7790c263..f300ff5a 100644 --- a/src/Commands/MakeEntityCommand.php +++ b/src/Commands/MakeEntityCommand.php @@ -2,7 +2,6 @@ namespace RonasIT\Support\Commands; -use Closure; use Illuminate\Console\Command; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Config; @@ -217,10 +216,9 @@ protected function outputNewConfig($packageConfigs, $projectConfigs) } $factories = 'database/factories'; - $factories = (version_compare(app()->version(), '8', '>=')) ? $factories : "{$factories}/ModelFactory.php"; - if ($newConfig['paths.factory'] !== $factories) { - $newConfig['paths.factory'] = $factories; + if ($newConfig['paths.factories'] !== $factories) { + $newConfig['paths.factories'] = $factories; } $differences = array_diff_key($newConfig, $flattenedProjectConfigs); diff --git a/src/Exceptions/FakerMethodNotFoundException.php b/src/Exceptions/FakerMethodNotFoundException.php new file mode 100644 index 00000000..c652e8a3 --- /dev/null +++ b/src/Exceptions/FakerMethodNotFoundException.php @@ -0,0 +1,9 @@ +buildRelationsTree($arrayModels)); } - protected function isFactoryExists($modelName): bool + protected function isFactoryExists(string $modelName): bool { - $factory = app(LegacyFactories::class); $modelClass = $this->getModelClass($modelName); - $isNewStyleFactoryExists = $this->classExists('factory', "{$modelName}Factory") && method_exists($modelClass, 'factory'); - - return $isNewStyleFactoryExists || !empty($factory[$this->getModelClass($modelName)]); + return $this->classExists('factories', "{$modelName}Factory") && method_exists($modelClass, 'factory'); } protected function isMethodExists($modelName, $method): bool @@ -170,13 +167,14 @@ protected function getModelFields($model): array protected function getMockModel($model): array { - if (!$this->isFactoryExists($model)) { + $hasFactory = $this->isFactoryExists($model); + + if (!$hasFactory) { return []; } $modelClass = $this->getModelClass($model); - $hasFactory = method_exists($modelClass, 'factory') && $this->classExists('factory', "{$model}Factory"); - $factory = ($hasFactory) ? $modelClass::factory() : factory($modelClass); + $factory = $modelClass::factory(); return $factory ->make() diff --git a/src/Generators/FactoryGenerator.php b/src/Generators/FactoryGenerator.php index 5ac02bb7..59ec4c3d 100644 --- a/src/Generators/FactoryGenerator.php +++ b/src/Generators/FactoryGenerator.php @@ -5,12 +5,11 @@ use Faker\Generator as Faker; use Illuminate\Support\Arr; use Illuminate\Support\Str; -use RonasIT\Support\Exceptions\ModelFactoryNotFound; +use InvalidArgumentException; +use RonasIT\Support\Exceptions\FakerMethodNotFoundException; use RonasIT\Support\Exceptions\ClassNotExistsException; -use RonasIT\Support\Exceptions\ModelFactoryNotFoundedException; use RonasIT\Support\Exceptions\ClassAlreadyExistsException; use RonasIT\Support\Events\SuccessCreateMessage; -use Exception; class FactoryGenerator extends EntityGenerator { @@ -26,109 +25,33 @@ class FactoryGenerator extends EntityGenerator 'json' => '[]', ]; - protected function generateSeparateClass(): string + public function generate(): void { if (!$this->classExists('models', $this->model)) { $this->throwFailureException( - ClassNotExistsException::class, - "Cannot create {$this->model}Factory cause {$this->model} Model does not exists.", - "Create a {$this->model} Model by himself or run command 'php artisan make:entity {$this->model} --only-model'." + exceptionClass: ClassNotExistsException::class, + failureMessage: "Cannot create {$this->model}Factory cause {$this->model} Model does not exists.", + recommendedMessage: "Create a {$this->model} Model by itself or run command 'php artisan make:entity {$this->model} --only-model'.", ); } - if ($this->classExists('factory', "{$this->model}Factory")) { + if ($this->classExists('factories', "{$this->model}Factory")) { $this->throwFailureException( - ClassAlreadyExistsException::class, - "Cannot create {$this->model}Factory cause {$this->model}Factory already exists.", - "Remove {$this->model}Factory." + exceptionClass: ClassAlreadyExistsException::class, + failureMessage: "Cannot create {$this->model}Factory cause {$this->model}Factory already exists.", + recommendedMessage: "Remove {$this->model}Factory.", ); } $factoryContent = $this->getStub('factory', [ - 'namespace' => $this->getOrCreateNamespace('factory'), + 'namespace' => $this->getOrCreateNamespace('factories'), 'entity' => $this->model, - 'fields' => $this->prepareFields() + 'fields' => $this->prepareFields(), ]); - $this->saveClass('factory', "{$this->model}Factory", $factoryContent); - - return "Created a new Factory: {$this->model}Factory"; - } - - protected function generateToGenericClass(): string - { - if (!file_exists($this->paths['factory'])) { - $this->prepareEmptyFactory(); - } - - if (!$this->checkExistModelFactory() && $this->checkExistRelatedModelsFactories()) { - $stubPath = config("entity-generator.stubs.legacy_factory"); - - $content = view($stubPath)->with([ - 'entity' => $this->model, - 'fields' => $this->prepareFields(), - 'modelsNamespace' => $this->getOrCreateNamespace('models') - ])->render(); - - $content = "\n\n" . $content; - - $createMessage = "Created a new Test factory for {$this->model} model in '{$this->paths['factory']}'"; - - file_put_contents($this->paths['factory'], $content, FILE_APPEND); - - $this->prepareRelatedFactories(); - } else { - $createMessage = "Factory for {$this->model} model has already created, so new factory not necessary create."; - } - - return $createMessage; - } + $this->saveClass('factories', "{$this->model}Factory", $factoryContent); - public function generate(): void - { - $createMessage = (version_compare(app()->version(), '8', '>=')) - ? $this->generateSeparateClass() - : $this->generateToGenericClass(); - - event(new SuccessCreateMessage($createMessage)); - } - - protected function prepareEmptyFactory(): void - { - $stubPath = config('entity-generator.stubs.legacy_empty_factory'); - $content = " $this->getOrCreateNamespace('models') - ])->render(); - - list($basePath, $databaseFactoryDir) = extract_last_part(config('entity-generator.paths.factory'), '/'); - - if (!is_dir($databaseFactoryDir)) { - mkdir($databaseFactoryDir); - } - - file_put_contents($this->paths['factory'], $content); - } - - protected function checkExistRelatedModelsFactories(): bool - { - $modelFactoryContent = file_get_contents($this->paths['factory']); - $relatedModels = $this->getRelatedModels($this->model, "{$this->model}Factory"); - $modelNamespace = $this->getOrCreateNamespace('models'); - - foreach ($relatedModels as $relatedModel) { - $relatedFactoryClass = "{$modelNamespace}\\$relatedModel::class"; - $existModelFactory = strpos($modelFactoryContent, $relatedFactoryClass); - - if (!$existModelFactory) { - $this->throwFailureException( - ModelFactoryNotFoundedException::class, - "Not found $relatedModel factory for $relatedModel model in '{$this->paths['factory']}", - "Please declare a factory for $relatedModel model on '{$this->paths['factory']}' path and run your command with option '--only-tests'." - ); - } - } - - return true; + event(new SuccessCreateMessage("Created a new Factory: {$this->model}Factory")); } protected static function getFakerMethod($field): string @@ -146,42 +69,10 @@ protected static function getCustomMethod($field): string return self::CUSTOM_METHODS[$field['type']]; } - $message = $field['type'] . 'not found in CUSTOM_METHODS variable CUSTOM_METHODS = ' . self::CUSTOM_METHODS; - throw new Exception($message); - } - - protected function prepareRelatedFactories(): void - { - $relations = array_merge( - $this->relations['hasOne'], - $this->relations['hasMany'] - ); - - foreach ($relations as $relation) { - $modelFactoryContent = file_get_contents($this->paths['factory']); - - if (!Str::contains($modelFactoryContent, $this->getModelClass($relation))) { - $this->throwFailureException( - ModelFactoryNotFound::class, - "Model factory for mode {$relation} not found.", - "Please create it and after thar you can run this command with flag '--only-tests'." - ); - } - - $matches = []; - - preg_match($this->getFactoryPattern($relation), $modelFactoryContent, $matches); - - foreach ($matches as $match) { - $field = Str::snake($this->model) . '_id'; - - $newField = "\n \"{$field}\" => 1,"; + $message = "Cannot generate fake data for unsupported {$field['type']} field type. " + . "Supported custom field types are " . implode(', ', array_keys(self::CUSTOM_METHODS)); - $modelFactoryContent = str_replace($match, $match . $newField, $modelFactoryContent); - } - - file_put_contents($this->paths['factory'], $modelFactoryContent); - } + throw new FakerMethodNotFoundException($message); } public static function getFactoryFieldsContent($field): string @@ -193,49 +84,33 @@ public static function getFactoryFieldsContent($field): string return 1; } - if (property_exists($faker, $field['name'])) { - return "\$faker-\>{$field['name']}"; + try { + $faker->{$field['name']}; + $hasFormatter = true; + } catch (InvalidArgumentException $e) { + $hasFormatter = false; } - if (method_exists($faker, $field['name'])) { - return "\$faker-\>{$field['name']}()"; + if ($hasFormatter) { + return "\$faker->{$field['name']}"; } return self::getFakerMethod($field); } - protected function checkExistModelFactory(): int - { - $modelFactoryContent = file_get_contents($this->paths['factory']); - $modelNamespace = $this->getOrCreateNamespace('models'); - $factoryClass = "{$modelNamespace}\\$this->model::class"; - - return strpos($modelFactoryContent, $factoryClass); - } - protected function prepareFields(): array { $result = []; foreach ($this->fields as $type => $fields) { foreach ($fields as $field) { - $explodedType = explode('-', $type); - $result[] = [ 'name' => $field, - 'type' => head($explodedType) + 'type' => Str::before($type, '-'), ]; } } return $result; } - - protected function getFactoryPattern($model): string - { - $modelNamespace = "App\\\\Models\\\\" . $model; - $return = "return \\["; - - return "/{$modelNamespace}.*{$return}/sU"; - } } diff --git a/stubs/factory.blade.php b/stubs/factory.blade.php index 2511cd69..bedba559 100644 --- a/stubs/factory.blade.php +++ b/stubs/factory.blade.php @@ -17,4 +17,4 @@ public function definition(): array @endforeach ]; } -} \ No newline at end of file +} diff --git a/stubs/legacy_empty_factory.blade.php b/stubs/legacy_empty_factory.blade.php deleted file mode 100644 index af39da4e..00000000 --- a/stubs/legacy_empty_factory.blade.php +++ /dev/null @@ -1,30 +0,0 @@ -use Illuminate\Support\Str; - -/* -|-------------------------------------------------------------------------- -| Model Factories -|-------------------------------------------------------------------------- -| -| Here you may define all of your model factories. Model factories give -| you a convenient way to create models for testing and seeding your -| database. Just tell the factory how a default model should look. -| -*/ - -/** @var \Illuminate\Database\Eloquent\Factory $factory */ -$factory->define({{$modelsNamespace}}\User::class, function (Faker\Generator $faker) { - static $password; - - return [ - 'name' => $faker->name, - 'email' => $faker->unique()->safeEmail, - 'password' => $password ?: $password = bcrypt('secret'), - 'remember_token' => Str::random(10), - ]; -}); - -$factory->define({{$modelsNamespace}}\Role::class, function () { - return [ - 'name' => 'user' - ]; -}); diff --git a/stubs/legacy_factory.blade.php b/stubs/legacy_factory.blade.php deleted file mode 100644 index 7756e2e0..00000000 --- a/stubs/legacy_factory.blade.php +++ /dev/null @@ -1,7 +0,0 @@ -$factory->define({{$modelsNamespace}}\{{$entity}}::class, function (Faker\Generator $faker) { - return [ -@foreach($fields as $field) - '{{$field['name']}}' => {!! \RonasIT\Support\Generators\FactoryGenerator::getFactoryFieldsContent($field) !!}, -@endforeach - ]; -}); \ No newline at end of file diff --git a/tests/ControllerGeneratorTest.php b/tests/ControllerGeneratorTest.php index a551dcf5..6b887ad1 100644 --- a/tests/ControllerGeneratorTest.php +++ b/tests/ControllerGeneratorTest.php @@ -24,10 +24,10 @@ public function setUp(): void public function testControllerAlreadyExists() { $this->mockClass(ControllerGenerator::class, [ - $this->classExistsMethodCall(['controllers', 'PostController']) + $this->classExistsMethodCall(['controllers', 'PostController']), ]); - $this->assertExceptionThrowed( + $this->assertExceptionThrew( className: ClassAlreadyExistsException::class, message: 'Cannot create PostController cause PostController already exists. Remove PostController.', ); @@ -41,10 +41,10 @@ public function testModelServiceNotExists() { $this->mockClass(ControllerGenerator::class, [ $this->classExistsMethodCall(['controllers', 'PostController'], false), - $this->classExistsMethodCall(['services', 'PostService'], false) + $this->classExistsMethodCall(['services', 'PostService'], false), ]); - $this->assertExceptionThrowed( + $this->assertExceptionThrew( className: ClassNotExistsException::class, message: 'Cannot create PostService cause PostService does not exists. Create a PostService by himself.', ); @@ -58,7 +58,7 @@ public function testRouteFileNotExists() { $this->mockFilesystemWithoutRoutesFile(); - $this->assertExceptionThrowed( + $this->assertExceptionThrew( className: FileNotFoundException::class, message: "Not found file with routes. Create a routes file on path: 'vfs://root/routes/api.php'", ); diff --git a/tests/FactoryGeneratorTest.php b/tests/FactoryGeneratorTest.php new file mode 100644 index 00000000..0d103dd9 --- /dev/null +++ b/tests/FactoryGeneratorTest.php @@ -0,0 +1,109 @@ +assertExceptionThrew( + className: ClassNotExistsException::class, + message: "Cannot create PostFactory cause Post Model does not exists. " + . "Create a Post Model by itself or run command 'php artisan make:entity Post --only-model'.", + ); + + app(FactoryGenerator::class) + ->setModel('Post') + ->generate(); + + Event::assertNothingDispatched(); + } + + public function testFactoryClassExists() + { + $this->assertExceptionThrew( + className: ClassAlreadyExistsException::class, + message: "Cannot create PostFactory cause PostFactory already exists. Remove PostFactory.", + ); + + $this->mockFactoryGenerator( + $this->classExistsMethodCall(['models', 'Post']), + $this->classExistsMethodCall(['factories', 'PostFactory']), + ); + + app(FactoryGenerator::class) + ->setModel('Post') + ->generate(); + + Event::assertNothingDispatched(); + } + + public function testProcessUnknownFieldType() + { + $this->mockConfigurations(); + $this->mockFilesystem(); + + $this->assertExceptionThrew( + className: ViewException::class, + message: "Cannot generate fake data for unsupported another_type field type. " + . "Supported custom field types are json", + ); + + app(FactoryGenerator::class) + ->setFields([ + 'another_type' => ['some_field'], + ]) + ->setRelations([ + 'hasOne' => [], + 'hasMany' => [], + 'belongsTo' => [], + ]) + ->setModel('Post') + ->generate(); + + } + + public function testCreateSuccess() + { + $this->mockConfigurations(); + $this->mockFilesystem(); + + app(FactoryGenerator::class) + ->setFields([ + 'integer-required' => ['author_id'], + 'string' => ['title', 'iban', 'something'], + 'json' => ['json_text'], + ]) + ->setRelations([ + 'hasOne' => ['user'], + 'hasMany' => [], + 'belongsTo' => ['user'], + ]) + ->setModel('Post') + ->generate(); + + $this->assertGeneratedFileEquals('post_factory.php', '/database/factories/PostFactory.php'); + + $this->assertEventPushed( + className: SuccessCreateMessage::class, + message: 'Created a new Factory: PostFactory', + ); + } +} diff --git a/tests/NovaResourceGeneratorTest.php b/tests/NovaResourceGeneratorTest.php index 95be8b76..292b0c69 100644 --- a/tests/NovaResourceGeneratorTest.php +++ b/tests/NovaResourceGeneratorTest.php @@ -38,7 +38,7 @@ public function testCreateNovaResourceWithMissingModel() { $this->mockNovaServiceProviderExists(); - $this->assertExceptionThrowed( + $this->assertExceptionThrew( className: ClassNotExistsException::class, message: 'Cannot create Post Nova resource cause Post Model does not exists. ' . "Create a Post Model by himself or run command 'php artisan make:entity Post --only-model'" @@ -58,7 +58,7 @@ public function testCreateNovaTestAlreadyExists() $this->classExistsMethodCall(['nova', 'PostResource']), ]); - $this->assertExceptionThrowed( + $this->assertExceptionThrew( className: ClassAlreadyExistsException::class, message: 'Cannot create PostResource cause PostResource already exists. Remove PostResource.', ); diff --git a/tests/NovaTestGeneratorTest.php b/tests/NovaTestGeneratorTest.php index c1ebeccc..afbb9441 100644 --- a/tests/NovaTestGeneratorTest.php +++ b/tests/NovaTestGeneratorTest.php @@ -26,7 +26,7 @@ public function testGenerateResourceNotExists() $this->classExistsMethodCall(['nova', 'Post'], false), ]); - $this->assertExceptionThrowed( + $this->assertExceptionThrew( className: ClassNotExistsException::class, message: 'Cannot create NovaPostTest cause Post Nova resource does not exist. Create Post Nova resource.', ); @@ -45,7 +45,7 @@ public function testGenerateNovaTestAlreadyExists() $this->classExistsMethodCall(['nova', 'NovaPostTest']) ]); - $this->assertExceptionThrowed( + $this->assertExceptionThrew( className: ClassAlreadyExistsException::class, message: "Cannot create NovaPostTest cause it's already exist. Remove NovaPostTest.", ); diff --git a/tests/Support/Factory/FactoryMockTrait.php b/tests/Support/Factory/FactoryMockTrait.php new file mode 100644 index 00000000..60a4b920 --- /dev/null +++ b/tests/Support/Factory/FactoryMockTrait.php @@ -0,0 +1,45 @@ +mockClass(FactoryGenerator::class, $functionCalls); + } + + public function mockConfigurations(): void + { + config([ + 'entity-generator.paths' => [ + 'models' => 'app/Models', + 'factories' => 'database/factories', + ], + ]); + } + + public function mockFilesystem(): void + { + $structure = [ + 'app' => [ + 'Models' => [ + 'Post.php' => $this->mockPhpFileContent(), + 'User.php' => $this->mockPhpFileContent(), + ], + ], + 'database' => [ + 'factories' => [], + ], + ]; + + vfsStream::create($structure); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 041c719a..2c2144ca 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -102,7 +102,7 @@ protected function assertEventPushedChain(array $events): void } } - protected function assertExceptionThrowed(string $className, string $message): void + protected function assertExceptionThrew(string $className, string $message): void { $this->expectException($className); $this->expectExceptionMessage($message); diff --git a/tests/fixtures/FactoryGeneratorTest/post_factory.php b/tests/fixtures/FactoryGeneratorTest/post_factory.php new file mode 100644 index 00000000..0b23874f --- /dev/null +++ b/tests/fixtures/FactoryGeneratorTest/post_factory.php @@ -0,0 +1,23 @@ + 1, + 'user_id' => 1, + 'title' => $faker->title, + 'iban' => $faker->iban, + 'something' => $faker->word, + 'json_text' => [], + ]; + } +}