# HG changeset patch # User Luka Sitas # Date 1794536393 18000 # Node ID c7fd313030248e70a8578f1a4543903c5083fa5c # Parent f65ab84ee47f4bcf95431f88b7546665fc3def16# Parent cde7e0f145fab02508d6c04e56d3e3702b8c7148 merging down to default diff -r f65ab84ee47f -r c7fd31303024 .vimrc --- a/.vimrc Wed Sep 10 21:00:47 2025 -0400 +++ b/.vimrc Thu Nov 12 21:19:53 2026 -0500 @@ -11,6 +11,11 @@ nnoremap :ALECodeAction set mouse=a +function! GeneratePhpCtags() + let command = 'ctags -R --fields=+aimS --php-kinds=cdfint --languages=php --extras=+q --tag-relative=yes --exclude=".git" --exclude=".hg" --exclude="node_modules" --exclude="composer.phar" --totals=yes' + let output = system(command) +endfunction + function! FixPhpFiles() " Save the current cursor position let save_cursor = getpos(".") diff -r f65ab84ee47f -r c7fd31303024 src/Generator/Controller/stubs/controller.stub --- a/src/Generator/Controller/stubs/controller.stub Wed Sep 10 21:00:47 2025 -0400 +++ b/src/Generator/Controller/stubs/controller.stub Thu Nov 12 21:19:53 2026 -0500 @@ -16,20 +16,19 @@ */ public function index({{ filterRequest }} $request) { - $validated = $request->validated(); + $validated = $request->validated(); $data = []; - $data['items'] = {{ model }}::get_data($validated); - $data = array_merge($data, {{ model }}::load_index()); + $data = array_merge($data, {{ model }}::load_index()); return view('{{ tableName }}.index', $data); } - public function get_data() + public function get_data({{ filterRequest }} $request) { + $validated = $request->validated() ?? []; $data = []; - $data['records'] = {{ model }}::get_data([]); - $data['total_records'] = count($data['records']); + $data = array_merge($data, {{ model }}::get_data($validated)); return $data; } diff -r f65ab84ee47f -r c7fd31303024 src/Generator/Factory/FactoryGenerator.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/Factory/FactoryGenerator.php Thu Nov 12 21:19:53 2026 -0500 @@ -0,0 +1,158 @@ +resolveStubPath('/stubs/factory.stub'); + } + + /** + * Resolve the fully-qualified path to the stub. + * + * @param string $stub + * @return string + */ + protected function resolveStubPath($stub) + { + return is_file($customPath = $this->laravel->basePath(trim($stub, '/'))) + ? $customPath + : __DIR__.$stub; + } + + protected function getClassName($name) + { + return $this->factory_name($name); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getPath($name = null) + { + return str_replace(['App\\', '\\'], ['app/', '/'], $this->getFactoryNamespace().'/'.$this->factory_name($this->getTableInput()).'.php'); + } + + protected function gatherRelations() + { + $relations = RelationshipNavigator::getRelations($this->getCurrentTable()); + foreach ($relations as $relation_type => $relation_values) { + $relations[$relation_type] = array_column($relation_values, null, 'column'); + } + + return $relations; + } + + protected function renderColumns() + { + $values = []; + $relations = $this->gatherRelations(); + + foreach ($this->get_columns() as $column) { + if (in_array($column['name'], $this->columns_to_ignore)) { + continue; + } + + $value = $this->getFakeValue($column, $relations); + $snippet = $this->getSnippet('value'); + + // Replace placeholders with actual values + $values[] = str_replace(['{{value}}', '{{column_name}}'], [$value, $column['name']], $snippet); + } + + return implode("\n", $values); + } + + protected function getFakeValue($column, $relations) + { + $value = '$this->faker'; + $nullable = ($column['nullable'] ? '->optional($weight = 0.5)' : ''); + + switch ($column['type_name']) { + case 'date': + return "$value${nullable}->date()"; + case 'timestamp': + return "$value${nullable}->timestamp()"; + case 'tinyint': + return "$value${nullable}->numberBetween(0,1)"; + case 'bigint': + if (isset($relations['belongsTo'][$column['name']])) { + $related = $this->getNamespacedModel($relations['belongsTo'][$column['name']]['table']); + return "$related::factory()"; + } + // fall through to int for lack of better option + case 'int': + return "$value${nullable}->randomNumber()"; + case 'float': + return "$value${nullable}->randomFloat()"; + default: + return "$value${nullable}->text()"; + } + } + + /** + * Get available insertions including model relationships. + */ + public function get_available_inserts(): array + { + // Merge parent insertions (attributes, fillable, etc.) + $inserts = parent::get_available_inserts(); + + // Gather and render relationships for this model + $columns = $this->renderColumns(); + + // Assign to stub placeholders + $inserts['# {{ factoryInsertPoint }}'] = $columns; + + return $inserts; + } +} diff -r f65ab84ee47f -r c7fd31303024 src/Generator/Factory/snippets/value.stub --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/Factory/snippets/value.stub Thu Nov 12 21:19:53 2026 -0500 @@ -0,0 +1,1 @@ +'{{column_name}}' => {{value}}, diff -r f65ab84ee47f -r c7fd31303024 src/Generator/Factory/stubs/factory.stub --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/Factory/stubs/factory.stub Thu Nov 12 21:19:53 2026 -0500 @@ -0,0 +1,35 @@ + + */ +class {{ class }} extends Factory +{ + + /** + * The name of the factory's corresponding model. + * + * @var class-string<\Illuminate\Database\Eloquent\Model> + */ + protected $model = {{ model }}::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + # {{ factoryInsertPoint }} + ]; + } + +} diff -r f65ab84ee47f -r c7fd31303024 src/Generator/Generator.php --- a/src/Generator/Generator.php Wed Sep 10 21:00:47 2025 -0400 +++ b/src/Generator/Generator.php Thu Nov 12 21:19:53 2026 -0500 @@ -41,7 +41,7 @@ /* dd($this->getCurrentTable()->getForeignKeys()); */ if ($this->option('all')) { - /* $this->input->setOption('factory', true); */ + $this->input->setOption('factory', true); /* $this->input->setOption('seed', true); */ /* $this->input->setOption('migration', true); */ $this->input->setOption('controller', true); @@ -49,11 +49,12 @@ $this->input->setOption('request', true); $this->input->setOption('view', true); $this->input->setOption('route', true); + $this->input->setOption('test', true); } - /* if ($this->option('factory')) { */ - /* $this->createFactory(); */ - /* } */ + if ($this->option('factory')) { + $this->createFactory(); + } /* if ($this->option('migration')) { */ /* $this->createMigration(); */ @@ -82,6 +83,10 @@ if ($this->option('route')) { $this->createRoute(); } + + if ($this->option('test')) { + $this->createTest(); + } } /** @@ -96,6 +101,8 @@ ['request', 'r', InputOption::VALUE_NONE, 'Generate base request classes for the table.'], ['view', '', InputOption::VALUE_NONE, 'Generate base views for the table.'], ['route', 'w', InputOption::VALUE_NONE, 'Generate base routes classes for the table.'], + ['factory', 'F', InputOption::VALUE_NONE, 'Generate base factory classes for the table.'], + ['test', 't', InputOption::VALUE_NONE, 'Generate base test classes for the table.'], ]); } @@ -130,4 +137,14 @@ { $this->call('mf:route', ['table' => $this->getTableInput(), '--fresh' => $this->option('fresh')]); } + + protected function createFactory() + { + $this->call('mf:factory', ['table' => $this->getTableInput(), '--fresh' => $this->option('fresh')]); + } + + protected function createTest() + { + $this->call('mf:test', ['table' => $this->getTableInput(), '--fresh' => $this->option('fresh')]); + } } diff -r f65ab84ee47f -r c7fd31303024 src/Generator/Model/stubs/model.stub --- a/src/Generator/Model/stubs/model.stub Wed Sep 10 21:00:47 2025 -0400 +++ b/src/Generator/Model/stubs/model.stub Thu Nov 12 21:19:53 2026 -0500 @@ -2,12 +2,14 @@ namespace {{ namespace }}; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; use Wizard\Framework\Models\BaseModel; class {{ class }} extends BaseModel { - //use HasFactory; + /** @use HasFactory<\Database\Factories\{{ class }}Factory> */ + use HasFactory; use SoftDeletes; /** @@ -38,6 +40,13 @@ # {{ defaultRelationsInsertPoint }} ]; + public static function named_joins() { + $joins = [ + // + ]; + return parent::named_joins(); + } + protected static $filters = [ # {{ defaultFiltersInsertPoint }} ]; diff -r f65ab84ee47f -r c7fd31303024 src/Generator/Requests/FilterRequestGenerator.php --- a/src/Generator/Requests/FilterRequestGenerator.php Wed Sep 10 21:00:47 2025 -0400 +++ b/src/Generator/Requests/FilterRequestGenerator.php Thu Nov 12 21:19:53 2026 -0500 @@ -44,7 +44,7 @@ */ protected function getStub() { - return $this->resolveStubPath('/stubs/request.stub'); + return $this->resolveStubPath('/stubs/filter_request.stub'); } /** @@ -74,4 +74,19 @@ { return str_replace(['App\\', '\\'], ['app/', '/'], $this->getRequestNamespace($this->getTableInput()).'/'.$this->filter_request_name($this->getTableInput()).'.php'); } + + /** + * Get available insertions including model relationships. + * + * @return array + */ + public function get_available_inserts(): array + { + // Merge parent insertions (attributes, fillable, etc.) + $inserts = parent::get_available_inserts(); + + $inserts['// {{ valuesForValidation }}'] = str_replace("'required'", "'nullable'", $inserts['// {{ valuesForValidation }}']); + + return $inserts; + } } diff -r f65ab84ee47f -r c7fd31303024 src/Generator/Requests/RequestGenerator.php --- a/src/Generator/Requests/RequestGenerator.php Wed Sep 10 21:00:47 2025 -0400 +++ b/src/Generator/Requests/RequestGenerator.php Thu Nov 12 21:19:53 2026 -0500 @@ -90,16 +90,16 @@ protected function createFilterRequest() { - $this->call('mf:filter_request', ['table' => $this->getTableInput()]); + $this->call('mf:filter_request', ['table' => $this->getTableInput(), '--fresh' => $this->option('fresh')]); } protected function createStoreRequest() { - $this->call('mf:store_request', ['table' => $this->getTableInput()]); + $this->call('mf:store_request', ['table' => $this->getTableInput(), '--fresh' => $this->option('fresh')]); } protected function createUpdateRequest() { - $this->call('mf:update_request', ['table' => $this->getTableInput()]); + $this->call('mf:update_request', ['table' => $this->getTableInput(), '--fresh' => $this->option('fresh')]); } } diff -r f65ab84ee47f -r c7fd31303024 src/Generator/Requests/stubs/filter_request.stub --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/Requests/stubs/filter_request.stub Thu Nov 12 21:19:53 2026 -0500 @@ -0,0 +1,33 @@ + + */ + public function rules(): array + { + return [ + 'filters' => [ + // {{ valuesForValidation }} + ], + 'page' => ['nullable', 'int', 'min:0'], + 'page_size' => ['nullable', 'int', 'min:0'], + 'sort' => ['nullable', 'array'], + ]; + } +} diff -r f65ab84ee47f -r c7fd31303024 src/Generator/Test/TestGenerator.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/Test/TestGenerator.php Thu Nov 12 21:19:53 2026 -0500 @@ -0,0 +1,79 @@ +resolveStubPath('/stubs/test.stub'); + } + + /** + * Resolve the fully-qualified path to the stub. + * + * @param string $stub + * @return string + */ + protected function resolveStubPath($stub) + { + return is_file($customPath = $this->laravel->basePath(trim($stub, '/'))) + ? $customPath + : __DIR__.$stub; + } + + protected function getClassName($name) + { + return $this->test_name($name); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getPath($name = null) + { + return str_replace(['App\\', '\\'], ['app/', '/'], $this->getTestNamespace() . '/' . $this->test_name($this->getTableInput()) . '.php'); + } +} diff -r f65ab84ee47f -r c7fd31303024 src/Generator/Test/stubs/test.stub --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/Test/stubs/test.stub Thu Nov 12 21:19:53 2026 -0500 @@ -0,0 +1,128 @@ +get(route('{{ tableName }}.index')); + + $response->assertStatus(200); + } + + public function test_{{ modelVariable }}_crud_get_data_access(): void + { + $response = $this + ->post(route('{{ tableName }}.get_data')); + + $response->assertStatus(200); + } + + public function test_{{ modelVariable }}_crud_create_access(): void + { + $response = $this + ->get(route('{{ tableName }}.create')); + + $response->assertStatus(200); + } + + public function test_{{ modelVariable }}_crud_edit_access(): void + { + $item = {{ model }}::factory()->create(); + + $response = $this + ->get(route('{{ tableName }}.edit', $item)); + + $response->assertStatus(200); + } + + public function test_{{ modelVariable }}_crud_create_usage(): void + { + $item = {{ model }}::factory()->make(); + + $response = $this + ->from(route('{{ tableName }}.create')) + ->post(route('{{ tableName }}.store'), + $item->toArray() + ); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('{{ tableName }}.index')); + + $this->assertDatabaseHas('{{ tableName }}', $item->toArray()); + } + + public function test_{{ modelVariable }}_crud_create_invalid_data(): void + { + $response = $this + ->from(route('{{ tableName }}.create')) + ->post(route('{{ tableName }}.store'), []); + + $response + ->assertSessionHasErrors() + ->assertRedirect(route('{{ tableName }}.create')); + + // Make sure no entry was added + $this->assertDatabaseCount('{{ tableName }}', 0); + } + + public function test_{{ modelVariable }}_crud_update_usage(): void + { + $item = {{ model }}::factory()->create(); + + $updateData = {{ model }}::factory()->make()->toArray(); + + $response = $this + ->from(route('{{ tableName }}.edit', $item)) + ->put(route('{{ tableName }}.update', $item), $updateData); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('{{ tableName }}.index')); + + $this->assertDatabaseHas('{{ tableName }}', $updateData); + } + + public function test_{{ modelVariable }}_crud_edit_invalid_data(): void + { + $item = {{ model }}::factory()->create(); + + $response = $this + ->from(route('{{ tableName }}.edit', $item)) + ->put(route('{{ tableName }}.update', $item), []); + + $response + ->assertSessionHasErrors() + ->assertRedirect(route('{{ tableName }}.edit', $item)); + + // Make sure no entry was added + $this->assertDatabaseHas('{{ tableName }}', array_intersect_key($item->toArray(), array_flip($item->getFillable()))); + } + + public function test_{{ modelVariable }}_crud_delete_usage(): void + { + $item = {{ model }}::factory()->create(); + + $response = $this + ->delete(route('{{ tableName }}.destroy', $item)); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('{{ tableName }}.index')); + + $this->assertSoftDeleted('{{ tableName }}', [ + 'id' => $item->id, + ]); + } + +} diff -r f65ab84ee47f -r c7fd31303024 src/Generator/View/stubs/index.stub --- a/src/Generator/View/stubs/index.stub Wed Sep 10 21:00:47 2025 -0400 +++ b/src/Generator/View/stubs/index.stub Thu Nov 12 21:19:53 2026 -0500 @@ -28,6 +28,7 @@ if (el) { new ServerTable(el, { endpoint: '/{{ tableName }}/get_data', + filters: Object.fromEntries(new URLSearchParams(location.search)), columns: [ {{ columnInsertPoint }} { diff -r f65ab84ee47f -r c7fd31303024 src/MagicForgerServiceProvider.php --- a/src/MagicForgerServiceProvider.php Wed Sep 10 21:00:47 2025 -0400 +++ b/src/MagicForgerServiceProvider.php Thu Nov 12 21:19:53 2026 -0500 @@ -5,12 +5,14 @@ use Illuminate\Support\ServiceProvider; use Wizard\MagicForger\Generator\Controller\ControllerGenerator; use Wizard\MagicForger\Generator\Generator; +use Wizard\MagicForger\Generator\Factory\FactoryGenerator; use Wizard\MagicForger\Generator\Model\ModelGenerator; use Wizard\MagicForger\Generator\Requests\RequestGenerator; use Wizard\MagicForger\Generator\Requests\FilterRequestGenerator; use Wizard\MagicForger\Generator\Requests\StoreRequestGenerator; use Wizard\MagicForger\Generator\Requests\UpdateRequestGenerator; use Wizard\MagicForger\Generator\Route\RouteGenerator; +use Wizard\MagicForger\Generator\Test\TestGenerator; use Wizard\MagicForger\Generator\View\ViewGenerator; use Wizard\MagicForger\Generator\View\IndexViewGenerator; use Wizard\MagicForger\Generator\View\CreateEditViewGenerator; @@ -37,6 +39,8 @@ IndexViewGenerator::class, CreateEditViewGenerator::class, ShowViewGenerator::class, + FactoryGenerator::class, + TestGenerator::class, ]); } diff -r f65ab84ee47f -r c7fd31303024 src/Replacer/Replacer.php --- a/src/Replacer/Replacer.php Wed Sep 10 21:00:47 2025 -0400 +++ b/src/Replacer/Replacer.php Thu Nov 12 21:19:53 2026 -0500 @@ -157,6 +157,22 @@ return Str::singular(Str::studly($name)); } + /** + * Generate factory name in Studly case. + */ + public function factory_name(string $name): string + { + return Str::singular(Str::studly($name)) . 'Factory'; + } + + /** + * Generate test name in Studly case. + */ + public function test_name(string $name): string + { + return Str::singular(Str::studly($name)) . 'Test'; + } + // Namespace Methods // These methods handle the formation of various namespaces used within the replacements. @@ -267,6 +283,24 @@ return $this->getViewNamespace($name) . '\\'; } + /** + * Get the factory namespace. + */ + public function getFactoryNamespace(string $name = ''): string + { + return database_path() + . DIRECTORY_SEPARATOR . 'factories' ; + } + + /** + * Get the test namespace. + */ + public function getTestNamespace(string $name = ''): string + { + return base_path() + . DIRECTORY_SEPARATOR . 'tests' + . DIRECTORY_SEPARATOR . 'CRUD' ; + } /** * Get the request uses string for replacement. diff -r f65ab84ee47f -r c7fd31303024 src/Replacer/TableReplacer.php --- a/src/Replacer/TableReplacer.php Wed Sep 10 21:00:47 2025 -0400 +++ b/src/Replacer/TableReplacer.php Thu Nov 12 21:19:53 2026 -0500 @@ -51,15 +51,15 @@ if (in_array($column['name'], $this->columns_to_ignore)) { continue; } - $type = $column['type_name']; - //date - if(in_array($type, ['date'])) { - $insert .= sprintf("'%s' => 'date:Y-m-d',", $column['name'])."\n"; - } - //time - if(in_array($type, ['timestamp'])) { - $insert .= sprintf("'%s' => 'date:Y-m-d H:i',", $column['name'])."\n"; - } + $type = $column['type_name']; + // date + if (in_array($type, ['date'])) { + $insert .= sprintf("'%s' => 'date:Y-m-d',", $column['name'])."\n"; + } + // time + if (in_array($type, ['timestamp'])) { + $insert .= sprintf("'%s' => 'date:Y-m-d H:i',", $column['name'])."\n"; + } } return $insert; @@ -104,10 +104,81 @@ { $insert = ''; foreach ($this->get_columns() as $column) { + // Don't do validation on some of the basic columns if (in_array($column['name'], $this->columns_to_ignore)) { continue; } - $insert .= sprintf("'%s' => 'nullable',", $column['name'])."\n"; + + $options = []; + + // Add default value checks if needed + if (/*is_null($column['default']) && */! $column['nullable']) { + $options[] = "'required'"; + } + else { + $options[] = "'nullable'"; + } + + // Determine the validations based on column type + $type = strtolower($column['type_name']); + $size = (preg_match('/\((\d+)\)/', $column['type'], $matches)) ? $matches[1] : null; + + switch ($type) { + case 'varchar': + case 'char': + $options[] = "'string'"; + + // Extract length if defined + if (! is_null($size)) { + $options[] = "'max:$size'"; + } + break; + + case 'text': + $options[] = "'string'"; + break; + + case 'int': + case 'tinyint': + case 'smallint': + case 'mediumint': + case 'bigint': + $options[] = "'integer'"; + if (strpos($column['type'], 'unsigned') !== false) { + $options[] = "'min:0'"; + } + if (! is_null($size)) { + $options[] = "'max_digits:".$size."'"; + } + break; + + case 'decimal': + case 'double': + case 'float': + $options[] = "'numeric'"; + // Extract precision and scale if defined + if (preg_match('/\((\d+),(\d+)\)/', $column['type'], $matches)) { + $precision = $matches[1]; + $scale = isset($matches[2]) ? ','.$matches[2] : ''; + + $options[] = "'decimal:$precision$scale'"; + } + break; + + case 'boolean': + $options[] = "'boolean'"; + break; + + case 'date': + case 'datetime': + case 'timestamp': + $options[] = "'date'"; + break; + + // Add other types as needed + } + + $insert .= sprintf("'%s' => [%s],\n", $column['name'], implode(',', $options)); } return $insert;