Skip to content

Commit

Permalink
Merge pull request #52 from daverogers/optimize-columns
Browse files Browse the repository at this point in the history
speed optimizations
  • Loading branch information
michael-mcmullen authored Oct 21, 2021
2 parents 84331ce + 41564ff commit 9f27557
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 89 deletions.
13 changes: 0 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions config/modelfromtable.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@

'table' => '',

'primaryKey' => 'id',

'folder' => '',

'filename' => '',
Expand Down
137 changes: 63 additions & 74 deletions src/Commands/ModelFromTableCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -34,30 +35,41 @@ class ModelFromTableCommand extends Command

private $db;
private $options;
private $startTime;
private $delimiter;
private $stubConnection;

private $modelPath;
private $modelStub;

/**
* Create a new command instance.
*
* @return void
*/
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,
'overwrite' => false
];

$this->delimiter = config('modelfromtable.delimiter', ', ');

$this->modelStub = file_get_contents($this->getStub());
}

/**
Expand All @@ -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);
}
Expand All @@ -91,25 +103,21 @@ 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;
}

$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)
Expand All @@ -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 = '';
Expand Down Expand Up @@ -220,6 +223,7 @@ public function hydrateStub($stub, $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);
Expand Down Expand Up @@ -269,6 +273,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']) {
Expand All @@ -285,7 +290,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'], '/');
}
Expand Down Expand Up @@ -313,11 +318,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.
*/
Expand All @@ -333,66 +333,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)
{
Expand Down

0 comments on commit 9f27557

Please sign in to comment.