Mercurial > packages > magicforger
changeset 34:f65ab84ee47f default
merge with codex
| author | luka |
|---|---|
| date | Wed, 10 Sep 2025 21:00:47 -0400 |
| parents | a9ff874afdbd (current diff) 93adaad3ca65 (diff) |
| children | 55d2e5c5dad9 |
| files | |
| diffstat | 43 files changed, 2044 insertions(+), 409 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,9 @@ +syntax: glob +vendor +*.env +*.env.backup +*.env.production +.php-cs-fixer.cache +*.aichat +tags +
--- a/.php-cs-fixer.cache Sat Dec 02 10:20:32 2023 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -{"php":"8.2.12","version":"3.19.1","indent":" ","lineEnding":"\n","rules":{"align_multiline_comment":true,"array_syntax":true,"backtick_to_shell_exec":true,"binary_operator_spaces":true,"blank_line_before_statement":{"statements":["return"]},"cast_spaces":true,"class_attributes_separation":{"elements":{"method":"one"}},"class_definition":{"single_line":true},"class_reference_name_casing":true,"clean_namespace":true,"concat_space":true,"curly_braces_position":{"allow_single_line_anonymous_functions":true,"allow_single_line_empty_anonymous_classes":true},"declare_parentheses":true,"echo_tag_syntax":true,"empty_loop_body":{"style":"braces"},"empty_loop_condition":true,"fully_qualified_strict_types":true,"function_typehint_space":true,"general_phpdoc_tag_rename":{"replacements":{"inheritDocs":"inheritDoc"}},"global_namespace_import":{"import_classes":false,"import_constants":false,"import_functions":false},"include":true,"increment_style":true,"integer_literal_case":true,"lambda_not_used_import":true,"linebreak_after_opening_tag":true,"magic_constant_casing":true,"magic_method_casing":true,"method_argument_space":{"on_multiline":"ignore"},"native_function_casing":true,"native_function_type_declaration_casing":true,"no_alias_language_construct_call":true,"no_alternative_syntax":true,"no_binary_string":true,"no_blank_lines_after_phpdoc":true,"no_empty_comment":true,"no_empty_phpdoc":true,"no_empty_statement":true,"no_extra_blank_lines":{"tokens":["attribute","case","continue","curly_brace_block","default","extra","parenthesis_brace_block","square_brace_block","switch","throw","use"]},"no_leading_namespace_whitespace":true,"no_mixed_echo_print":true,"no_multiline_whitespace_around_double_arrow":true,"no_null_property_initialization":true,"no_short_bool_cast":true,"no_singleline_whitespace_before_semicolons":true,"no_spaces_around_offset":true,"no_superfluous_phpdoc_tags":{"remove_inheritdoc":true},"no_trailing_comma_in_singleline":true,"no_unneeded_control_parentheses":{"statements":["break","clone","continue","echo_print","others","return","switch_case","yield","yield_from"]},"no_unneeded_curly_braces":{"namespaces":true},"no_unneeded_import_alias":true,"no_unset_cast":true,"no_unused_imports":true,"no_useless_concat_operator":true,"no_useless_nullsafe_operator":true,"no_whitespace_before_comma_in_array":true,"normalize_index_brace":true,"nullable_type_declaration_for_default_null_value":{"use_nullable_type_declaration":false},"object_operator_without_whitespace":true,"operator_linebreak":{"only_booleans":true},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"alpha"},"php_unit_fqcn_annotation":true,"php_unit_method_casing":true,"phpdoc_align":true,"phpdoc_annotation_without_dot":true,"phpdoc_indent":true,"phpdoc_inline_tag_normalizer":true,"phpdoc_no_access":true,"phpdoc_no_alias_tag":true,"phpdoc_no_package":true,"phpdoc_no_useless_inheritdoc":true,"phpdoc_order":{"order":["param","return","throws"]},"phpdoc_return_self_reference":true,"phpdoc_scalar":true,"phpdoc_separation":true,"phpdoc_single_line_var_spacing":true,"phpdoc_summary":true,"phpdoc_tag_type":{"tags":{"inheritDoc":"inline"}},"phpdoc_to_comment":true,"phpdoc_trim":true,"phpdoc_trim_consecutive_blank_line_separation":true,"phpdoc_types":true,"phpdoc_types_order":{"null_adjustment":"always_last","sort_algorithm":"none"},"phpdoc_var_without_name":true,"semicolon_after_instruction":true,"simple_to_complex_string_variable":true,"single_class_element_per_statement":true,"single_import_per_statement":true,"single_line_comment_spacing":true,"single_line_comment_style":{"comment_types":["hash"]},"single_line_throw":true,"single_quote":true,"single_space_around_construct":true,"space_after_semicolon":{"remove_in_empty_for_expressions":true},"standardize_increment":true,"standardize_not_equals":true,"switch_continue_to_break":true,"trailing_comma_in_multiline":true,"trim_array_spaces":true,"types_spaces":true,"unary_operator_spaces":true,"whitespace_after_comma_in_array":true,"yoda_style":true,"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"compact_nullable_typehint":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"new_with_braces":true,"no_blank_lines_after_class_opening":true,"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"return_type_declaration":true,"short_scalar_cast":true,"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"visibility_required":true,"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_spaces_inside_parenthesis":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_line_after_imports":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true},"hashes":{"src\/Generator\/Route\/RouteGenerator.php":"02bf90516b88a1b0b00a84afb10d8a45","src\/Generator\/Controller\/ControllerGenerator.php":"717cdb9124e7e4afe4a28e17b94089e9","src\/Generator\/Model\/ModelGenerator.php":"31743678bf017f63680db19e9682df45","src\/Generator\/Generator.php":"c7cd32166a24be83f6575d33f6c4ba80","src\/Generator\/Requests\/UpdateRequestGenerator.php":"63475513d258b0727637db19629d4ac6","src\/Generator\/Requests\/RequestGenerator.php":"93fbc04a1ea18b794ee7f24902ae9cc1","src\/Generator\/Requests\/StoreRequestGenerator.php":"42bac2b426288d9a08216dfc5fe45faf","src\/Generator\/BaseGenerator.php":"c0aa37a727252383bc28aebf7a0019ec","src\/MagicForgerServiceProvider.php":"00c1321ad894d1e0807036dbe9114bf2","src\/Replacer\/Replacer.php":"501a0ebcf606a7602973b6033ee54b8e","src\/Replacer\/TableReplacer.php":"dc289802665136521abf6ffea0203732","src\/ConfigHelper.php":"c11480fee67c40ca948c38c14bed7d16"}} \ No newline at end of file
--- a/.vimrc Sat Dec 02 10:20:32 2023 -0500 +++ b/.vimrc Wed Sep 10 21:00:47 2025 -0400 @@ -8,6 +8,7 @@ "set number nnoremap <Leader>cc :set colorcolumn=80<cr> nnoremap <Leader>ncc :set colorcolumn-=80<cr> +nnoremap <C-l> :ALECodeAction <cr> set mouse=a function! FixPhpFiles() @@ -27,23 +28,22 @@ " Committing commands map <C-k> :wa<CR>:!hg addremove && hg commit <CR> -" Git commands, for now don't port to hg -" function! GitDiffCached() -" let files = system('git diff --cached --name-only') -" -" if v:shell_error -" echo "Error running git diff" -" return -" endif -" -" let filelist = split(files, "\n") -" let chosen_file = inputlist(filelist) -" -" if chosen_file != -1 -" let cmd = 'tabnew ' . filelist[chosen_file] -" execute cmd -" endif -" endfunction -" -" execute "set <M-d>=\033d" -" map <M-d> :call GitDiffCached()<CR> + + +function! SendBufferToProgram() + " Create a temporary file + let temp_file = tempname() + + " Write current buffer to the temporary file + exe "write! " . temp_file + + " Send the content of the temporary file to your program + " Replace <your_program> with the actual command to run your program + let command = "cat " . temp_file . " | <your_program>" + + " Execute the command + call system(command) + + " Optionally, delete the temporary file if not needed + call delete(temp_file) +endfunction
--- a/composer.json Sat Dec 02 10:20:32 2023 -0500 +++ b/composer.json Wed Sep 10 21:00:47 2025 -0400 @@ -1,20 +1,21 @@ { - "name": "wizzard/magicforger", + "name": "wizard/magicforger", + "version": "1.0.0", "description": "Magically makes all your CRUD in a smart and repeatable way.", - "version": "dev-default", "autoload": { "psr-4": { - "Wizzard\\MagicForger\\":"src/" + "Wizard\\MagicForger\\":"src/" } }, "minimum-stability": "stable", "require": { - "laravel/framework": ">=10.3" + "laravel/framework": ">=12", + "doctrine/dbal": "*" }, "extra": { "laravel": { "providers": [ - "Wizzard\\MagicForger\\MagicForgerServiceProvider" + "Wizard\\MagicForger\\MagicForgerServiceProvider" ] } }
--- a/src/ConfigHelper.php Sat Dec 02 10:20:32 2023 -0500 +++ b/src/ConfigHelper.php Wed Sep 10 21:00:47 2025 -0400 @@ -1,6 +1,8 @@ <?php -namespace Wizzard\MagicForger; +namespace Wizard\MagicForger; + +use Illuminate\Support\Collection; class ConfigHelper { @@ -11,32 +13,28 @@ public const CONFIG_FILE_NAME = 'mf_config.php'; // Config path variable - public static $config_path; + public static string $config_path; /** - * Set up configuration path - * - * @param string $base_path + * Set up configuration path. */ - public static function setup_config_path(string $base_path) + public static function setup_config_path(string $base_path): void { self::$config_path = $base_path.'/'.self::CONFIG_FILE_NAME; } /** - * Get configuration path - * - * @return string + * Get configuration path. */ - protected static function get_config_path() + protected static function get_config_path(): string { return self::$config_path; } /** - * Write configuration into a file + * Write configuration into a file. */ - public static function write_config() + public static function write_config(): void { $path = self::get_config_path(); $str = '<?php @@ -48,18 +46,18 @@ } /** - * Read configuration from a file + * Read configuration from a file. */ - public static function read_config() + public static function read_config(): void { $path = self::get_config_path(); self::$config = include $path; } /** - * Print configuration + * Print configuration. */ - public static function print_config() + public static function print_config(): void { self::varexport(self::$config); } @@ -71,11 +69,10 @@ * * @see https://www.php.net/manual/en/function.var-export.php * - * @param mixed $expression - * @param boolean $return - * @return mixed + * @param bool $return + * @return string|string[]|null */ - public static function varexport($expression, $return = false) + public static function varexport(mixed $expression, $return = false): string|array|null { $export = var_export($expression, true); $patterns = [ @@ -90,31 +87,47 @@ } else { echo $export; } + + return null; } /** - * Format the given file - * - * @param string $path + * Format the given file. */ - protected static function format_file(string $path) + protected static function format_file(string $path): void { exec('php-cs-fixer fix '.$path); } /** - * Set up tables + * Set up tables. */ - public static function set_up_tables() + public static function set_up_tables(): Collection { $schema = \DB::connection()->getDoctrineSchemaManager(); - // get all the tables available in the database $tables = collect($schema->listTableNames())->all(); + $table_foreign_keys = []; + foreach ($tables as $table) { + $table_foreign_keys[$table] = $schema->listTableForeignKeys($table); + } $insert_tables = []; foreach ($tables as $table) { $columns = []; - $table_columns = $schema->introspectTable($table)->getColumns(); + $table_columns = $schema->listTableColumns($table); + + // Initiate new arrays for foreign keys + $foreign_keys = []; + $foreign_keys_reverse = []; + + // Check foreign key references from this table + $foreign_keys_list = $table_foreign_keys[$table]; + foreach ($foreign_keys_list as $fk) { + $foreign_keys[$fk->getLocalColumns()[0]] = [ + 'foreign_table' => $fk->getForeignTableName(), + 'foreign_column' => $fk->getForeignColumns()[0], + ]; + } foreach ($table_columns as $column) { $full_class = get_class($column->getType()); @@ -122,37 +135,52 @@ $class_name = end($class_parts); $columns[$column->getName()] = [ - 'type' => $class_name, - 'should_insert' => [ - 'controller' => true, - 'model' => true, - 'requests' => true, - 'views' => true, - ], - ]; + 'type' => $class_name, + 'should_insert' => [ + 'controller' => true, + 'model' => true, + 'requests' => true, + 'views' => true, + ], + ]; } - + // Check foreign key references to this table + foreach ($tables as $other_table) { + if ($other_table != $table) { + $foreign_keys_list = $table_foreign_keys[$other_table]; + foreach ($foreign_keys_list as $fk) { + if ($fk->getForeignTableName() == $table) { + $foreign_keys_reverse[] = [ + 'table' => $other_table, + 'column' => $fk->getLocalColumns()[0], + ]; + } + } + } + } $insert_tables[$table] = []; $insert_tables[$table]['columns'] = $columns; + $insert_tables[$table]['foreign_keys'] = $foreign_keys; // Foreign keys FROM this table + $insert_tables[$table]['foreign_keys_reverse'] = $foreign_keys_reverse; // Foreign keys TO this table $insert_tables[$table]['type'] = 'default'; } - // Merge the new tables configuration into the initial config + self::merge_array_priority(self::$config['tables'], $insert_tables); return $tables; } /** - * Merge two arrays and ensure priority values do not get overwritten + * Merge two arrays and ensure priority values do not get overwritten. * - * @param array $priority - * @param array $merged + * @param array $priority + * @param array $merged */ - private static function merge_array_priority(&$priority, $merged) + private static function merge_array_priority(&$priority, $merged): void { foreach ($merged as $key => $value) { // if the priority key is not set, automatically add the merged values - if (!isset($priority[$key])) { + if (! isset($priority[$key])) { $priority[$key] = $value; } else { // if the value is an array recursively merge with priority
--- a/src/Generator/BaseGenerator.php Sat Dec 02 10:20:32 2023 -0500 +++ b/src/Generator/BaseGenerator.php Wed Sep 10 21:00:47 2025 -0400 @@ -1,242 +1,203 @@ <?php -namespace Wizzard\MagicForger\Generator; +namespace Wizard\MagicForger\Generator; -use DB; use Illuminate\Console\GeneratorCommand; +use Illuminate\Support\Facades\Schema; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Wizzard\MagicForger\Replacer\Replacer; -use Wizzard\MagicForger\Replacer\TableReplacer; +use Wizard\MagicForger\Replacer\Replacer; +use Wizard\MagicForger\Replacer\TableReplacer; abstract class BaseGenerator extends GeneratorCommand { use Replacer; use TableReplacer; - /** - * The schema of the database. - * - * @var string - */ - protected $schema; + protected $schema = null; + + protected $tables = null; - /** - * The tables available in the schema. - * - * @var array - */ - protected $tables; + protected $currentTable = null; - /** - * The current Table being used. - * - * @var table - */ - protected $currentTable; + protected static $cached_snippets = []; - /** - * Execute the console command. - */ public function handle() { - // First we need to ensure that the table exists, then we can - if (!$this->tableExists($this->getTableInput())) { + + if (! $this->tableExists($this->getTableInput())) { $this->components->error('The table: "'.$this->getTableInput().'" does not exist in the database.'); return false; } $this->setCurrentTable($this->getTableInput()); - $path = $this->getPath(); - $file = $this->getFile($path); - $file = $this->apply_replacements($file); - $file = $this->apply_inserts($file); - $this->makeDirectory($path); - $this->files->put($path, $this->sortImports($file)); - $this->format_file($path); - - $info = $this->type; - - $this->components->info(sprintf('%s [%s] created successfully.', $info, $path)); + $this->components->info(sprintf('%s [%s] created successfully.', $this->type, $path)); } - /** - * Override the original so that we can prompt for a table with autocomplete. - */ - protected function promptForMissingArguments(InputInterface $input, OutputInterface $output) + protected function promptForMissingArguments(InputInterface $input, OutputInterface $output): void { $prompted = false; if (is_null($input->getArgument('table'))) { $prompted = true; $table = null; - while (null === $table) { + while ($table === null) { $table = $this->components->askWithCompletion( 'What Table should we use?', $this->possibleTables() ); } - $input->setArgument('table', $table); } parent::promptForMissingArguments($input, $output); - // This will get missed if we prompt here but not in the parent if ($prompted) { $this->afterPromptingForMissingArguments($input, $output); } } - /** - * Get the console command arguments. - * - * @return array - */ - protected function getArguments() + protected function getArguments(): array { return [ ['table', InputOption::VALUE_REQUIRED, 'The table to generate files for.'], ]; } - /** - * Prompt for missing input arguments using the returned questions. - * - * @return array - */ - protected function promptForMissingArgumentsUsing() + protected function promptForMissingArgumentsUsing(): array { - return [ - ]; + return []; } - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() + protected function getOptions(): array { return [ ['fresh', 'f', InputOption::VALUE_NONE, 'Start from the stub or use existing if possible.'], ]; } - /** - * Interact further with the user if they were prompted for missing arguments. - * - * @return void - */ - protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output) + protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output): void { + // Additional logic after prompting goes here } - /** - * Determines if the file exists. - */ protected function fileExists(string $path): bool { return $this->files->exists($path); } - /** - * Gets the file that will be worked on. If there is already an existing file - * then we can open that. However if we are forcing the operation, then we - * will start with an empty stub. - */ - protected function getFile($name) + protected function getFile($name): string { - if (!($this->hasOption('fresh') - && $this->option('fresh')) - && $this->fileExists($name)) { - // Working with an existing file + if (! ($this->hasOption('fresh') && $this->option('fresh')) && $this->fileExists($name)) { return $this->files->get($name); } - // Working with a stub return $this->files->get($this->getStub()); } - /** - * Get the desired class table from the input. - * - * @return string - */ - protected function getTableInput() + protected function getTableInput(): string { return trim($this->argument('table')); } - /** - * Determines if the table exists in the current database. - */ protected function tableExists(string $table_name): bool { return in_array($table_name, $this->getTables()); } - /** - * Get a list of possible table names. - */ - protected function possibleTables() + protected function possibleTables(): array { return $this->getTables(); } - /** - * Get the tables in the schema. - */ - protected function getTables() + protected function getTables(): array { if (is_null($this->tables)) { - $this->tables = collect($this->getSchema()->listTableNames())->all(); + $this->tables = Schema::getTableListing(schema: config('database.connections.mariadb.database'), schemaQualified: false); } return $this->tables; } - /** - * Get the database schema for DB interactions. - */ - protected function getSchema() - { - if (is_null($this->schema)) { - $this->schema = \DB::connection()->getDoctrineSchemaManager(); - } - - return $this->schema; - } - protected function getTable(string $table_name) { return $this->getSchema()->introspectTable($table_name); } + /* + * returns array of columns in the form of: + * + [ + "name" => "column_type" + "type_name" => "bigint" + "type" => "bigint(20) unsigned" + "collation" => null + "nullable" => true + "default" => "NULL" + "auto_increment" => false + "comment" => null + "generation" => null + ] + */ + protected static function getTableColumns(string $table_name) + { + return Schema::getColumns($table_name); + } + + /* + * returns array of foreign keys in the form of: + * + [ + "name" => "foreign_key_name" + "columns" => [ + 0 => "local_column_name" + ] + "foreign_schema" => "schema_name" + "foreign_table" => "foreign_table_name" + "foreign_columns" => [ + 0 => "foreign_column_name" + ] + "on_update" => "restrict" + "on_delete" => "restrict" + ] + * + */ + protected static function getTableForeignKeys(string $table_name) + { + return Schema::getForeignKeys($table_name); + } + protected function getCurrentTable() { return $this->currentTable; } - protected function setCurrentTable(string $table_name) + protected function setCurrentTable(string $table_name): void { - $table = null; - if (!is_null($table_name) && '' !== trim($table_name)) { - $table = $this->getTable($table_name); - } - $this->currentTable = $table; + $this->currentTable = $table_name; + } + + protected function format_file(string $path): void + { + exec('./vendor/bin/pint '.escapeshellarg($path)); } - protected function format_file(string $path) + protected function getSnippet($snippet_name) { - exec('php-cs-fixer fix '.$path); + // Cache snippet contents to avoid re-reading files + if (! isset(self::$cached_snippets[$snippet_name])) { + self::$cached_snippets[$snippet_name] = $this->files->get( + $this->resolveStubPath("/snippets/$snippet_name.stub")); + } + + return self::$cached_snippets[$snippet_name]; } }
--- a/src/Generator/Controller/ControllerGenerator.php Sat Dec 02 10:20:32 2023 -0500 +++ b/src/Generator/Controller/ControllerGenerator.php Wed Sep 10 21:00:47 2025 -0400 @@ -1,9 +1,9 @@ <?php -namespace Wizzard\MagicForger\Generator\Controller; +namespace Wizard\MagicForger\Generator\Controller; use Symfony\Component\Console\Attribute\AsCommand; -use Wizzard\MagicForger\Generator\BaseGenerator; +use Wizard\MagicForger\Generator\BaseGenerator; #[AsCommand(name: 'mf:controller')] class ControllerGenerator extends BaseGenerator @@ -32,47 +32,46 @@ /** * Execute the console command. */ - public function handle() + public function handle(): void { parent::handle(); } /** * Get the stub file for the generator. - * - * @return string */ - protected function getStub() + protected function getStub(): string { return $this->resolveStubPath('/stubs/controller.stub'); } /** * Resolve the fully-qualified path to the stub. - * - * @param string $stub - * - * @return string */ - protected function resolveStubPath($stub) + protected function resolveStubPath(string $stub): string { - return is_file($customPath = $this->laravel->basePath(trim($stub, '/'))) - ? $customPath - : __DIR__.$stub; + $customPath = $this->laravel->basePath(trim($stub, '/')); + + return is_file($customPath) ? $customPath : __DIR__.$stub; } - protected function getClassName($name) + /** + * Get the path for the generated file. + */ + protected function getPath($name = null) + { + return str_replace( + ['App\\', '\\'], + ['app/', '/'], + $this->getControllerNamespace().'/'.$this->controller_name($this->getTableInput()).'.php' + ); + } + + /** + * Get the class name for the controller. + */ + protected function getClassName(string $name): string { return $this->controller_name($name); } - - /** - * Get the stub file for the generator. - * - * @return string - */ - protected function getPath($name = null) - { - return str_replace(['App\\', '\\'], ['app/', '/'], $this->getControllerNamespace().'/'.$this->controller_name($this->getTableInput()).'.php'); - } }
--- a/src/Generator/Controller/stubs/controller.stub Sat Dec 02 10:20:32 2023 -0500 +++ b/src/Generator/Controller/stubs/controller.stub Wed Sep 10 21:00:47 2025 -0400 @@ -10,91 +10,126 @@ { /** * Display a listing of the resource. + * + * @param {{ filterRequest }} $request + * @return \Illuminate\View\View */ - public function index() + public function index({{ filterRequest }} $request) { - $data = []; - - $data['items'] = {{ model }}::all(); + $validated = $request->validated(); + $data = []; + $data['items'] = {{ model }}::get_data($validated); + $data = array_merge($data, {{ model }}::load_index()); return view('{{ tableName }}.index', $data); } + public function get_data() + { + $data = []; + $data['records'] = {{ model }}::get_data([]); + $data['total_records'] = count($data['records']); + + return $data; + } + /** * Show the form for creating a new resource. + * + * @return \Illuminate\View\View */ public function create() { - $data = []; + $data = []; + $data = array_merge($data, {{ model }}::load_create()); return view('{{ tableName }}.create_edit', $data); } /** * Store a newly created resource in storage. + * + * @param {{ storeRequest }} $request + * @return \Illuminate\Http\RedirectResponse */ public function store({{ storeRequest }} $request) { - $validated = $request->validated(); - - // - ${{ modelVariable }} = new {{ model }}(); + $validated = $request->validated(); - //insert the values into the model - // {{ valuesForCreation }} - - ${{ modelVariable }}->save(); + {{ model }}::create($validated); return redirect()->route('{{ tableName }}.index'); } /** * Display the specified resource. + * + * @param {{ model }} ${{ modelVariable }} + * @return \Illuminate\View\View */ public function show({{ model }} ${{ modelVariable }}) { - $data = []; - - $data['item'] = ${{ modelVariable }}; + $data = []; + $data['item'] = ${{ modelVariable }}; + $data['fields'] = (new {{ model }}())->getFillable(); + + return view('{{ tableName }}.show', $data); + } - return view('{{ tableName }}.show', $data); + /** + * Returns the resource in JSON format. + * + * @param ModelType $modelVariable + * @return string + */ + public function load({{ model }} ${{ modelVariable }}) + { + return ${{ modelVariable }}->toJson(); } /** * Show the form for editing the specified resource. + * + * @param {{ model }} ${{ modelVariable }} + * @return \Illuminate\View\View */ public function edit({{ model }} ${{ modelVariable }}) { - $data = []; + $data = []; + $data['item'] = ${{ modelVariable }}; - $data['item'] = ${{ modelVariable }}; + // Load data for relationships + $data = array_merge($data, {{ model }}::load_edit()); - return view('{{ tableName }}.create_edit', $data); + return view('{{ tableName }}.create_edit', $data); } /** * Update the specified resource in storage. + * + * @param {{ updateRequest }} $request + * @param {{ model }} ${{ modelVariable }} + * @return \Illuminate\Http\RedirectResponse */ public function update({{ updateRequest }} $request, {{ model }} ${{ modelVariable }}) { - $validated = $request->validated(); + $validated = $request->validated(); - // Set the variables - //{{ valuesForCreation }} + ${{ modelVariable }}->update($validated); - ${{ modelVariable }}->save(); - - return redirect()->route('{{ tableName }}.index'); + return redirect()->route('{{ tableName }}.index'); } /** * Remove the specified resource from storage. + * + * @param {{ model }} ${{ modelVariable }} + * @return \Illuminate\Http\RedirectResponse */ public function destroy({{ model }} ${{ modelVariable }}) { - // - ${{ modelVariable }}->delete(); + ${{ modelVariable }}->delete(); - return redirect()->route('{{ tableName }}.index'); + return redirect()->route('{{ tableName }}.index'); } }
--- a/src/Generator/Generator.php Sat Dec 02 10:20:32 2023 -0500 +++ b/src/Generator/Generator.php Wed Sep 10 21:00:47 2025 -0400 @@ -1,6 +1,6 @@ <?php -namespace Wizzard\MagicForger\Generator; +namespace Wizard\MagicForger\Generator; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; @@ -29,8 +29,9 @@ */ public function handle() { + // First we need to ensure that the table exists, then we can - if (!$this->tableExists($this->getTableInput())) { + if (! $this->tableExists($this->getTableInput())) { $this->components->error('The table: "'.$this->getTableInput().'" does not exist in the database.'); return false; @@ -46,6 +47,7 @@ $this->input->setOption('controller', true); $this->input->setOption('model', true); $this->input->setOption('request', true); + $this->input->setOption('view', true); $this->input->setOption('route', true); } @@ -73,6 +75,10 @@ $this->createRequest(); } + if ($this->option('view')) { + $this->createView(); + } + if ($this->option('route')) { $this->createRoute(); } @@ -80,34 +86,27 @@ /** * Get the console command options. - * - * @return array */ - protected function getOptions() + protected function getOptions(): array { return array_merge(parent::getOptions(), [ ['all', 'a', InputOption::VALUE_NONE, 'Generate a migration, seeder, factory, policy, resource controller, and form request classes for the table.'], ['controller', 'c', InputOption::VALUE_NONE, 'Generate a controller class for the table.'], ['model', 'm', InputOption::VALUE_NONE, 'Generate a model class for the table.'], ['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.'], ]); } /** * Interact further with the user if they were prompted for missing arguments. - * - * @return void */ - protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output) - { - } + protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output): void {} - protected function getStub() - { - } + protected function getStub(): void {} - protected function createController() + protected function createController(): void { $this->call('mf:controller', ['table' => $this->getTableInput(), '--fresh' => $this->option('fresh')]); } @@ -122,6 +121,11 @@ $this->call('mf:request', ['table' => $this->getTableInput(), '--fresh' => $this->option('fresh'), '--all' => true]); } + protected function createView() + { + $this->call('mf:view', ['table' => $this->getTableInput(), '--fresh' => $this->option('fresh'), '--all' => true]); + } + protected function createRoute() { $this->call('mf:route', ['table' => $this->getTableInput(), '--fresh' => $this->option('fresh')]);
--- a/src/Generator/Model/ModelGenerator.php Sat Dec 02 10:20:32 2023 -0500 +++ b/src/Generator/Model/ModelGenerator.php Wed Sep 10 21:00:47 2025 -0400 @@ -1,9 +1,11 @@ <?php -namespace Wizzard\MagicForger\Generator\Model; +namespace Wizard\MagicForger\Generator\Model; use Symfony\Component\Console\Attribute\AsCommand; -use Wizzard\MagicForger\Generator\BaseGenerator; +use Wizard\MagicForger\Generator\BaseGenerator; +use Wizard\MagicForger\Helpers\RelationshipNavigator; +use Illuminate\Support\Str; #[AsCommand(name: 'mf:model')] class ModelGenerator extends BaseGenerator @@ -29,12 +31,17 @@ */ protected $type = 'Model'; + protected static $cached_snippets = []; + /** * Execute the console command. + * + * @return mixed */ public function handle() { - parent::handle(); + // Delegate to parent handler (includes replacements and insertions) + return parent::handle(); } /** @@ -44,14 +51,17 @@ */ protected function getStub() { + if (! is_null(RelationshipNavigator::isPivot($this->getCurrentTable()))) { + return $this->resolveStubPath('/stubs/model.pivot.stub'); + } + return $this->resolveStubPath('/stubs/model.stub'); } /** * Resolve the fully-qualified path to the stub. * - * @param string $stub - * + * @param string $stub * @return string */ protected function resolveStubPath($stub) @@ -75,4 +85,151 @@ { return str_replace(['App\\', '\\'], ['app/', '/'], $this->getModelNamespace().'/'.$this->model_name($this->getTableInput()).'.php'); } + + protected function gatherRelations() { + $relations = RelationshipNavigator::getRelations($this->getCurrentTable()); + + return $relations; + + } + + protected function renderFilters() { + $insert = ''; + foreach ($this->get_columns() as $column) { + if (in_array($column['name'], $this->columns_to_ignore)) { + continue; + } + $snippet = $this->getSnippet('filter'); + $tableName = $this->getCurrentTable(); + $value = 'value'; // TODO: this should be determined based on column type + $columnName = $column['name']; + $columnDisplay = Str::headline($columnName); + + // Replace placeholders with actual values + $string = str_replace( + ['{{value}}', '{{columnDisplay}}', '{{tableName}}', '{{columnName}}'], + [$value, $columnDisplay, $tableName, $columnName], + $snippet + ); + $insert .= sprintf("%s", $string); + } + + return $insert; + } + + protected function renderRelations($relations) { + $renders = [ + 'belongsTo' => [], + 'hasMany' => [], + 'belongsToMany' => [], + ]; + // Render belongsTo relations + foreach (($relations['belongsTo'] ?? []) as $relation) { + $renders['belongsTo'][] = $this->renderBelongsTo($relation); + } + + // Render hasMany relations + foreach (($relations['hasMany'] ?? []) as $relation) { + $renders['hasMany'][] = $this->renderHasMany($relation); + } + + // Render belongsToMany (many-to-many) via hasManyThrough pivot relations + foreach (($relations['hasManyThrough'] ?? []) as $relation) { + $renders['belongsToMany'][] = $this->renderBelongsToMany($relation); + } + return $renders; + } + + protected function renderBelongsTo($relationship) + { + $snippet = $this->getSnippet('belongs_to_relation'); + $relationName = Str::singular($relationship['table']); + $relatedModel = $this->getClassName($relationship['table']); + $columnName = $relationship['column']; + + // Replace placeholders with actual values + $string = str_replace( + ['{{relationName}}', '{{relatedModel}}', '{{columnName}}'], + [$relationName, $relatedModel, $columnName], + $snippet + ); + + return $string; + } + + /** + * Render a hasMany relation. + * + * @param array $relationship + * @return string + */ + protected function renderHasMany($relationship) + { + $snippet = $this->getSnippet('has_many_relation'); + // Method name uses camel case for plural relation + $relationName = Str::camel($relationship['table']); + $relatedModel = $this->getClassName($relationship['table']); + $columnName = $relationship['column']; + + // Replace placeholders with actual values + $string = str_replace( + ['{{relationName}}', '{{relatedModel}}', '{{columnName}}'], + [$relationName, $relatedModel, $columnName], + $snippet + ); + + return $string; + } + + protected function renderBelongsToMany($relationship) + { + $snippet = $this->getSnippet('belongs_to_many_relation'); + $relationName = $relationship['table']; + $relatedModel = $this->getClassName($relationship['table']); + $pivotTable = $relationship['through']['table']; + $foreignPivotKey = $relationship['through']['external_column']; + $relatedPivotKey = $relationship['through']['internal_column']; + + // Replace placeholders with actual values + $string = str_replace( + ['{{relationName}}', '{{relatedModel}}', '{{pivotTable}}', '{{foreignPivotKey}}', '{{relatedPivotKey}}'], + [$relationName, $relatedModel, $pivotTable, $foreignPivotKey, $relatedPivotKey], + $snippet + ); + + return $string; + } + + /** + * 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(); + + // Gather and render relationships for this model + $relations = $this->gatherRelations(); + $rendered = $this->renderRelations($relations); + $filters = $this->renderFilters(); + + // Build code blocks for each relation type + $belongs = !empty($rendered['belongsTo']) ? implode("\n ", $rendered['belongsTo']) : ''; + $hasMany = !empty($rendered['hasMany']) ? implode("\n ", $rendered['hasMany']) : ''; + $belongsMany = !empty($rendered['belongsToMany']) ? implode("\n ", $rendered['belongsToMany']) : ''; + + // Default relations are based on the belongsTo relationship + $default_relations = implode(", \n", array_map(function ($rel) {return '\'' . Str::singular($rel['table']) . '\''; }, $relations['belongsTo'])); + + // Assign to stub placeholders + $inserts['# {{ belongs_to_relationships }}'] = $belongs; + $inserts['# {{ has_many_relationships }}'] = $hasMany; + $inserts['# {{ has_many_through_relationships }}'] = $belongsMany; + $inserts['# {{ defaultRelationsInsertPoint }}'] = $default_relations; + $inserts['# {{ defaultFiltersInsertPoint }}'] = $filters; + + return $inserts; + } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/Model/snippets/belongs_to_many_relation.stub Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,6 @@ + +public function {{relationName}}() +{ + return $this->belongsToMany({{relatedModel}}::class, '{{pivotTable}}', '{{foreignPivotKey}}', '{{relatedPivotKey}}'); +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/Model/snippets/belongs_to_relation.stub Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,6 @@ + +public function {{relationName}}() +{ + return $this->belongsTo({{relatedModel}}::class, '{{columnName}}'); +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/Model/snippets/filter.stub Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,6 @@ +'{{columnName}}' => [ + 'column_name' => '{{columnName}}', + 'table' => '{{tableName}}', + 'display' => '{{columnDisplay}}', + 'type' => '{{value}}', +],
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/Model/snippets/has_many_relation.stub Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,4 @@ +public function {{relationName}}() +{ + return $this->hasMany({{relatedModel}}::class, '{{columnName}}'); +}
--- a/src/Generator/Model/stubs/model.pivot.stub Sat Dec 02 10:20:32 2023 -0500 +++ b/src/Generator/Model/stubs/model.pivot.stub Wed Sep 10 21:00:47 2025 -0400 @@ -10,12 +10,96 @@ /** * Indicates if the model should be timestamped. - * By default our pivots will not use timestamps + * By default our pivots will not use timestamps * * @var bool */ public $timestamps = false; + protected $default_relations = [ + # {{ defaultRelationsInsertPoint }} + ]; + //relations + + // BelongsTo + # {{ belongs_to_relationships }} + + // HasMany + # {{ has_many_relationships }} + + // HasManyThrough + # {{ has_many_through_relationships }} + + /** + * Load the default relations for the model. + * + * @return $this + */ + public function load_relations() { + foreach($this->default_relations as $relation) { + $this->load($relation); + } + return $this; + } + + //MARK FOR MODEL + protected static function load_auxilary_data() { + $data = []; + + $instance = new static(); + + foreach($instance->default_relations as $relation) { + $related_model = $instance->$relation()->getRelated(); + $related_table = $related_model->getTable(); + $data[$related_table] = $related_model->all()->pluck('name','id')->toArray(); + } + + return $data; + } + + //MARK FOR MODEL + public static function load_index() { + return static::load_auxilary_data(); + } + + //MARK FOR MODEL + public static function load_create() { + return static::load_auxilary_data(); + } + + //MARK FOR MODEL + public static function load_edit() { + return static::load_auxilary_data(); + } + + + /** + * Retrieve a query builder instance with default relations loaded. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + //MARK FOR MODEL + public static function data_query() { + $query = static::query(); + + $instance = new static(); + + foreach($instance->default_relations as $relation) { + $query->with($relation); + } + + return $query; + } + + /** + * Retrieve a query builder instance with default relations loaded. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public static function get_data() + { + return static::data_query()->get(); + } }
--- a/src/Generator/Model/stubs/model.stub Sat Dec 02 10:20:32 2023 -0500 +++ b/src/Generator/Model/stubs/model.stub Wed Sep 10 21:00:47 2025 -0400 @@ -2,11 +2,10 @@ namespace {{ namespace }}; -use Illuminate\Database\Eloquent\Factories\HasFactory; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; +use Wizard\Framework\Models\BaseModel; -class {{ class }} extends Model +class {{ class }} extends BaseModel { //use HasFactory; use SoftDeletes; @@ -27,17 +26,67 @@ # {{ atributeInsertPoint }} ]; + protected $casts = [ + # {{ castInsertPoint }} + ]; - public static function boot() : void { - parent::boot(); + protected $fillable = [ + # {{ fillableInsertPoint }} + ]; + + protected $default_relations = [ + # {{ defaultRelationsInsertPoint }} + ]; + + protected static $filters = [ + # {{ defaultFiltersInsertPoint }} + ]; + + public static function get_filters() { + return static::filters; + } + + //relations + + // BelongsTo + # {{ belongs_to_relationships }} + + // HasMany + # {{ has_many_relationships }} + + // HasManyThrough + # {{ has_many_through_relationships }} + - self::creating(function ($item) { - $item->created_by = \Auth::user()->id; - $item->updated_by = \Auth::user()->id; - }); + /** + * Load the default relations for the model. + * + * @return $this + */ + public function load_relations() { + foreach($this->default_relations as $relation) { + $this->load($relation); + } + return $this; + } - self::saving(function ($item) { - $item->updated_by = \Auth::user()->id; - }); + /** + * Retrieve a query builder instance with default relations loaded. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public static function data_query() { + return parent::data_query(); } + + /** + * Retrieve a query builder instance with default relations loaded. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public static function get_data(array $validated = []) + { + return parent::get_data($validated); + } + }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/Requests/FilterRequestGenerator.php Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,77 @@ +<?php + +namespace Wizard\MagicForger\Generator\Requests; + +use Symfony\Component\Console\Attribute\AsCommand; +use Wizard\MagicForger\Generator\BaseGenerator; + +#[AsCommand(name: 'mf:filter_request')] +class FilterRequestGenerator extends BaseGenerator +{ + /** + * The name and signature of the console command. + * + * @var string + */ + protected $name = 'mf:filter_request'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Generates the FilterRequest File for a table.'; + + /** + * The type of class being generated. + * + * @var string + */ + protected $type = 'FilterRequest'; + + /** + * Execute the console command. + */ + public function handle() + { + parent::handle(); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + return $this->resolveStubPath('/stubs/request.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->filter_request_name($name); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getPath($name = null) + { + return str_replace(['App\\', '\\'], ['app/', '/'], $this->getRequestNamespace($this->getTableInput()).'/'.$this->filter_request_name($this->getTableInput()).'.php'); + } +}
--- a/src/Generator/Requests/RequestGenerator.php Sat Dec 02 10:20:32 2023 -0500 +++ b/src/Generator/Requests/RequestGenerator.php Wed Sep 10 21:00:47 2025 -0400 @@ -1,12 +1,12 @@ <?php -namespace Wizzard\MagicForger\Generator\Requests; +namespace Wizard\MagicForger\Generator\Requests; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Wizzard\MagicForger\Generator\BaseGenerator; +use Wizard\MagicForger\Generator\BaseGenerator; #[AsCommand(name: 'mf:request')] class RequestGenerator extends BaseGenerator @@ -38,17 +38,22 @@ public function handle() { // First we need to ensure that the table exists, then we can - if (!$this->tableExists($this->getTableInput())) { + if (! $this->tableExists($this->getTableInput())) { $this->components->error('The table: "'.$this->getTableInput().'" does not exist in the database.'); return false; } if ($this->option('all')) { + $this->input->setOption('filter_request', true); $this->input->setOption('store_request', true); $this->input->setOption('update_request', true); } + if ($this->option('filter_request')) { + $this->createFilterRequest(); + } + if ($this->option('store_request')) { $this->createStoreRequest(); } @@ -60,13 +65,12 @@ /** * Get the console command options. - * - * @return array */ - protected function getOptions() + protected function getOptions(): array { return array_merge(parent::getOptions(), [ ['all', 'a', InputOption::VALUE_NONE, 'Generate all request classes for the table.'], + ['filter_request', 'i', InputOption::VALUE_NONE, 'Generate filter request class for the table.'], ['store_request', 's', InputOption::VALUE_NONE, 'Generate store request class for the table.'], ['update_request', 'u', InputOption::VALUE_NONE, 'Generate update request class for the table.'], ]); @@ -74,20 +78,19 @@ /** * Interact further with the user if they were prompted for missing arguments. - * - * @return void */ - protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output) - { - } + protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output): void {} /** * Get the stub file for the generator. * * @return string */ - protected function getStub() + protected function getStub() {} + + protected function createFilterRequest() { + $this->call('mf:filter_request', ['table' => $this->getTableInput()]); } protected function createStoreRequest()
--- a/src/Generator/Requests/StoreRequestGenerator.php Sat Dec 02 10:20:32 2023 -0500 +++ b/src/Generator/Requests/StoreRequestGenerator.php Wed Sep 10 21:00:47 2025 -0400 @@ -1,9 +1,9 @@ <?php -namespace Wizzard\MagicForger\Generator\Requests; +namespace Wizard\MagicForger\Generator\Requests; use Symfony\Component\Console\Attribute\AsCommand; -use Wizzard\MagicForger\Generator\BaseGenerator; +use Wizard\MagicForger\Generator\BaseGenerator; #[AsCommand(name: 'mf:store_request')] class StoreRequestGenerator extends BaseGenerator @@ -50,8 +50,7 @@ /** * Resolve the fully-qualified path to the stub. * - * @param string $stub - * + * @param string $stub * @return string */ protected function resolveStubPath($stub)
--- a/src/Generator/Requests/UpdateRequestGenerator.php Sat Dec 02 10:20:32 2023 -0500 +++ b/src/Generator/Requests/UpdateRequestGenerator.php Wed Sep 10 21:00:47 2025 -0400 @@ -1,9 +1,9 @@ <?php -namespace Wizzard\MagicForger\Generator\Requests; +namespace Wizard\MagicForger\Generator\Requests; use Symfony\Component\Console\Attribute\AsCommand; -use Wizzard\MagicForger\Generator\BaseGenerator; +use Wizard\MagicForger\Generator\BaseGenerator; #[AsCommand(name: 'mf:update_request')] class UpdateRequestGenerator extends BaseGenerator @@ -50,8 +50,7 @@ /** * Resolve the fully-qualified path to the stub. * - * @param string $stub - * + * @param string $stub * @return string */ protected function resolveStubPath($stub)
--- a/src/Generator/Requests/stubs/request.stub Sat Dec 02 10:20:32 2023 -0500 +++ b/src/Generator/Requests/stubs/request.stub Wed Sep 10 21:00:47 2025 -0400 @@ -22,7 +22,7 @@ public function rules(): array { return [ - // + // {{ valuesForValidation }} ]; } }
--- a/src/Generator/Route/RouteGenerator.php Sat Dec 02 10:20:32 2023 -0500 +++ b/src/Generator/Route/RouteGenerator.php Wed Sep 10 21:00:47 2025 -0400 @@ -1,9 +1,9 @@ <?php -namespace Wizzard\MagicForger\Generator\Route; +namespace Wizard\MagicForger\Generator\Route; use Symfony\Component\Console\Attribute\AsCommand; -use Wizzard\MagicForger\Generator\BaseGenerator; +use Wizard\MagicForger\Generator\BaseGenerator; #[AsCommand(name: 'mf:routes')] class RouteGenerator extends BaseGenerator @@ -50,8 +50,7 @@ /** * Resolve the fully-qualified path to the stub. * - * @param string $stub - * + * @param string $stub * @return string */ protected function resolveStubPath($stub)
--- a/src/Generator/Route/stubs/route.stub Sat Dec 02 10:20:32 2023 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ -<?php -Route::controller({{ controllerName }}::class) - ->prefix('{{ tableName }}') - ->alias('{{ tableName }}.') - ->group( function () { - Route::get('/', 'index')->name('index'); - Route::get('/create', 'create')->name('create'); - Route::get('/edit', 'edit')->name('edit'); - - Route::post('/store', 'store')->name('store'); - Route::put('/udpate/{id}', 'update')->name('update'); - Route::delete('/delete/{id}', 'delete')->name('delete'); - });
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/Route/stubs/routes.stub Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,19 @@ +<?php +use Illuminate\Support\Facades\Route; +use \App\Http\Controllers\{{ controllerName }}; +Route::controller({{ controllerName }}::class) + ->middleware(['web','auth']) + ->prefix('{{ tableName }}') + ->as('{{ tableName }}.') + ->group( function () { + Route::get('/', 'index')->name('index'); + Route::post('/get_data', 'get_data')->name('get_data'); + Route::get('/create', 'create')->name('create'); + Route::get('/{{{ modelVariable }}}/edit', 'edit')->name('edit'); + Route::get('/{{{ modelVariable }}}', 'show')->name('show'); + Route::get('/{{{ modelVariable }}}/load', 'load')->name('load'); + + Route::post('/store', 'store')->name('store'); + Route::put('/udpate/{{{ modelVariable }}}', 'update')->name('update'); + Route::delete('/destroy/{{{ modelVariable }}}', 'destroy')->name('destroy'); + });
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/View/CreateEditViewGenerator.php Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,164 @@ +<?php + +namespace Wizard\MagicForger\Generator\View; + +use Symfony\Component\Console\Attribute\AsCommand; +use Wizard\MagicForger\Generator\BaseGenerator; +use Wizard\MagicForger\Helpers\RelationshipNavigator; +use Illuminate\Support\Str; + +#[AsCommand(name: 'mf:create_edit_view')] +class CreateEditViewGenerator extends BaseGenerator +{ + /** + * The name and signature of the console command. + * + * @var string + */ + protected $name = 'mf:create_edit_view'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Generates the CreateEditView File for a table.'; + + /** + * The type of class being generated. + * + * @var string + */ + protected $type = 'CreateEditView'; + + /** + * Execute the console command. + */ + public function handle() + { + parent::handle(); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + return $this->resolveStubPath('/stubs/create_edit.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->create_edit_view_name($name); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getPath($name = null) + { + return str_replace(['Resources\\', '\\'], ['resources/', '/'], $this->getViewNamespace($this->getTableInput()).'create_edit.blade.php'); + } + + protected function renderColumns() { + $renders = []; + + $columns = $this->getTableColumns($this->getCurrentTable()); + $relations = RelationshipNavigator::getRelations($this->getCurrentTable()); + //gether the select columns based on the relations + $selects = []; + + foreach($relations['belongsTo'] as $relation) { + $selects[$relation['column']] = $relation['table']; + } + + foreach($columns as $column) { + $name = $column['name']; + if(in_array($name, $this->columns_to_ignore)) continue; + + $type = $column['type_name']; + + $replacements = [ + '{{fieldName}}' => $name, + '{{fieldLabel}}' => Str::headline($name), + '{{required}}' => $column['nullable'] ? 'false' : 'true', + ]; + $snippet = ''; + + //date + if(in_array($type, ['date', 'timestamp'])) { + $snippet = $this->getSnippet('input/date'); + } + //checkbox + elseif(in_array($type, ['tinyint']) && array_key_exists($name, $selects)) { + $snippet = $this->getSnippet('input/checkbox'); + } + + //select + elseif(in_array($type, ['bigint']) && array_key_exists($name, $selects)) { + $snippet = $this->getSnippet('input/select'); + $replacements['{{fieldLabel}}'] = Str::headline(Str::singular($selects[$name])); + $replacements['{{options}}'] = '$'.$selects[$name]; + } + + //text area + elseif(in_array($type, ['text'])) { + $snippet = $this->getSnippet('input/textarea'); + } + else { + //varchar, bigint, float, etc + $snippet = $this->getSnippet('input/text'); + } + + + // Replace placeholders with actual values + $renders[] = str_replace( + array_keys($replacements), + $replacements, + $snippet + ); + } + return $renders; + } + + + + /** + * 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(); + + // Gather and render relationships for this model + $rendered = $this->renderColumns(); + + // Build code blocks for each relation type + $columns = !empty($rendered) ? implode("\n ", $rendered) : ''; + + // Assign to stub placeholders + $inserts['{{ fieldsInsertPoint }}'] = $columns; + + return $inserts; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/View/IndexViewGenerator.php Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,173 @@ +<?php + +namespace Wizard\MagicForger\Generator\View; + +use Illuminate\Support\Str; +use Symfony\Component\Console\Attribute\AsCommand; +use Wizard\MagicForger\Generator\BaseGenerator; +use Wizard\MagicForger\Helpers\RelationshipNavigator; + +#[AsCommand(name: 'mf:index_view')] +class IndexViewGenerator extends BaseGenerator +{ + /** + * The name and signature of the console command. + * + * @var string + */ + protected $name = 'mf:index_view'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Generates the IndexView File for a table.'; + + /** + * The type of class being generated. + * + * @var string + */ + protected $type = 'IndexView'; + + /** + * Execute the console command. + */ + public function handle() + { + parent::handle(); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + return $this->resolveStubPath('/stubs/index.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->index_view_name($name); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getPath($name = null) + { + return str_replace(['Resources\\', '\\'], ['resources/', '/'], $this->getViewNamespace($this->getTableInput()).'index.blade.php'); + } + + protected function renderColumns() + { + $renders = []; + $values = []; + + $columns = $this->getTableColumns($this->getCurrentTable()); + $relations = RelationshipNavigator::getRelations($this->getCurrentTable()); + // gether the select columns based on the relations + $selects = []; + + foreach ($relations['belongsTo'] as $relation) { + $selects[$relation['column']] = $relation['table']; + } + + foreach ($columns as $column) { + $name = $column['name']; + if (in_array($name, $this->columns_to_ignore)) { + continue; + } + + // Get the expected header name + $replacements = [ + '{{header}}' => Str::headline($name), + '{{column_name}}' => $name, + '{{valueClass}}' => 'p-2', + ]; + + $type = $column['type_name']; + + // date + if (in_array($type, ['date'])) { + $replacements['{{value}}'] = '{{ $item->'.$name.'?->format(\'Y-m-d\') ?? "" }}'; + } + // time + if (in_array($type, ['timestamp'])) { + $replacements['{{value}}'] = '{{ $item->'.$name.'?->format(\'Y-m-d H:i\') ?? "" }}'; + } + // checkbox + if (in_array($type, ['tinyint'])) { + $replacements['{{valueClass}}'] .= ' text-center'; + $replacements['{{value}}'] = '{{ $item->'.$name.' ?? "0" }}'; + } + // select + elseif (in_array($type, ['bigint']) && array_key_exists($name, $selects)) { + $replacements['{{header}}'] = Str::headline(Str::singular($selects[$name])); + $replacements['{{value}}'] = '{{ $item->'.Str::singular($selects[$name]).'?->name ?? "" }}'; + } + // bigint, float + elseif (in_array($type, ['bigint', 'float', 'int'])) { + $replacements['{{valueClass}}'] .= ' text-start'; + } else { + // text area + // varchar, , etc + } + + $snippet = $this->getSnippet('index/value'); + // Replace placeholders with actual values + $values[] = str_replace( + array_keys($replacements), + $replacements, + $snippet + ); + + } + + return ['values' => $values]; + } + + /** + * 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 + $rendered = $this->renderColumns(); + + // Build code blocks for each relation type + $headers = ''; + $values = ''; + $colCount = ''; + + if (! empty($rendered)) { + $values = ! empty($rendered['values']) ? implode("\n ", $rendered['values']) : ''; + } + + // Assign to stub placeholders + $inserts['{{ columnInsertPoint }}'] = $values; + + return $inserts; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/View/ShowViewGenerator.php Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,77 @@ +<?php + +namespace Wizard\MagicForger\Generator\View; + +use Symfony\Component\Console\Attribute\AsCommand; +use Wizard\MagicForger\Generator\BaseGenerator; + +#[AsCommand(name: 'mf:show_view')] +class ShowViewGenerator extends BaseGenerator +{ + /** + * The name and signature of the console command. + * + * @var string + */ + protected $name = 'mf:show_view'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Generates the ShowView File for a table.'; + + /** + * The type of class being generated. + * + * @var string + */ + protected $type = 'ShowView'; + + /** + * Execute the console command. + */ + public function handle() + { + parent::handle(); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + return $this->resolveStubPath('/stubs/show.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->show_view_name($name); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getPath($name = null) + { + return str_replace(['Resources\\', '\\'], ['resources/', '/'], $this->getViewNamespace($this->getTableInput()).'show.blade.php'); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/View/ViewGenerator.php Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,105 @@ +<?php + +namespace Wizard\MagicForger\Generator\View; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Wizard\MagicForger\Generator\BaseGenerator; + +#[AsCommand(name: 'mf:view')] +class ViewGenerator extends BaseGenerator +{ + /** + * The name and signature of the console command. + * + * @var string + */ + protected $name = 'mf:view'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Generates the View File for a table.'; + + /** + * The type of class being generated. + * + * @var string + */ + protected $type = 'View'; + + /** + * Execute the console command. + */ + public function handle() + { + // First we need to ensure that the table exists, then we can + if (! $this->tableExists($this->getTableInput())) { + $this->components->error('The table: "'.$this->getTableInput().'" does not exist in the database.'); + + return false; + } + + if ($this->option('all')) { + $this->input->setOption('index_view', true); + $this->input->setOption('create_edit_view', true); + $this->input->setOption('show_view', true); + } + + if ($this->option('index_view')) { + $this->createIndexView(); + } + + if ($this->option('create_edit_view')) { + $this->createCreateEditView(); + } + + if ($this->option('show_view')) { + $this->createShowView(); + } + } + + /** + * Get the console command options. + */ + protected function getOptions(): array + { + return array_merge(parent::getOptions(), [ + ['all', 'a', InputOption::VALUE_NONE, 'Generate all views for the table.'], + ['index_view', 'i', InputOption::VALUE_NONE, 'Generate index view for the table.'], + ['create_edit_view', 'c', InputOption::VALUE_NONE, 'Generate create_edit view for the table.'], + ['show_view', 's', InputOption::VALUE_NONE, 'Generate show view for the table.'], + ]); + } + + /** + * Interact further with the user if they were prompted for missing arguments. + */ + protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output): void {} + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() {} + + protected function createIndexView() + { + $this->call('mf:index_view', ['table' => $this->getTableInput(), '--fresh' => $this->option('fresh')]); + } + + protected function createCreateEditView() + { + $this->call('mf:create_edit_view', ['table' => $this->getTableInput(), '--fresh' => $this->option('fresh')]); + } + + protected function createShowView() + { + $this->call('mf:show_view', ['table' => $this->getTableInput(), '--fresh' => $this->option('fresh')]); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/View/snippets/index/header.stub Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,1 @@ +<th scope="col" class="{{headerClass}}">{{header}}</th>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/View/snippets/index/value.stub Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,5 @@ +{ + name: '{{column_name}}', + label: '{{header}}', + class: '{{valueClass}}' +},
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/View/snippets/input/checkbox.stub Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,5 @@ +<x-form.checkbox + name="{{fieldName}}" + label="{{fieldLabel}}" + :checked="request('{{fieldName}}', $item?->{{fieldName}} ?? 'false')" +></x-form.checkbox>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/View/snippets/input/date.stub Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,6 @@ +<x-form.date + name="{{fieldName}}" + label="{{fieldLabel}}" + :value="request('{{fieldName}}', optional($item?->{{fieldName}} ?? null)->format('Y-m-d') ?? '')" + :required="{{required}}" +></x-form.date>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/View/snippets/input/select.stub Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,7 @@ +<x-form.select + name="{{fieldName}}" + label="{{fieldLabel}}" + :options="{{options}}" + :value="request('{{fieldName}}', $item?->{{fieldName}} ?? '')" + :required="{{required}}" +></x-form.select>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/View/snippets/input/text.stub Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,6 @@ +<x-form.text + name="{{fieldName}}" + label="{{fieldLabel}}" + :value="request('{{fieldName}}', $item?->{{fieldName}} ?? '')" + :required="{{required}}" +></x-form.text>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/View/snippets/input/textarea.stub Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,6 @@ +<x-form.textarea + name="{{fieldName}}" + label="{{fieldLabel}}" + :value="request('{{fieldName}}', $item?->{{fieldName}} ?? '')" + :required="{{required}}" +></x-form.textarea>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/View/stubs/create_edit.stub Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,30 @@ +<x-app-layout> + <x-slot name="header"> + <h2 class="fw-semibold fs-4 text-dark"> + {{ isset($item) ? 'Edit' : 'Create' }} {{ ucfirst('{{ modelVariable }}') }} + </h2> + </x-slot> + + <div class="py-5"> + <div class="container"> + <div class="bg-white shadow-sm rounded p-4"> + <form method="POST" action="{{ isset($item) ? route('{{ tableName }}.update', $item) : route('{{ tableName }}.store') }}"> + @csrf + @if (isset($item)) + @method('PUT') + @endif + + {{ fieldsInsertPoint }} + + <div class="d-flex justify-content-end"> + <a href="{{ route('{{ tableName }}.index') }}" + class="btn btn-secondary me-2">Back</a> + <button type="submit" class="btn btn-primary"> + {{ isset($item) ? 'Update' : 'Create' }} + </button> + </div> + </form> + </div> + </div> + </div> +</x-app-layout>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/View/stubs/index.stub Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,83 @@ +<x-app-layout> + <x-slot name="header"> + <h2 class="fw-semibold fs-4 text-dark"> + All {{ Str::plural(ucfirst('{{ modelVariable }}')) }} + </h2> + </x-slot> + + <div class="py-5"> + <div class="container"> + <div class="bg-white shadow-sm rounded p-4"> + <div class="d-flex align-items-center justify-content-between mb-3"> + <a href="{{ route('{{ tableName }}.create') }}" class="btn btn-primary"> + + New {{ ucfirst('{{ modelVariable }}') }} + </a> + </div> + <div id="my-table"></div> + </div> + </div> + </div> + + @include('includes.ServerTable') + @pushOnce('scripts') + <meta name="csrf-token" content="{{ csrf_token() }}"> + @endpushOnce + <script> + document.addEventListener('DOMContentLoaded', function() { + const el = document.getElementById('my-table'); + if (el) { + new ServerTable(el, { + endpoint: '/{{ tableName }}/get_data', + columns: [ + {{ columnInsertPoint }} + { + name: 'actions', + label: ' ', + render: function(row, col, i) { + let actions = ` + <div class="d-flex gap-2"> + <a href="{{ route('{{ tableName }}.show', 'PLACEHOLDER') }}" class="text-primary">View</a> + <a href="{{ route('{{ tableName }}.edit', 'PLACEHOLDER') }}" class="text-warning">Edit</a> + <form action="{{ route('{{ tableName }}.destroy', 'PLACEHOLDER') }}" method="POST" + class="d-inline"> + @csrf + @method('DELETE') + <button type="submit" class="btn btn-link text-danger p-0" + onclick="return confirm('Delete?')">Delete</button> + </form> + </div> + `; + actions = actions.split('PLACEHOLDER').join(row.id); + return actions; + } + } + ], + pageSize: 10, + initialSort: [{ + col: 'created_at', + dir: 'desc' + }], + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute( + 'content') + }, + skeleton: ` + <div class="st-table-container"> + <table class="table table-hover"> + <thead> + </thead> + <tbody> + <!-- Data rows will go here --> + </tbody> + </table> + <div class="st-controls"> + <span class="st-pagination"></span> + <span class="st-status"></span> + </div> + </div> + ` + }); + } + }); + </script> +</x-app-layout>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Generator/View/stubs/show.stub Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,26 @@ +<x-app-layout> + <x-slot name="header"> + <h2 class="fw-semibold fs-4 text-dark"> + {{ ucfirst('{{ modelVariable }}') }} Details + </h2> + </x-slot> + + <div class="py-5"> + <div class="container"> + <div class="bg-white shadow-sm rounded p-4"> + <div class="mb-3"> + @foreach($fields as $field) + <div class="mb-2"> + <span class="fw-semibold">{{ ucfirst($field) }}:</span> + <span class="ms-2">{{ $item->$field }}</span> + </div> + @endforeach + </div> + <div class="d-flex justify-content-end mt-4"> + <a href="{{ route('{{ tableName }}.edit', $item) }}" class="me-2 btn btn-warning text-white">Edit</a> + <a href="{{ route('{{ tableName }}.index') }}" class="btn btn-secondary">Back</a> + </div> + </div> + </div> + </div> +</x-app-layout>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Helpers/FileModifier.php Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,73 @@ +<?php + +namespace Wizard\MagicForger\Helpers; + +/* + * FileModifier + * + * A class that handles all file modifications + * + * General flow: + * Provide a file, a point to insert, and a value to insert. + * Insert the data. + * + * Replacements will consume the insert point. + * Inserts will maintain the insert point. + * + * */ +class FileModifier +{ + private $contents; + + public $file_path; + + public function __construct($file_path) + { + $this->get_file_contents($file_path); + $this->file_path = $file_path; + } + + public function get_file_contents($file_path): void + { + // TODO: there needs to be more/any error checking + $f = fopen($file_path, 'r'); + $this->contents = fread($f, filesize($file_path)); + fclose($f); + } + + public function write_to_path($file_path = null): void + { + $file_path = $file_path ?? $this->file_path; + + $f = fopen($file_path, 'w'); + fwrite($f, $this->contents); + fclose($f); + } + + /** + * Replaces the replacement point with the value in the current contents. + */ + public function replace($value, $replacement_point): void + { + $this->contents = str_replace($replacement_point, $value, $this->contents); + } + + /** + * Inserts the value above the insert point in the current contents. + */ + public function insert($value, $insert_point): void + { + // seperate on new lines into an array + $file_arr = explode("\n", $this->contents); + $temp_arr = []; + + foreach ($file_arr as $line) { + if (str_contains($line, $insert_point)) { + $temp_arr[] = $value; + } + $temp_arr[] = $line; + } + + $this->contents = implode("\n", $temp_arr); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Helpers/RelationshipNavigator.php Wed Sep 10 21:00:47 2025 -0400 @@ -0,0 +1,232 @@ +<?php + +namespace Wizard\MagicForger\Helpers; + +use Illuminate\Support\Facades\DB; + +/** + * Class RelationshipNavigator + * + * This class is responsible for navigating database table relationships, specifically + * identifying and categorizing 'belongsTo', 'hasMany', and 'hasManyThrough' relationships. + */ +class RelationshipNavigator +{ + /** + * Handles the retrieval and display of table relationships. + * + * @return void + */ + public static function handle() + { + $tables = DB::select('SHOW TABLES'); + $tableNames = array_map(fn($table) => current((array) $table), $tables); + + foreach ($tableNames as $table) { + echo "Table: $table \n"; + + $relations = self::getRelations($table); + echo "Relationships: \n"; + + foreach ($relations as $relation => $relatedTables) { + echo "$relation: \n"; + foreach ($relatedTables as $relatedTable) { + echo "\t"; + foreach ($relatedTable as $key => $value) { + if (is_array($value)) { + echo "\n\t\t" . implode("\n\t\t", array_map(fn($k, $v) => "$k: $v", array_keys($value), $value)); + } else { + echo "$key: $value "; + } + } + echo "\n"; + } + } + echo "\n --- \n"; + } + } + + /** + * Retrieves relationships of a specific table. + * + * @param string $table The table name. + * @return array An array containing 'belongsTo', 'hasMany', and 'hasManyThrough' relations. + */ + public static function getRelations($table) + { + $relations = [ + 'belongsTo' => [], + 'hasMany' => [], + 'hasManyThrough' => [], + ]; + + $foreignKeys = DB::select("SHOW KEYS FROM $table WHERE Key_name != 'PRIMARY'"); + $referencedTables = self::getAllReferencedTables($table); + + // Determine 'belongsTo' Relationships + foreach ($foreignKeys as $fk) { + $column = $fk->Column_name; + + // Skip certain columns + if (in_array($column, ['created_by', 'updated_by'])) { + continue; + } + + $referencedTable = $referencedTables[$column] ?? null; + + if ($referencedTable) { + $relations['belongsTo'][] = ['column' => $column, 'table' => $referencedTable->REFERENCED_TABLE_NAME]; + } + } + + // Determine 'hasMany' Relationships + if ($reverseRelation = self::findReverseRelation($table)) { + foreach ($reverseRelation as $relatedTable) { + $relations['hasMany'][] = $relatedTable; + } + } + + // Determine 'hasManyThrough' Relationships + if ($hasManyThroughRelations = self::findHasManyThroughRelations($table)) { + foreach ($hasManyThroughRelations as $relatedTable) { + $relations['hasManyThrough'][] = $relatedTable; + } + } + + return $relations; + } + + /** + * Retrieves all referenced tables for a given table. + * + * @param string $table The table name. + * @return array|null An associative array of referenced tables, keyed by column name. + */ + public static function getAllReferencedTables($table) + { + $results = DB::select(" + SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME + FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = DATABASE() + AND REFERENCED_TABLE_NAME IS NOT NULL + AND TABLE_NAME = ? + ", [$table]); + + return self::re_key_array($results, 'COLUMN_NAME') ?: null; + } + + /** + * Finds 'hasMany' inverse relationships for a given table. + * + * @param string $table The table name. + * @return array|null An array of related tables with column names. + */ + public static function findReverseRelation($table) + { + $relations = DB::select(" + SELECT TABLE_NAME, COLUMN_NAME + FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = DATABASE() + AND REFERENCED_TABLE_NAME = ? + AND REFERENCED_COLUMN_NAME = 'id' + AND COLUMN_NAME NOT IN ('created_by', 'updated_by') + ", [$table]); + + return array_map(fn($rel) => ['table' => $rel->TABLE_NAME, 'column' => $rel->COLUMN_NAME], $relations) ?: null; + } + + /** + * Finds 'hasManyThrough' relationships for a given table. + * + * @param string $table The table name. + * @return array An array of 'hasManyThrough' relationships. + */ + public static function findHasManyThroughRelations($table) + { + $relations = []; + $intermediaryTables = self::findReverseRelation($table); + + if ($intermediaryTables !== null) { + foreach ($intermediaryTables as $intermediary) { + if ($isPivot = self::isPivot($intermediary['table'])) { + $isPivot = current($isPivot); + + $potentialTables = array_keys($isPivot['tables']); + $externalTable = $potentialTables[0] === $table ? $isPivot['tables'][$potentialTables[1]] : $isPivot['tables'][$potentialTables[0]]; + $internalTable = $potentialTables[0] === $table ? $isPivot['tables'][$potentialTables[0]] : $isPivot['tables'][$potentialTables[1]]; + + $hasManyThrough = [ + 'table' => $externalTable['table_name'], + 'through' => [ + 'table' => $isPivot['table'], + 'external_column' => $externalTable['column'], + 'internal_column' => $internalTable['column'], + ], + ]; + $relations[] = $hasManyThrough; + } + } + } + + return $relations; + } + + /** + * Determines if a table is a pivot table. + * + * @param string $table The table name. + * @return array|null An array with pivot details or null if not a pivot. + */ + public static function isPivot($table) + { + $relations = []; + $pivotTables = DB::select(" + SELECT TABLE_NAME, TABLE_COMMENT + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = ? + AND TABLE_COMMENT != '' + ", [$table]); + + if (!is_null($pivotTables) && count($pivotTables) > 0) { + $ref = current($pivotTables); + $pivots = json_decode(str_replace('PIVOT:', '', $ref->TABLE_COMMENT), true); + $tables = []; + + if (count($pivots) > 0) { + $references = self::getAllReferencedTables($table); + $references = self::re_key_array($references, 'REFERENCED_TABLE_NAME'); + + foreach ($pivots as $key => $value) { + if ($refData = ($references[$value] ?? null)) { + $tables[$value] = [ + 'table_name' => $value, + 'column' => $refData->COLUMN_NAME, + ]; + } + } + } + $relations[] = ['table' => $ref->TABLE_NAME, 'tables' => $tables]; + } + + return !empty($relations) ? $relations : null; + } + + /** + * Re-keys an array of objects using a specific object's property. + * + * @param array $oldArray The original array of objects. + * @param string $key The key to re-index by. + * @return array The re-keyed array. + */ + public static function re_key_array($oldArray, $key) + { + $newArray = []; + if (count($oldArray) > 0) { + foreach ($oldArray as $array) { + $newArray[$array->$key] = $array; + } + } + return $newArray; + } +}
--- a/src/MagicForgerServiceProvider.php Sat Dec 02 10:20:32 2023 -0500 +++ b/src/MagicForgerServiceProvider.php Wed Sep 10 21:00:47 2025 -0400 @@ -1,15 +1,20 @@ <?php -namespace Wizzard\MagicForger; +namespace Wizard\MagicForger; use Illuminate\Support\ServiceProvider; -use Wizzard\MagicForger\Generator\Controller\ControllerGenerator; -use Wizzard\MagicForger\Generator\Generator; -use Wizzard\MagicForger\Generator\Model\ModelGenerator; -use Wizzard\MagicForger\Generator\Requests\RequestGenerator; -use Wizzard\MagicForger\Generator\Requests\StoreRequestGenerator; -use Wizzard\MagicForger\Generator\Requests\UpdateRequestGenerator; -use Wizzard\MagicForger\Generator\Route\RouteGenerator; +use Wizard\MagicForger\Generator\Controller\ControllerGenerator; +use Wizard\MagicForger\Generator\Generator; +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\View\ViewGenerator; +use Wizard\MagicForger\Generator\View\IndexViewGenerator; +use Wizard\MagicForger\Generator\View\CreateEditViewGenerator; +use Wizard\MagicForger\Generator\View\ShowViewGenerator; class MagicForgerServiceProvider extends ServiceProvider { @@ -24,9 +29,14 @@ ControllerGenerator::class, ModelGenerator::class, RequestGenerator::class, + FilterRequestGenerator::class, StoreRequestGenerator::class, UpdateRequestGenerator::class, RouteGenerator::class, + ViewGenerator::class, + IndexViewGenerator::class, + CreateEditViewGenerator::class, + ShowViewGenerator::class, ]); }
--- a/src/Replacer/Replacer.php Sat Dec 02 10:20:32 2023 -0500 +++ b/src/Replacer/Replacer.php Wed Sep 10 21:00:47 2025 -0400 @@ -1,6 +1,6 @@ <?php -namespace Wizzard\MagicForger\Replacer; +namespace Wizard\MagicForger\Replacer; use Illuminate\Support\Str; @@ -9,27 +9,22 @@ /** * Prefix and Suffix for controller. * Usage is up to the user. - * - * @var string */ - protected $controller_prefix = ''; + protected string $controller_prefix = ''; /** * Prefix and Suffix for controller. * Usage is up to the user. - * - * @var string */ - protected $controller_suffix = 'Controller'; + protected string $controller_suffix = 'Controller'; /** * Finds all places in a string that could be replaced. - * Returns an array of all potential replacements as they - * appear in the target. + * Returns an array of all potential replacements as they appear in the target. */ public function get_all_keywords(string $target): array { - // find all the matches to our expected syntax + // find all matches to our expected syntax $matches = []; preg_match_all('/{{[\sa-zA-Z\-_]+}}/', $target, $matches); // sort the array and return unique values @@ -38,46 +33,48 @@ return array_values(array_unique($matches[0])); } + /** + * Apply replacements to the target string. + */ public function apply_replacements(string $target): string { $inserts = $this->get_all_keywords($target); $available_replacements = $this->get_available_replacements(); - $target = str_replace( + return str_replace( array_keys($available_replacements), $available_replacements, $target ); - - return $target; } - public function get_available_replacements() + /** + * Get available replacements for string replacements. + */ + public function get_available_replacements(): array { $table_name = $this->getTableInput(); - $replacements = [ - '{{ class }}' => $this->getClassName($table_name), - '{{ controllerName }}' => $this->controller_name($table_name), - '{{ model }}' => $this->model_name($table_name), - '{{ modelVariable }}' => $this->model_variable($table_name), - '{{ namespace }}' => $this->{'get'.$this->type.'Namespace'}($table_name), - '{{ namespacedModel }}' => $this->getNamespacedModel($table_name), - '{{ requestUses }}' => $this->getRequestUses($table_name), - '{{ rootNamespace }}' => $this->getRootNamespace(), - '{{ storeRequest }}' => $this->store_request_name($table_name), - '{{ tableName }}' => $table_name, - '{{ updateRequest }}' => $this->update_request_name($table_name), + + return [ + '{{ class }}' => $this->getClassName($table_name), + '{{ controllerName }}' => $this->controller_name($table_name), + '{{ model }}' => $this->model_name($table_name), + '{{ modelVariable }}' => $this->model_variable($table_name), + '{{ namespace }}' => $this->{'get'.$this->type.'Namespace'}($table_name), + '{{ namespacedModel }}' => $this->getNamespacedModel($table_name), + '{{ requestUses }}' => $this->getRequestUses($table_name), + '{{ rootNamespace }}' => $this->getRootNamespace(), + '{{ storeRequest }}' => $this->store_request_name($table_name), + '{{ filterRequest }}' => $this->filter_request_name($table_name), + '{{ updateRequest }}' => $this->update_request_name($table_name), + '{{ tableName }}' => $table_name, ]; - - return $replacements; } - // ////////////////////////////////////////// - // Internals and Classes // - // ////////////////////////////////////////// + // Model and Controller Naming /** - * Model names are generated in uppercase first Camel case. + * Generate model name in Studly case. */ public function model_name(string $name): string { @@ -85,17 +82,15 @@ } /** - * Model variable is standardly just a singular version of the table name. + * Generate singular model variable name. */ public function model_variable(string $name): string { - /* return Str::singular($name); */ - return 'item'; + return Str::singular($name); } /** - * Controller names are generated in uppercase first Camel case - * and wrapped in the prefix and suffix. + * Generate controller name using prefix/suffix and studly case. */ public function controller_name(string $name): string { @@ -104,76 +99,192 @@ $this->controller_suffix; } + /** + * Generate the store request name. + */ public function store_request_name(string $name): string { return 'Store'.$this->model_name($name).'Request'; } + /** + * Generate the filter request name. + */ + public function filter_request_name(string $name): string + { + return 'Filter'.$this->model_name($name).'Request'; + } + + /** + * Generate the update request name. + */ public function update_request_name(string $name): string { return 'Update'.$this->model_name($name).'Request'; } - // ////////////////////////////////////////// - // Namespaces // - // ////////////////////////////////////////// + + /** + * Generate the index view name. + */ + public function index_view_name(string $name): string + { + return ''; + } + + /** + * Generate the create_edit view name. + */ + public function create_edit_view_name(string $name): string + { + return ''; + } - public function getRootNamespace() + /** + * Generate the show view name. + */ + public function show_view_name(string $name): string + { + return ''; + } + + + /** + * Generate route name in Studly case. + */ + public function routes_name(string $name): string + { + return Str::singular(Str::studly($name)); + } + + // Namespace Methods + // These methods handle the formation of various namespaces used within the replacements. + + /** + * Get the root namespace for the application. + */ + public function getRootNamespace(): string { return $this->laravel->getNamespace(); } - public function getModelNamespace(string $name = '') + /** + * Get the model namespace. + */ + public function getRouteNamespace(string $name = ''): string + { + return base_path() + . DIRECTORY_SEPARATOR . 'routes' + . DIRECTORY_SEPARATOR . 'resources' + ; + } + + /** + * Get the model namespace. + */ + public function getModelNamespace(string $name = ''): string { return $this->getRootNamespace().'Models'; } - public function getNamespacedModel(string $name = '') + /** + * Get the fully-qualified namespaced model class. + */ + public function getNamespacedModel(string $name = ''): string { return $this->getModelNamespace().'\\'.$this->model_name($name); } - public function getControllerNamespace(string $name = '') + /** + * Get the controller namespace. + */ + public function getControllerNamespace(string $name = ''): string { return $this->getRootNamespace().'Http\\Controllers'; } - public function getRequestNamespace(string $name) + /** + * Get the request namespace. + */ + public function getRequestNamespace(string $name): string { return $this->getRootNamespace().'Http\\Requests\\'.$this->model_name($name); } - public function getStoreRequestNamespace(string $name) + /** + * Get the store request namespace. + */ + public function getStoreRequestNamespace(string $name): string { return $this->getRequestNamespace($name); } - public function getUpdateRequestNamespace(string $name) + /** + * Get the filter request namespace. + */ + public function getFilterRequestNamespace(string $name): string + { + return $this->getRequestNamespace($name); + } + + /** + * Get the update request namespace. + */ + public function getUpdateRequestNamespace(string $name): string { return $this->getRequestNamespace($name); } - public function getRequestUses(string $name) + /** + * Get the view namespace. + */ + public function getViewNamespace(string $name): string + { + return $this->viewPath($name) . '\\'; + } + + /** + * Get the index view namespace. + */ + public function getIndexViewNamespace(string $name): string + { + return $this->getViewNamespace($name) . '\\'; + } + + /** + * Get the create_edit view namespace. + */ + public function getCreateEditViewNamespace(string $name): string + { + return $this->getViewNamespace($name) . '\\'; + } + + /** + * Get the show view namespace. + */ + public function getShowViewNamespace(string $name): string + { + return $this->getViewNamespace($name) . '\\'; + } + + + /** + * Get the request uses string for replacement. + */ + public function getRequestUses(string $name): string { return implode("\n", [ 'use '.$this->getRequestNamespace($name).'\\'.$this->store_request_name($name).';', + 'use '.$this->getRequestNamespace($name).'\\'.$this->filter_request_name($name).';', 'use '.$this->getRequestNamespace($name).'\\'.$this->update_request_name($name).';', ]); } - public function getRouteNamespace(string $name = '') - { - return $this->getRootNamespace().'Http\\Controllers'; - } - - // ////////////////////////////////////////// - // Language and Presentables // - // ////////////////////////////////////////// + // Text Manipulation /** - * Breaks up a string and makes it human readable. - * - * This function assumes that the inputted name is camel case + * Convert a string to a human-readable format. + * Assumes camel case input. */ public function human_readable(string $name): string { @@ -181,9 +292,8 @@ } /** - * Breaks up a string and makes it human readable and lowecase. - * - * This function assumes that the inputted name is camel case + * Convert a string to a lowercase human-readable format. + * Assumes camel case input. */ public function human_readable_lc(string $name): string {
--- a/src/Replacer/TableReplacer.php Sat Dec 02 10:20:32 2023 -0500 +++ b/src/Replacer/TableReplacer.php Wed Sep 10 21:00:47 2025 -0400 @@ -1,59 +1,144 @@ <?php -namespace Wizzard\MagicForger\Replacer; +namespace Wizard\MagicForger\Replacer; trait TableReplacer { - protected $columns; + protected ?array $columns = null; - protected function get_columns() + protected array $columns_to_ignore = [ + 'id', + 'created_at', + 'updated_at', + 'created_by', + 'updated_by', + 'deleted_at', + ]; + + /** + * Retrieve columns for the current table. + */ + protected function get_columns(): array { if (is_null($this->columns)) { - $this->columns = $this->getCurrentTable()->getColumns(); + $this->columns = $this->getTableColumns($this->getCurrentTable()); } return $this->columns; } - protected function get_attributes() + /** + * Get a string representation of values for creation. + */ + protected function getValuesForCreation(): string { + $insert = ''; + foreach ($this->get_columns() as $column) { + $column_name = $column['name']; + $insert .= sprintf('$item->%s = $validated["%s"] ?? NULL;', $column_name, $column_name)."\n"; + } + + return $insert; } - protected function getValuesForCreation() + /** + * Get a string representation of table attributes. + */ + protected function getCasts(): string { $insert = ''; foreach ($this->get_columns() as $column) { - $insert .= '$item->'.$column->getName().' = $validated["'.$column->getName().'"] ?? NULL;'."\n"; + 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"; + } + } + + return $insert; + } + + /** + * Get a string representation of table attributes. + */ + protected function getAttributes(): string + { + $insert = ''; + foreach ($this->get_columns() as $column) { + if (in_array($column['name'], $this->columns_to_ignore)) { + continue; + } + $insert .= sprintf("'%s' => '',", $column['name'])."\n"; } return $insert; } + /** + * Get a string representation of table fillable columns. + */ + protected function getFillable(): string + { + $insert = ''; + foreach ($this->get_columns() as $column) { + if (in_array($column['name'], $this->columns_to_ignore)) { + continue; + } + $insert .= sprintf("'%s',", $column['name'])."\n"; + } + + return $insert; + } + + /** + * Get formatted validation rules for table columns. + */ + protected function getValuesForValidation(): string + { + $insert = ''; + foreach ($this->get_columns() as $column) { + if (in_array($column['name'], $this->columns_to_ignore)) { + continue; + } + $insert .= sprintf("'%s' => 'nullable',", $column['name'])."\n"; + } + + return $insert; + } + + /** + * Apply insertions in the target template. + */ public function apply_inserts(string $target): string { $inserts = $this->get_all_keywords($target); - $available_replacements = $this->get_available_inserts(); + $available_insertions = $this->get_available_inserts(); - $target = str_replace( - array_keys($available_replacements), - $available_replacements, + return str_replace( + array_keys($available_insertions), + $available_insertions, $target ); - - return $target; } - public function get_available_inserts() + /** + * Get available insertion points for the template. + */ + public function get_available_inserts(): array { - $table_name = $this->getTableInput(); - $replacements = [ - '// {{ valuesForCreation }}' => self::getValuesForCreation(), + return [ + '// {{ valuesForCreation }}' => $this->getValuesForCreation(), + '# {{ attributeInsertPoint }}' => $this->getAttributes(), + '# {{ castInsertPoint }}' => $this->getCasts(), + '# {{ fillableInsertPoint }}' => $this->getFillable(), + '// {{ valuesForValidation }}' => $this->getValuesForValidation(), ]; - - foreach ($replacements as $key => &$replacement) { - $replacement = $replacement."\n".$key; - } - - return $replacements; } }
