changeset 23:827efbf4d73c main-dev

Huge changes to the relationships for models and more complex
author Luka Sitas <sitas.luka.97@gmail.com>
date Fri, 11 Apr 2025 20:50:20 -0400
parents ee8ef14e158d
children 31109c61ce02
files .hgignore .vimrc src/ConfigHelper.php src/Generator/BaseGenerator.php src/Generator/Controller/ControllerGenerator.php src/Generator/Controller/stubs/controller.stub src/Generator/Generator.php src/Generator/Model/ModelGenerator.php src/Generator/Model/snippets/belongs_to_many_relation.stub src/Generator/Model/snippets/belongs_to_relation.stub src/Generator/Model/snippets/has_many_relation.stub src/Generator/Model/stubs/model.pivot.stub src/Generator/Model/stubs/model.stub src/Generator/Requests/RequestGenerator.php src/Generator/Requests/StoreRequestGenerator.php src/Generator/Requests/UpdateRequestGenerator.php src/Generator/Route/RouteGenerator.php src/Helpers/RelationshipNavigator.php src/Replacer/Replacer.php src/Replacer/TableReplacer.php
diffstat 20 files changed, 467 insertions(+), 197 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Wed Feb 26 20:12:17 2025 -0500
+++ b/.hgignore	Fri Apr 11 20:50:20 2025 -0400
@@ -5,4 +5,5 @@
 *.env.production
 .php-cs-fixer.cache
 *.aichat
+tags
 
--- a/.vimrc	Wed Feb 26 20:12:17 2025 -0500
+++ b/.vimrc	Fri Apr 11 20:50:20 2025 -0400
@@ -28,30 +28,6 @@
 " 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()
--- a/src/ConfigHelper.php	Wed Feb 26 20:12:17 2025 -0500
+++ b/src/ConfigHelper.php	Fri Apr 11 20:50:20 2025 -0400
@@ -69,8 +69,7 @@
      *
      * @see https://www.php.net/manual/en/function.var-export.php
      *
-     * @param bool $return
-     *
+     * @param  bool  $return
      * @return string|string[]|null
      */
     public static function varexport(mixed $expression, $return = false): string|array|null
@@ -125,9 +124,9 @@
             $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],
-                          ];
+                    'foreign_table' => $fk->getForeignTableName(),
+                    'foreign_column' => $fk->getForeignColumns()[0],
+                ];
             }
 
             foreach ($table_columns as $column) {
@@ -136,14 +135,14 @@
                 $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) {
@@ -152,9 +151,9 @@
                     foreach ($foreign_keys_list as $fk) {
                         if ($fk->getForeignTableName() == $table) {
                             $foreign_keys_reverse[] = [
-                                               'table' => $other_table,
-                                               'column' => $fk->getLocalColumns()[0],
-                                          ];
+                                'table' => $other_table,
+                                'column' => $fk->getLocalColumns()[0],
+                            ];
                         }
                     }
                 }
@@ -174,14 +173,14 @@
     /**
      * 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): 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	Wed Feb 26 20:12:17 2025 -0500
+++ b/src/Generator/BaseGenerator.php	Fri Apr 11 20:50:20 2025 -0400
@@ -3,27 +3,30 @@
 namespace Wizard\MagicForger\Generator;
 
 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 Wizard\MagicForger\Replacer\Replacer;
 use Wizard\MagicForger\Replacer\TableReplacer;
-use Illuminate\Support\Facades\DB;
-use InvalidArgumentException;
 
 abstract class BaseGenerator extends GeneratorCommand
 {
     use Replacer;
     use TableReplacer;
 
-    protected string $schema;
-    protected array $tables;
-    protected $currentTable;
+    protected $schema = null;
+
+    protected $tables = null;
+
+    protected $currentTable = null;
 
     public function handle()
     {
-        if (!$this->tableExists($this->getTableInput())) {
-            $this->components->error('The table: "' . $this->getTableInput() . '" does not exist in the database.');
+
+        if (! $this->tableExists($this->getTableInput())) {
+            $this->components->error('The table: "'.$this->getTableInput().'" does not exist in the database.');
+
             return false;
         }
 
@@ -44,7 +47,7 @@
         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()
@@ -91,7 +94,7 @@
 
     protected function getFile($name): string
     {
-        if (!($this->hasOption('fresh') && $this->option('fresh')) && $this->fileExists($name)) {
+        if (! ($this->hasOption('fresh') && $this->option('fresh')) && $this->fileExists($name)) {
             return $this->files->get($name);
         }
 
@@ -116,26 +119,60 @@
     protected function getTables(): array
     {
         if (is_null($this->tables)) {
-            $this->tables = DB::connection()->getDoctrineSchemaManager()->listTableNames();
+            $this->tables = Schema::getTableListing(schema: config('database.connections.mariadb.database'), schemaQualified: false);
         }
 
         return $this->tables;
     }
-    
-    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;
@@ -143,11 +180,11 @@
 
     protected function setCurrentTable(string $table_name): void
     {
-        $this->currentTable = !empty(trim($table_name)) ? $this->getTable($table_name) : null;
+        $this->currentTable = $table_name;
     }
 
     protected function format_file(string $path): void
     {
-        exec('php-cs-fixer fix ' . escapeshellarg($path));
+        exec('./vendor/bin/pint '.escapeshellarg($path));
     }
 }
--- a/src/Generator/Controller/ControllerGenerator.php	Wed Feb 26 20:12:17 2025 -0500
+++ b/src/Generator/Controller/ControllerGenerator.php	Fri Apr 11 20:50:20 2025 -0400
@@ -31,8 +31,6 @@
 
     /**
      * Execute the console command.
-     *
-     * @return void
      */
     public function handle(): void
     {
@@ -41,8 +39,6 @@
 
     /**
      * Get the stub file for the generator.
-     *
-     * @return string
      */
     protected function getStub(): string
     {
@@ -51,15 +47,12 @@
 
     /**
      * Resolve the fully-qualified path to the stub.
-     *
-     * @param string $stub
-     * @return string
      */
     protected function resolveStubPath(string $stub): string
     {
         $customPath = $this->laravel->basePath(trim($stub, '/'));
 
-        return is_file($customPath) ? $customPath : __DIR__ . $stub;
+        return is_file($customPath) ? $customPath : __DIR__.$stub;
     }
 
     /**
@@ -70,15 +63,12 @@
         return str_replace(
             ['App\\', '\\'],
             ['app/', '/'],
-            $this->getControllerNamespace() . '/' . $this->controller_name($this->getTableInput()) . '.php'
+            $this->getControllerNamespace().'/'.$this->controller_name($this->getTableInput()).'.php'
         );
     }
 
     /**
      * Get the class name for the controller.
-     *
-     * @param string $name
-     * @return string
      */
     protected function getClassName(string $name): string
     {
--- a/src/Generator/Controller/stubs/controller.stub	Wed Feb 26 20:12:17 2025 -0500
+++ b/src/Generator/Controller/stubs/controller.stub	Fri Apr 11 20:50:20 2025 -0400
@@ -43,8 +43,7 @@
     {
         $validated = $request->validated();
 
-        ${{ modelVariable }} = new {{ model }}($validated);
-        ${{ modelVariable }}->save();
+        {{ model }}::create($validated);
 
         return redirect()->route('{{ tableName }}.index');
     }
@@ -90,8 +89,7 @@
     {
         $validated = $request->validated();
 
-        ${{ modelVariable }}->map_values($validated);
-        ${{ modelVariable }}->save();
+        ${{ modelVariable }}->update($validated);
 
         return redirect()->route('{{ tableName }}.index');
     }
--- a/src/Generator/Generator.php	Wed Feb 26 20:12:17 2025 -0500
+++ b/src/Generator/Generator.php	Fri Apr 11 20:50:20 2025 -0400
@@ -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;
@@ -80,8 +81,6 @@
 
     /**
      * Get the console command options.
-     *
-     * @return array
      */
     protected function getOptions(): array
     {
@@ -96,16 +95,10 @@
 
     /**
      * Interact further with the user if they were prompted for missing arguments.
-     *
-     * @return void
      */
-    protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output):void
-    {
-    }
+    protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output): void {}
 
-    protected function getStub():void
-    {
-    }
+    protected function getStub(): void {}
 
     protected function createController(): void
     {
--- a/src/Generator/Model/ModelGenerator.php	Wed Feb 26 20:12:17 2025 -0500
+++ b/src/Generator/Model/ModelGenerator.php	Fri Apr 11 20:50:20 2025 -0400
@@ -4,6 +4,7 @@
 
 use Symfony\Component\Console\Attribute\AsCommand;
 use Wizard\MagicForger\Generator\BaseGenerator;
+use Wizard\MagicForger\Helpers\RelationshipNavigator;
 
 #[AsCommand(name: 'mf:model')]
 class ModelGenerator extends BaseGenerator
@@ -29,12 +30,19 @@
      */
     protected $type = 'Model';
 
+    protected static $cached_snippets = [];
+
     /**
      * Execute the console command.
      */
     public function handle()
     {
-        parent::handle();
+
+        $belongs_to_many_relations = RelationshipNavigator::getRelations($this->getTableInput());
+				dd($belongs_to_many_relations);
+        echo $this->renderBelongsToMany($belongs_to_many_relations['hasManyThrough'][0]);
+        dd('here');
+        /* parent::handle(); */
     }
 
     /**
@@ -44,14 +52,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 +86,77 @@
     {
         return str_replace(['App\\', '\\'], ['app/', '/'], $this->getModelNamespace().'/'.$this->model_name($this->getTableInput()).'.php');
     }
+
+    protected function getSnippet($snippet_name)
+    {
+        if (! isset($cached_snippets[$snippet_name])) {
+					$cached_snippets[$snippet_name] = $this->files->get(
+						$this->resolveStubPath("/snippets/$snippet_name.stub"));
+        }
+
+        return $cached_snippets[$snippet_name];
+    }
+
+		protected function gatherRelations() {
+			$relations = RelationshipNavigator::getRelations($this->getCurrentTable());
+
+			return renderRelations($relations);
+			
+		}
+
+		protected function renderRelations($relations) {
+			$renders = [
+				'belongsTo' => [],
+				'hasMany' => [],
+				'belongsToMany' => [],
+			];
+			foreach($relations['belongsTo'] as $belongs_to_relation) {
+				$renders['belongsTo'] = $this->renderBelongsTo($belongs_to_relation);
+			}
+
+			foreach($relations['hasMany'] as $has_many_relation) {
+				$renders['hasMany'] = $this->renderHasMany($has_many_relation);
+			}
+
+			foreach($relations['belongsToMany'] as $belongs_to_many_relation) {
+				$renders['belongsToMany'] = $this->renderBelongsToMany($belongs_to_many_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}}', '{{pivotTable}}', '{{foreignPivotKey}}', '{{relatedPivotKey}}'],
+            [$relationName, $relatedModel, $pivotTable, $foreignPivotKey, $relatedPivotKey],
+            $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;
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Generator/Model/snippets/belongs_to_many_relation.stub	Fri Apr 11 20:50:20 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	Fri Apr 11 20:50:20 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/has_many_relation.stub	Fri Apr 11 20:50:20 2025 -0400
@@ -0,0 +1,4 @@
+public function {{ $relationhip_name }}()
+{
+    return $this->hasMany({{ external_class_name }}::class, '{{ column_name }}');
+}
--- a/src/Generator/Model/stubs/model.pivot.stub	Wed Feb 26 20:12:17 2025 -0500
+++ b/src/Generator/Model/stubs/model.pivot.stub	Fri Apr 11 20:50:20 2025 -0400
@@ -10,12 +10,22 @@
 
     /**
      * 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;
 
 
+		//relations
+
+		// BelongsTo
+		# {{ belongs_to_relationships }}
+
+		// HasMany
+		# {{ has_may_relationships }}
+		
+    // HasManyThrough
+		# {{ has_many_through_relationships }}
 
 }
--- a/src/Generator/Model/stubs/model.stub	Wed Feb 26 20:12:17 2025 -0500
+++ b/src/Generator/Model/stubs/model.stub	Fri Apr 11 20:50:20 2025 -0400
@@ -27,6 +27,10 @@
 			# {{ atributeInsertPoint }}
     ];
 
+		protected $fillable =[
+			# {{ fillableInsertPoint }}
+		];
+
 
 		public static function boot() : void {
 			parent::boot();
@@ -45,8 +49,12 @@
 		//relations
 
 		// BelongsTo
+		# {{ belongs_to_relationships }}
 
 		// HasMany
+		# {{ has_may_relationships }}
 		
+    // HasManyThrough
+		# {{ has_many_through_relationships }}
 
 }
--- a/src/Generator/Requests/RequestGenerator.php	Wed Feb 26 20:12:17 2025 -0500
+++ b/src/Generator/Requests/RequestGenerator.php	Fri Apr 11 20:50:20 2025 -0400
@@ -38,7 +38,7 @@
     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;
@@ -60,10 +60,8 @@
 
     /**
      * Get the console command options.
-     *
-     * @return array
      */
-    protected function getOptions():array
+    protected function getOptions(): array
     {
         return array_merge(parent::getOptions(), [
             ['all', 'a', InputOption::VALUE_NONE, 'Generate all request classes for the table.'],
@@ -74,21 +72,15 @@
 
     /**
      * Interact further with the user if they were prompted for missing arguments.
-     *
-     * @return void
      */
-    protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output):void
-    {
-    }
+    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 createStoreRequest()
     {
--- a/src/Generator/Requests/StoreRequestGenerator.php	Wed Feb 26 20:12:17 2025 -0500
+++ b/src/Generator/Requests/StoreRequestGenerator.php	Fri Apr 11 20:50:20 2025 -0400
@@ -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	Wed Feb 26 20:12:17 2025 -0500
+++ b/src/Generator/Requests/UpdateRequestGenerator.php	Fri Apr 11 20:50:20 2025 -0400
@@ -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/RouteGenerator.php	Wed Feb 26 20:12:17 2025 -0500
+++ b/src/Generator/Route/RouteGenerator.php	Fri Apr 11 20:50:20 2025 -0400
@@ -50,8 +50,7 @@
     /**
      * Resolve the fully-qualified path to the stub.
      *
-     * @param string $stub
-     *
+     * @param  string  $stub
      * @return string
      */
     protected function resolveStubPath($stub)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Helpers/RelationshipNavigator.php	Fri Apr 11 20:50:20 2025 -0400
@@ -0,0 +1,205 @@
+<?php
+
+namespace Wizard\MagicForger\Helpers;
+
+use Illuminate\Support\Facades\DB;
+
+class RelationshipNavigator
+{
+    public static function handle()
+    {
+        $tables = DB::select('SHOW TABLES');
+        $tableNames = array_map(function ($table) {
+            return current((array) $table);
+        }, $tables);
+
+        foreach ($tableNames as $table) {
+            echo "Table: $table \n";
+
+            $relations = self::getRelations($table);
+            echo "Relationships: \n";
+            foreach ($relations as $relation => $related_tables) {
+                echo $relation.": \n";
+                foreach ($related_tables as $related_table) {
+                    echo "\t";
+                    foreach ($related_table as $key => $value) {
+                        echo "$key : ";
+                        if (is_array($value)) {
+                            foreach ($related_table as $key => $value) {
+                                echo "\n\t\t";
+                                echo "$key: $value";
+                            }
+                        } else {
+                            echo $value.' ';
+                        }
+                    }
+                    echo "\n";
+                }
+            }
+            echo "\n --- \n";
+        }
+    }
+
+    public static function getRelations($table)
+    {
+        $relations = [
+            'belongsTo' => [],
+            'hasMany' => [],
+            'hasManyThrough' => [],
+        ];
+
+        $foreignKeys = DB::select("SHOW KEYS FROM $table WHERE Key_name != 'PRIMARY'");
+        $referencedTables = self::getAllReferencedTables($table);
+
+        // BelongsTo Relationships
+        foreach ($foreignKeys as $fk) {
+            $column = $fk->Column_name;
+
+            // skip created and updated by
+            if (in_array($column, ['created_by', 'updated_by'])) {
+                continue;
+            }
+
+            $referencedTable = $referencedTables[$column] ?? null;
+
+            if ($referencedTable) {
+                $relations['belongsTo'][] = ['column' => $column, 'table' => $referencedTable->REFERENCED_TABLE_NAME];
+            }
+
+        }
+
+        // HasMany Relationships
+        if ($reverseRelation = self::findReverseRelation($table)) {
+            foreach ($reverseRelation as $relatedTable) {
+                $relations['hasMany'][] = $relatedTable;
+            }
+        }
+
+        // HasManyThrough Relationships
+        if ($hasManyThroughRelations = self::findHasManyThroughRelations($table)) {
+            foreach ($hasManyThroughRelations as $relatedTable) {
+                $relations['hasManyThrough'][] = $relatedTable;
+            }
+        }
+
+        return $relations;
+    }
+
+    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 TABLE_NAME = '$table' 
+        ");
+
+        $tables = self::re_key_array($results, 'COLUMN_NAME');
+
+        return (count($tables) > 0) ? $tables : null;
+    }
+
+    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 = '$table' 
+            AND REFERENCED_COLUMN_NAME = 'id'
+					  AND COLUMN_NAME != 'created_by'
+					  AND COLUMN_NAME != 'updated_by'
+        ");
+
+        $relatedTables = array_map(function ($rel) {
+            return ['table' => $rel->TABLE_NAME, 'column' => $rel->COLUMN_NAME];
+        }, $relations);
+
+        return $relatedTables ? $relatedTables : null;
+    }
+
+    public static function findHasManyThroughRelations($table)
+    {
+        $relations = [];
+
+        // Find potential intermediary tables
+        $intermediaryTables = self::findReverseRelation($table);
+
+        if (! is_null($intermediaryTables)) {
+
+            foreach ($intermediaryTables as $intermediary) {
+
+                if ($is_pivot = self::isPivot($intermediary['table'])) {
+                    $is_pivot = current($is_pivot);
+
+                    // reformat the table based on the current and external
+										$potential_tables = array_keys($is_pivot['tables']);
+										$external_table = $potential_tables[0] == $table ? $is_pivot['tables'][$potential_tables[1]] : $is_pivot['tables'][$potential_tables[0]];
+										$internal_table = $potential_tables[0] == $table ? $is_pivot['tables'][$potential_tables[0]] : $is_pivot['tables'][$potential_tables[1]];
+
+										$hasManyThrough = [
+											'table' => $external_table['table_name'],
+											'through' => [
+												'table' => $is_pivot['table'],
+												'external_column' => $external_table['column'],
+												'internal_column' => $internal_table['column']
+											]
+                    ];
+                    $relations[] = $hasManyThrough;
+                }
+            }
+
+        }
+
+        return $relations;
+    }
+
+    public static function isPivot($table)
+    {
+
+        $relations = [];
+        // TODO: alsot get the columns that are relevant
+        $pivotTables = DB::select("
+                SELECT TABLE_NAME, TABLE_COMMENT
+                FROM INFORMATION_SCHEMA.TABLES 
+                WHERE TABLE_SCHEMA = DATABASE() 
+                AND TABLE_NAME = '{$table}'
+								AND TABLE_COMMENT != ''
+            ");
+
+				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) {
+							//re-key array
+        	    $references = self::getAllReferencedTables($table);
+							$references = self::re_key_array($references, 'REFERENCED_TABLE_NAME');
+
+							foreach($pivots as $key => $value) {
+								if($ref_data = ($references[$value] ?? null)) {
+									$tables[$value] = [
+										'table_name' => $value,
+										'column' => $ref_data->COLUMN_NAME
+									];
+								}
+							}
+        	}
+        	$relations[] = ['table' => $ref->TABLE_NAME, 'tables' => $tables];
+				}
+
+        return $relations != [] ? $relations : null;
+    }
+
+		public static function re_key_array($old_array, $key) {
+			$new_array = [];
+      if (count($old_array) > 0) {
+          foreach ($old_array as $array) {
+              $new_array[$array->$key] = $array;
+          }
+      }
+			return $new_array;
+		}
+}
--- a/src/Replacer/Replacer.php	Wed Feb 26 20:12:17 2025 -0500
+++ b/src/Replacer/Replacer.php	Fri Apr 11 20:50:20 2025 -0400
@@ -9,25 +9,18 @@
     /**
      * Prefix and Suffix for controller.
      * Usage is up to the user.
-     *
-     * @var string
      */
     protected string $controller_prefix = '';
 
     /**
      * Prefix and Suffix for controller.
      * Usage is up to the user.
-     *
-     * @var string
      */
     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.
-     *
-     * @param string $target
-     * @return array
      */
     public function get_all_keywords(string $target): array
     {
@@ -42,9 +35,6 @@
 
     /**
      * Apply replacements to the target string.
-     *
-     * @param string $target
-     * @return string
      */
     public function apply_replacements(string $target): string
     {
@@ -60,8 +50,6 @@
 
     /**
      * Get available replacements for string replacements.
-     *
-     * @return array
      */
     public function get_available_replacements(): array
     {
@@ -72,7 +60,7 @@
             '{{ 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),
+            '{{ namespace }}' => $this->{'get'.$this->type.'Namespace'}($table_name),
             '{{ namespacedModel }}' => $this->getNamespacedModel($table_name),
             '{{ requestUses }}' => $this->getRequestUses($table_name),
             '{{ rootNamespace }}' => $this->getRootNamespace(),
@@ -86,9 +74,6 @@
 
     /**
      * Generate model name in Studly case.
-     *
-     * @param string $name
-     * @return string
      */
     public function model_name(string $name): string
     {
@@ -97,9 +82,6 @@
 
     /**
      * Generate singular model variable name.
-     *
-     * @param string $name
-     * @return string
      */
     public function model_variable(string $name): string
     {
@@ -108,37 +90,28 @@
 
     /**
      * Generate controller name using prefix/suffix and studly case.
-     *
-     * @param string $name
-     * @return string
      */
     public function controller_name(string $name): string
     {
-        return $this->controller_prefix .
-            $this->model_name($name) .
+        return $this->controller_prefix.
+            $this->model_name($name).
             $this->controller_suffix;
     }
 
     /**
      * Generate the store request name.
-     *
-     * @param string $name
-     * @return string
      */
     public function store_request_name(string $name): string
     {
-        return 'Store' . $this->model_name($name) . 'Request';
+        return 'Store'.$this->model_name($name).'Request';
     }
 
     /**
      * Generate the update request name.
-     *
-     * @param string $name
-     * @return string
      */
     public function update_request_name(string $name): string
     {
-        return 'Update' . $this->model_name($name) . 'Request';
+        return 'Update'.$this->model_name($name).'Request';
     }
 
     // Namespace Methods
@@ -146,8 +119,6 @@
 
     /**
      * Get the root namespace for the application.
-     *
-     * @return string
      */
     public function getRootNamespace(): string
     {
@@ -156,53 +127,38 @@
 
     /**
      * Get the model namespace.
-     *
-     * @param string $name
-     * @return string
      */
     public function getModelNamespace(string $name = ''): string
     {
-        return $this->getRootNamespace() . 'Models';
+        return $this->getRootNamespace().'Models';
     }
 
     /**
      * Get the fully-qualified namespaced model class.
-     *
-     * @param string $name
-     * @return string
      */
     public function getNamespacedModel(string $name = ''): string
     {
-        return $this->getModelNamespace() . '\\' . $this->model_name($name);
+        return $this->getModelNamespace().'\\'.$this->model_name($name);
     }
 
     /**
      * Get the controller namespace.
-     *
-     * @param string $name
-     * @return string
      */
     public function getControllerNamespace(string $name = ''): string
     {
-        return $this->getRootNamespace() . 'Http\\Controllers';
+        return $this->getRootNamespace().'Http\\Controllers';
     }
 
     /**
      * Get the request namespace.
-     *
-     * @param string $name
-     * @return string
      */
     public function getRequestNamespace(string $name): string
     {
-        return $this->getRootNamespace() . 'Http\\Requests\\' . $this->model_name($name);
+        return $this->getRootNamespace().'Http\\Requests\\'.$this->model_name($name);
     }
 
     /**
      * Get the store request namespace.
-     *
-     * @param string $name
-     * @return string
      */
     public function getStoreRequestNamespace(string $name): string
     {
@@ -211,9 +167,6 @@
 
     /**
      * Get the update request namespace.
-     *
-     * @param string $name
-     * @return string
      */
     public function getUpdateRequestNamespace(string $name): string
     {
@@ -222,15 +175,12 @@
 
     /**
      * Get the request uses string for replacement.
-     *
-     * @param string $name
-     * @return string
      */
     public function getRequestUses(string $name): string
     {
         return implode("\n", [
-            'use ' . $this->getRequestNamespace($name) . '\\' . $this->store_request_name($name) . ';',
-            'use ' . $this->getRequestNamespace($name) . '\\' . $this->update_request_name($name) . ';',
+            'use '.$this->getRequestNamespace($name).'\\'.$this->store_request_name($name).';',
+            'use '.$this->getRequestNamespace($name).'\\'.$this->update_request_name($name).';',
         ]);
     }
 
@@ -239,9 +189,6 @@
     /**
      * Convert a string to a human-readable format.
      * Assumes camel case input.
-     *
-     * @param string $name
-     * @return string
      */
     public function human_readable(string $name): string
     {
@@ -251,9 +198,6 @@
     /**
      * Convert a string to a lowercase human-readable format.
      * Assumes camel case input.
-     *
-     * @param string $name
-     * @return string
      */
     public function human_readable_lc(string $name): string
     {
--- a/src/Replacer/TableReplacer.php	Wed Feb 26 20:12:17 2025 -0500
+++ b/src/Replacer/TableReplacer.php	Fri Apr 11 20:50:20 2025 -0400
@@ -6,15 +6,22 @@
 {
     protected ?array $columns = null;
 
+    protected array $columns_to_ignore = [
+        'id',
+        'created_at',
+        'updated_at',
+        'created_by',
+        'updated_by',
+        'deleted_at',
+    ];
+
     /**
      * Retrieve columns for the current table.
-     *
-     * @return array
      */
     protected function get_columns(): array
     {
         if (is_null($this->columns)) {
-            $this->columns = $this->getCurrentTable()->getColumns();
+            $this->columns = $this->getTableColumns($this->getCurrentTable());
         }
 
         return $this->columns;
@@ -22,14 +29,13 @@
 
     /**
      * Get a string representation of values for creation.
-     *
-     * @return string
      */
     protected function getValuesForCreation(): string
     {
         $insert = '';
         foreach ($this->get_columns() as $column) {
-            $insert .= sprintf('$item->%s = $validated["%s"] ?? NULL;', $column->getName(), $column->getName()) . "\n";
+            $column_name = $column['name'];
+            $insert .= sprintf('$item->%s = $validated["%s"] ?? NULL;', $column_name, $column_name)."\n";
         }
 
         return $insert;
@@ -37,14 +43,31 @@
 
     /**
      * Get a string representation of table attributes.
-     *
-     * @return string
      */
     protected function getAttributes(): string
     {
         $insert = '';
         foreach ($this->get_columns() as $column) {
-            $insert .= sprintf("'%s' => '',", $column->getName()) . "\n";
+            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;
@@ -52,14 +75,15 @@
 
     /**
      * Get formatted validation rules for table columns.
-     *
-     * @return string
      */
     protected function getValuesForValidation(): string
     {
         $insert = '';
         foreach ($this->get_columns() as $column) {
-            $insert .= sprintf("'%s' => 'nullable',", $column->getName()) . "\n";
+            if (in_array($column['name'], $this->columns_to_ignore)) {
+                continue;
+            }
+            $insert .= sprintf("'%s' => 'nullable',", $column['name'])."\n";
         }
 
         return $insert;
@@ -67,9 +91,6 @@
 
     /**
      * Apply insertions in the target template.
-     *
-     * @param string $target
-     * @return string
      */
     public function apply_inserts(string $target): string
     {
@@ -85,14 +106,13 @@
 
     /**
      * Get available insertion points for the template.
-     *
-     * @return array
      */
     public function get_available_inserts(): array
     {
         return [
             '// {{ valuesForCreation }}' => $this->getValuesForCreation(),
             '# {{ attributeInsertPoint }}' => $this->getAttributes(),
+            '# {{ fillableInsertPoint }}' => $this->getFillable(),
             '// {{ valuesForValidation }}' => $this->getValuesForValidation(),
         ];
     }