From 9db9465fcaf56e192c6d143bfad1bea409517a81 Mon Sep 17 00:00:00 2001 From: David Rogers Date: Wed, 13 Oct 2021 06:47:41 -0700 Subject: [PATCH 1/2] must go faster --- README.md | 13 --- config/modelfromtable.php | 2 - src/Commands/ModelFromTableCommand.php | 136 +++++++++++-------------- 3 files changed, 62 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 7150131..bc27cdf 100644 --- a/README.md +++ b/README.md @@ -99,19 +99,6 @@ php artisan generate:modelfromtable --table=user --folder=app\Models ## Configuration file for saving defaults, dynamic lambdas A [config file](https://github.com/laracademy/generators/blob/master/config/modelfromtable.php) should be in your project's config folder (if not, you can easily create it). Through this, you can set defaults you commonly use to cut down on the input your command line call requires. Some fields, like `namespace`, accept a static value or, more powerfully, a lambda to generate dynamic values. Additional fields not available to the CLI are available in the config. See below. -### Primary Key, using lamba (config only) -Some apps do not use the Laravel default of `id`, so say your table name prefixes the primary key... -```php -'primaryKey' => fn(string $tableName) => "{$tableName}_id", -``` -Some legacy databases do not conform to a standard, so a more powerful example could be querying the primary key column directly... -```php -'primaryKey' => (function (string $tableName) { - $primaryKey = collect(DB::select(DB::raw("SHOW KEYS FROM {$tableName} WHERE Key_name = 'PRIMARY'")))->first(); - return ($primaryKey) ? $primaryKey->Column_name : false; -}), -``` - ### Whitelist/Blacklist (config only) Particularly large databases often have a number of tables that aren't meant to have models. These can easily be filtered through either the whitelist or blacklist (or both!). Laravel's "migrations" table is already included in the blacklist. One nice feature is that you can wildcard table names if that makes sense for your situation... ```php diff --git a/config/modelfromtable.php b/config/modelfromtable.php index 13447c0..b8a46b4 100644 --- a/config/modelfromtable.php +++ b/config/modelfromtable.php @@ -20,8 +20,6 @@ 'table' => '', - 'primaryKey' => 'id', - 'folder' => '', 'filename' => '', diff --git a/src/Commands/ModelFromTableCommand.php b/src/Commands/ModelFromTableCommand.php index a961302..fa1585c 100644 --- a/src/Commands/ModelFromTableCommand.php +++ b/src/Commands/ModelFromTableCommand.php @@ -15,6 +15,7 @@ class ModelFromTableCommand extends Command */ protected $signature = 'generate:modelfromtable {--table= : a single table or a list of tables separated by a comma (,)} + {--schema= : what schema to use} {--connection= : database connection to use, leave off and it will use the .env connection} {--debug= : turns on debugging} {--folder= : by default models are stored in app, but you can change that} @@ -34,9 +35,13 @@ class ModelFromTableCommand extends Command private $db; private $options; + private $startTime; private $delimiter; private $stubConnection; + private $modelPath; + private $modelStub; + /** * Create a new command instance. * @@ -44,13 +49,18 @@ class ModelFromTableCommand extends Command */ public function __construct() { + $this->startTime = microtime(true); + parent::__construct(); + $this->modelPath = (app()->version() > '8')? app()->path('Models') : app()->path(); + $this->options = [ 'connection' => '', 'namespace' => '', 'table' => '', - 'folder' => $this->getModelPath(), + 'schema' => '', + 'folder' => $this->modelPath, 'filename' => '', 'debug' => false, 'singular' => false, @@ -58,6 +68,8 @@ public function __construct() ]; $this->delimiter = config('modelfromtable.delimiter', ', '); + + $this->modelStub = file_get_contents($this->getStub()); } /** @@ -81,7 +93,7 @@ public function handle() // figure out if we need to create a folder or not // NOTE: lambas will need to handle this themselves - if (!is_callable($path) && $path != $this->getModelPath()) { + if (!is_callable($path) && $path != $this->modelPath) { if (!is_dir($path)) { mkdir($path); } @@ -91,10 +103,6 @@ public function handle() // cycle through each table foreach ($tables as $table) { - // grab a fresh copy of our stub - $stub = $modelStub; - - // if (!$overwrite and file_exists($fullPath)) { if (!$overwrite and file_exists($table['file']['path'])) { $this->doComment("Skipping file: {$table['file']['name']}"); continue; @@ -102,14 +110,14 @@ public function handle() $this->doComment("Generating file: {$table['file']['name']}"); - $stub = $this->hydrateStub($stub, $table); + $stub = $this->hydrateStub($table); // writing stub out $this->doComment("Writing model: {$table['file']['path']}", true); file_put_contents($table['file']['path'], $stub); } - $this->info('Complete'); + $this->info('Completed in ' . (number_format(microtime(true) - $this->startTime, 2)) . ' seconds'); } public function describeTable($tableName) @@ -127,17 +135,12 @@ public function describeTable($tableName) * * @return string stub content */ - public function hydrateStub($stub, $table) + public function hydrateStub($table) { // replace table - $stub = str_replace('{{table}}', $table['name'], $stub); - - $primaryKey = config('modelfromtable.primaryKey', 'id'); + $stub = $this->modelStub; - // allow config to apply a lamba to obtain non-ordinary primary key name - if (is_callable($primaryKey)) { - $primaryKey = $primaryKey($table['name']); - } + $primaryKey = $table['primary']; // reset stub fields $stubDocBlock = $stubFillable = $stubHidden = $stubCast = $stubDate = ''; @@ -269,6 +272,7 @@ public function hydrateOptions() $this->options['folder'] = $this->getOption('folder', ''); $this->options['filename'] = $this->getOption('filename', ''); $this->options['namespace'] = $this->getOption('namespace', ''); + $this->options['schema'] = $this->getOption('schema', ''); // if there is no folder specified and no namespace, set default namespaace if (!$this->options['folder'] && !$this->options['namespace']) { @@ -285,7 +289,7 @@ public function hydrateOptions() // finish setting up folder (if not a function) if (!is_callable($this->options['folder'])) { - $this->options['folder'] = ($this->options['folder']) ? base_path($this->options['folder']) : $this->getModelPath(); + $this->options['folder'] = ($this->options['folder']) ? base_path($this->options['folder']) : $this->modelPath; // trim trailing slashes $this->options['folder'] = rtrim($this->options['folder'], '/'); } @@ -313,11 +317,6 @@ private function getOption(string $key, $default = null, bool $isBool = false) return $return; } - private function getModelPath() - { - return (app()->version() > '8')? app()->path('Models') : app()->path(); - } - /** * will add a comment to the screen if debug is on, or is over-ridden. */ @@ -333,66 +332,55 @@ public function doComment($text, $overrideDebug = false) */ public function getTables() { - $tables = collect(); + $this->doComment('Retrieving database tables'); + + $whitelist = config('modelfromtable.whitelist', []); + $blacklist = config('modelfromtable.blacklist', ['migrations']); if ($this->options['table']) { - $tableNames = explode(',', $this->options['table']); - } else { - // get all tables by default - $whitelist = config('modelfromtable.whitelist', []); - $blacklist = config('modelfromtable.blacklist', []); - - $tableNames = collect($this->db->select($this->db->raw("show full tables where Table_Type = 'BASE TABLE'")))->flatten(); - - $tableNames = $tableNames->map(function ($value) { - return collect($value)->flatten()[0]; - })->reject(function ($value) use ($blacklist) { - foreach($blacklist as $reject) { - if (fnmatch($reject, $value)) { - return true; - } - } - })->filter(function ($value) use ($whitelist) { - if (!$whitelist) { - return true; - } - foreach($whitelist as $accept) { - if (fnmatch($accept, $value)) { - return true; - } - } - }); + $whitelist = $whitelist + explode(',', $this->options['table']); } - // get all columns - foreach($tableNames as $tableName) { - $tables->push([ - 'name' => $tableName, - 'columns' => $this->getColumns($tableName), - 'file' => $this->getPath($tableName) - ]); + // mysql REGEXP behaves differently than fnmatch, so slightly modify operators + $whitelistString = Str::replace('*', '.*', implode('|', $whitelist)); + $whitelistString = "($whitelistString)$"; + $blacklistString = Str::replace('*', '.*', implode('|', $blacklist)); + $blacklistString = "($blacklistString)$"; + + // get all tables by default + $query = $this->db + ->query() + ->select(['TABLE_NAME as name', 'COLUMN_NAME as field', 'COLUMN_TYPE as type']) + ->selectRaw("IF(COLUMN_KEY = 'PRI', 1, 0) as isPrimary") + ->from('INFORMATION_SCHEMA.COLUMNS') + ->where('TABLE_NAME', 'REGEXP', $whitelistString) + ->where('TABLE_NAME', 'NOT REGEXP', $blacklistString) + ->orderBy('TABLE_NAME') + ->orderBy('isPrimary', 'DESC'); + + if ($this->options['schema']) { + $query->where('TABLE_SCHEMA', $this->options['schema']); + } else { + $query->whereNotIn('TABLE_SCHEMA', ['information_schema', 'mysql', 'sys']); } - return $tables; - } - - private function getColumns($table) - { - // fix these up - $columns = $this->describeTable($table); - - // use a collection - $return = collect(); - - foreach ($columns as $col) { - $return->push([ - 'field' => $col->Field, - 'type' => $col->Type, + $columns = $query->get(); + + return $columns + ->groupBy('name') + ->mapWithKeys(fn($x, $tableName) => [ + $tableName => [ + 'name' => $tableName, + 'columns' => $x->map(fn($y) => [ + 'field' => $y->field, + 'type' => $y->type + ]), + 'primary' => ($x[0]->isPrimary) ? $x[0]->field : null, + 'file' => $this->getPath($tableName) + ] ]); - } - - return $return; } + private function getPath($tableName) { From 41564ff2aa928af490c727d1966c4d432c01df3d Mon Sep 17 00:00:00 2001 From: David Rogers Date: Fri, 15 Oct 2021 17:29:57 -0700 Subject: [PATCH 2/2] regression fix really need to add pest lol --- src/Commands/ModelFromTableCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Commands/ModelFromTableCommand.php b/src/Commands/ModelFromTableCommand.php index fa1585c..cfc6e71 100644 --- a/src/Commands/ModelFromTableCommand.php +++ b/src/Commands/ModelFromTableCommand.php @@ -223,6 +223,7 @@ public function hydrateStub($table) $stub = str_replace('{{connection}}', $this->stubConnection, $stub); $stub = str_replace('{{class}}', $table['file']['class'], $stub); $stub = str_replace('{{docblock}}', $stubDocBlock, $stub); + $stub = str_replace('{{table}}', $table['name'], $stub); $stub = str_replace('{{primaryKey}}', $primaryKey, $stub); $stub = str_replace('{{fillable}}', $stubFillable, $stub); $stub = str_replace('{{hidden}}', $stubHidden, $stub);