diff --git a/src/AbstractPlugin.php b/src/AbstractPlugin.php index 6b28918..89eec58 100644 --- a/src/AbstractPlugin.php +++ b/src/AbstractPlugin.php @@ -46,41 +46,11 @@ abstract class AbstractPlugin implements PluginEntryPointInterface $view_factory = $this->getViewFactory($app, $fake_filesystem); - $stubs_generator_command = new \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand( - $app['config'], - $fake_filesystem, - $view_factory - ); - - $stubs_generator_command->setLaravel($app); - $cache_dir = __DIR__ . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR; - $fake_filesystem->setDestination($cache_dir . 'stubs.php'); - - $stubs_generator_command->run( - new \Symfony\Component\Console\Input\ArrayInput([]), - new \Symfony\Component\Console\Output\NullOutput() - ); - - /** @psalm-suppress InvalidArgument */ - $meta_generator_command = new FakeMetaCommand( - $fake_filesystem, - $view_factory, - $app['config'] - ); - - $meta_generator_command->setLaravel($app); - - $fake_filesystem->setDestination($cache_dir . 'meta.php'); - - $meta_generator_command->run( - new \Symfony\Component\Console\Input\ArrayInput([]), - new \Symfony\Component\Console\Output\NullOutput() - ); - - $registration->addStubFile($cache_dir . 'stubs.php'); - $registration->addStubFile($cache_dir . 'meta.php'); + $this->ingestFacadeStubs($registration, $app, $fake_filesystem, $view_factory, $cache_dir); + $this->ingestMetaStubs($registration, $app, $fake_filesystem, $view_factory, $cache_dir); + $this->ingestModelStubs($registration, $app, $fake_filesystem, $cache_dir); require_once 'ReturnTypeProvider/AuthReturnTypeProvider.php'; $registration->registerHooksFromClass(ReturnTypeProvider\AuthReturnTypeProvider::class); @@ -92,6 +62,117 @@ abstract class AbstractPlugin implements PluginEntryPointInterface $registration->registerHooksFromClass(AppInterfaceProvider::class); } + /** + * @param \Illuminate\Contracts\Container\Container $app + * @param \Illuminate\View\Factory $view_factory + */ + private function ingestFacadeStubs( + RegistrationInterface $registration, + $app, + \Illuminate\Filesystem\Filesystem $fake_filesystem, + $view_factory, + string $cache_dir + ) { + $stubs_generator_command = new \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand( + $app['config'], + $fake_filesystem, + $view_factory + ); + + $stubs_generator_command->setLaravel($app); + + @unlink($cache_dir . 'stubs.php'); + + $fake_filesystem->setDestination($cache_dir . 'stubs.php'); + + $stubs_generator_command->run( + new \Symfony\Component\Console\Input\ArrayInput([]), + new \Symfony\Component\Console\Output\NullOutput() + ); + + $registration->addStubFile($cache_dir . 'stubs.php'); + + unlink($cache_dir . 'stubs.php'); + } + + /** + * @param \Illuminate\Contracts\Container\Container $app + * @param \Illuminate\View\Factory $view_factory + */ + private function ingestMetaStubs( + RegistrationInterface $registration, + $app, + \Illuminate\Filesystem\Filesystem $fake_filesystem, + $view_factory, + string $cache_dir + ) { + /** @psalm-suppress InvalidArgument */ + $meta_generator_command = new FakeMetaCommand( + $fake_filesystem, + $view_factory, + $app['config'] + ); + + $meta_generator_command->setLaravel($app); + + @unlink($cache_dir . 'meta.php'); + + $fake_filesystem->setDestination($cache_dir . 'meta.php'); + + $meta_generator_command->run( + new \Symfony\Component\Console\Input\ArrayInput([]), + new \Symfony\Component\Console\Output\NullOutput() + ); + + $registration->addStubFile($cache_dir . 'meta.php'); + + unlink($cache_dir . 'meta.php'); + } + + /** + * @param \Illuminate\Contracts\Container\Container $app + */ + private function ingestModelStubs( + RegistrationInterface $registration, + $app, + \Illuminate\Filesystem\Filesystem $fake_filesystem, + string $cache_dir + ) { + $migrations_folder = dirname(__DIR__, 4) . '/database/migrations/'; + + $project_analyzer = \Psalm\Internal\Analyzer\ProjectAnalyzer::getInstance(); + $codebase = $project_analyzer->getCodebase(); + + $schema_aggregator = new SchemaAggregator(); + + foreach (glob($migrations_folder . '*.php') as $file) { + //echo $file . "\n"; + $schema_aggregator->addStatements($codebase->getStatementsForFile($file)); + } + + $models_generator_command = new FakeModelsCommand( + $fake_filesystem, + $schema_aggregator + ); + + $models_generator_command->setLaravel($app); + + @unlink($cache_dir . 'models.php'); + + $fake_filesystem->setDestination($cache_dir . 'models.php'); + + $models_generator_command->run( + new \Symfony\Component\Console\Input\ArrayInput([ + '--nowrite' => true + ]), + new \Symfony\Component\Console\Output\NullOutput() + ); + + $registration->addStubFile($cache_dir . 'models.php'); + + unlink($cache_dir . 'models.php'); + } + /** * Undocumented function * diff --git a/src/FakeModelsCommand.php b/src/FakeModelsCommand.php new file mode 100644 index 0000000..4945008 --- /dev/null +++ b/src/FakeModelsCommand.php @@ -0,0 +1,110 @@ +schema = $schema; + } + + /** + * Load the properties from the database table. + * + * @param \Illuminate\Database\Eloquent\Model $model + */ + protected function getPropertiesFromTable($model) : void + { + $table_name = $model->getTable(); + + if (!isset($this->schema->tables[$table_name])) { + return; + } + + $columns = $this->schema->tables[$table_name]->columns; + + foreach ($columns as $column) { + $name = $column->name; + + if (in_array($name, $model->getDates())) { + $type = '\Illuminate\Support\Carbon'; + } else { + switch ($column->type) { + case 'string': + case 'int': + case 'float': + $type = $column->type; + break; + + case 'bool': + switch (config('database.default')) { + case 'sqlite': + case 'mysql': + $type = 'int'; + break; + default: + $type = 'bool'; + break; + } + + break; + + case 'enum': + if (!$column->options) { + $type = 'string'; + } else { + $type = '\'' . implode('\'|\'', $column->options) . '\''; + } + + break; + + default: + $type = 'mixed'; + break; + } + } + + if ($column->nullable) { + $this->nullableColumns[$name] = true; + } + + $this->setProperty($name, $type, true, true, '', $column->nullable); + if ($this->write_model_magic_where) { + $this->setMethod( + Str::camel("where_" . $name), + '\Illuminate\Database\Eloquent\Builder|\\' . get_class($model), + array('$value') + ); + } + } + } + + /** + * @param \Illuminate\Database\Eloquent\Model $model + */ + protected function getPropertiesFromMethods($model) + { + // do nothing here + } +} diff --git a/src/SchemaAggregator.php b/src/SchemaAggregator.php new file mode 100644 index 0000000..20da091 --- /dev/null +++ b/src/SchemaAggregator.php @@ -0,0 +1,546 @@ + */ + public $tables = []; + + /** + * @param array $statements + */ + public function addStatements(array $stmts) : void + { + foreach ($stmts as $stmt) { + if ($stmt instanceof PhpParser\Node\Stmt\Class_) { + $this->addClassStatements($stmt->stmts); + } + } + } + + /** + * @param array $statements + */ + private function addClassStatements(array $stmts) : void + { + foreach ($stmts as $stmt) { + if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod + && $stmt->name->name === 'up' + ) { + $this->addUpMethodStatements($stmt->stmts); + } + } + } + + /** + * @param array $statements + */ + private function addUpMethodStatements(array $stmts) : void + { + foreach ($stmts as $stmt) { + if ($stmt instanceof PhpParser\Node\Stmt\Expression + && $stmt->expr instanceof PhpParser\Node\Expr\StaticCall + && $stmt->expr->class instanceof PhpParser\Node\Name + && $stmt->expr->name instanceof PhpParser\Node\Identifier + && $stmt->expr->class->getAttribute('resolvedName') === \Illuminate\Support\Facades\Schema::class + ) { + switch ($stmt->expr->name->name) { + case 'create': + $this->alterTable($stmt->expr, true); + break; + + case 'table': + $this->alterTable($stmt->expr, false); + break; + + case 'drop': + case 'dropIfExists': + $this->dropTable($stmt->expr); + break; + + case 'rename': + $this->renameTable($stmt->expr); + } + + } + } + } + + private function alterTable(PhpParser\Node\Expr\StaticCall $call, bool $creating) : void + { + if (!isset($call->args[0]) + || !$call->args[0]->value instanceof PhpParser\Node\Scalar\String_ + ) { + return; + } + + $table_name = $call->args[0]->value->value; + + if ($creating) { + $this->tables[$table_name] = new SchemaTable($table_name); + } + + if (!isset($call->args[1]) + || !$call->args[1]->value instanceof PhpParser\Node\Expr\Closure + || count($call->args[1]->value->params) < 1 + || ($call->args[1]->value->params[0]->type instanceof PhpParser\Node\Name + && $call->args[1]->value->params[0]->type->getAttribute('resolvedName') + !== \Illuminate\Database\Schema\Blueprint::class) + ) { + return; + } + + $update_closure = $call->args[1]->value; + + $call_arg_name = $call->args[1]->value->params[0]->var->name; + + $this->processColumnUpdates($table_name, $call_arg_name, $update_closure->stmts); + } + + private function dropTable(PhpParser\Node\Expr\StaticCall $call) + { + if (!isset($call->args[0]) + || !$call->args[0]->value instanceof PhpParser\Node\Scalar\String_ + ) { + return; + } + + $table_name = $call->args[0]->value->value; + + unset($this->tables[$table_name]); + } + + private function renameTable(PhpParser\Node\Expr\StaticCall $call) + { + if (!isset($call->args[0]) + || !$call->args[0]->value instanceof PhpParser\Node\Scalar\String_ + || !isset($call->args[1]) + || !$call->args[1]->value instanceof PhpParser\Node\Scalar\String_ + ) { + return; + } + + $old_table_name = $call->args[0]->value->value; + $new_table_name = $call->args[1]->value->value; + + if (!isset($this->tables[$old_table_name])) { + return; + } + + $table = $this->tables[$old_table_name]; + + unset($this->tables[$old_table_name]); + + $table->name = $new_table_name; + + $this->tables[$new_table_name] = $table; + } + + private function processColumnUpdates(string $table_name, string $call_arg_name, array $stmts) : void + { + if (!isset($this->tables[$table_name])) { + return; + } + + $table = $this->tables[$table_name]; + + foreach ($stmts as $stmt) { + if ($stmt instanceof PhpParser\Node\Stmt\Expression + && $stmt->expr instanceof PhpParser\Node\Expr\MethodCall + && $stmt->expr->name instanceof PhpParser\Node\Identifier + ) { + $root_var = $stmt->expr; + + $first_method_call = $root_var; + + $additional_method_calls = []; + + $nullable = false; + + while ($root_var instanceof PhpParser\Node\Expr\MethodCall) { + if ($root_var->name instanceof PhpParser\Node\Identifier + && $root_var->name->name === 'nullable' + ) { + $nullable = true; + } + + $first_method_call = $root_var; + $root_var = $root_var->var; + } + + if ($root_var instanceof PhpParser\Node\Expr\Variable + && $root_var->name === $call_arg_name + ) { + $first_arg = $first_method_call->args[0]->value ?? null; + $second_arg = $first_method_call->args[1]->value ?? null; + + if (!$first_arg instanceof PhpParser\Node\Scalar\String_) { + if ($first_method_call->name->name === 'timestamps' + || $first_method_call->name->name === 'timestampsTz' + || $first_method_call->name->name === 'nullableTimestamps' + || $first_method_call->name->name === 'nullableTimestampsTz' + || $first_method_call->name->name === 'rememberToken' + ) { + $column_name = null; + } elseif ($first_method_call->name->name === 'softDeletes' + || $first_method_call->name->name === 'softDeletesTz' + || $first_method_call->name->name === 'dropSoftDeletes' + || $first_method_call->name->name === 'dropSoftDeletesTz' + ) { + $column_name = 'deleted_at'; + } else { + continue; + } + } else { + $column_name = $first_arg->value; + } + + $second_arg_array = null; + + if ($second_arg instanceof PhpParser\Node\Expr\Array_) { + $second_arg_array = []; + + foreach ($second_arg->items as $array_item) { + if ($array_item->value instanceof PhpParser\Node\Scalar\String_) { + $second_arg_array[] = $array_item->value->value; + } + } + } + + switch ($first_method_call->name->name) { + case 'bigIncrements': + $table->setColumn(new SchemaColumn($column_name, 'int', $nullable)); + break; + + case 'bigInteger': + $table->setColumn(new SchemaColumn($column_name, 'int', $nullable)); + break; + + case 'binary': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'boolean': + $table->setColumn(new SchemaColumn($column_name, 'bool', $nullable)); + break; + + case 'char': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'computed': + $table->setColumn(new SchemaColumn($column_name, 'mixed', $nullable)); + break; + + case 'date': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'dateTime': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'dateTimeTz': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'decimal': + $table->setColumn(new SchemaColumn($column_name, 'float', $nullable)); + break; + + case 'double': + $table->setColumn(new SchemaColumn($column_name, 'float', $nullable)); + break; + + case 'drop': + $table->dropColumn($column_name); + break; + + case 'dropColumn': + $table->dropColumn($column_name); + break; + + case 'dropForeign': + case 'dropIndex': + case 'dropPrimary': + case 'dropUnique': + case 'dropSpatialIndex': + break; + + case 'dropIfExists': + $table->dropColumn($column_name); + break; + + case 'dropMorphs': + $table->dropColumn($column_name . '_type'); + $table->dropColumn($column_name . '_id'); + break; + + case 'dropRememberToken': + $table->dropColumn('remember_token'); + break; + + case 'dropSoftDeletes': + $table->dropColumn($column_name); + break; + + case 'dropSoftDeletesTz': + $table->dropColumn($column_name); + break; + + case 'dropTimestamps': + case 'dropTimestampsTz': + $table->dropColumn('created_at'); + $table->dropColumn('updated_at'); + break; + + case 'enum': + $table->setColumn(new SchemaColumn($column_name, 'enum', $nullable, $second_arg_array)); + break; + + case 'float': + $table->setColumn(new SchemaColumn($column_name, 'float', $nullable)); + break; + + case 'foreign': + break; + + case 'geometry': + $table->setColumn(new SchemaColumn($column_name, 'mixed', $nullable)); + break; + + case 'geometryCollection': + $table->setColumn(new SchemaColumn($column_name, 'mixed', $nullable)); + break; + + case 'increments': + break; + + case 'index': + break; + + case 'integer': + $table->setColumn(new SchemaColumn($column_name, 'int', $nullable)); + break; + + case 'integerIncrements': + $table->setColumn(new SchemaColumn($column_name, 'int', $nullable)); + break; + + case 'ipAddress': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'json': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'jsonb': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'lineString': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'longText': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'macAddress': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'mediumIncrements': + $table->setColumn(new SchemaColumn($column_name, 'int', $nullable)); + break; + + case 'mediumInteger': + $table->setColumn(new SchemaColumn($column_name, 'int', $nullable)); + break; + + case 'mediumText': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'morphs': + $table->setColumn(new SchemaColumn($column_name . '_type', 'string', $nullable)); + $table->setColumn(new SchemaColumn($column_name . '_id', 'int', $nullable)); + break; + + case 'multiLineString': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'multiPoint': + $table->setColumn(new SchemaColumn($column_name, 'mixed', $nullable)); + break; + + case 'multiPolygon': + $table->setColumn(new SchemaColumn($column_name, 'mixed', $nullable)); + break; + + case 'multiPolygonZ': + $table->setColumn(new SchemaColumn($column_name, 'mixed', $nullable)); + break; + + case 'nullableMorphs': + $table->setColumn(new SchemaColumn($column_name . '_type', 'string', true)); + $table->setColumn(new SchemaColumn($column_name . '_id', 'int', true)); + break; + + case 'nullableTimestamps': + $table->setColumn(new SchemaColumn('created_at', 'string', true)); + $table->setColumn(new SchemaColumn('updated_at', 'string', true)); + break; + + case 'nullableUuidMorphs': + $table->setColumn(new SchemaColumn($column_name . '_type', 'string', true)); + $table->setColumn(new SchemaColumn($column_name . '_id', 'string', true)); + break; + + case 'point': + $table->setColumn(new SchemaColumn($column_name, 'mixed', $nullable)); + break; + + case 'polygon': + $table->setColumn(new SchemaColumn($column_name, 'mixed', $nullable)); + break; + + case 'primary': + break; + + case 'rememberToken': + $table->setColumn(new SchemaColumn('remember_token', 'string', $nullable)); + break; + + case 'removeColumn': + $table->dropColumn($column_name); + break; + + case 'rename': + if ($second_arg instanceof PhpParser\Node\Scalar\String_) { + $table->renameColumn($column_name, $second_arg->value); + } + break; + + case 'renameColumn': + break; + + case 'renameIndex': + break; + + case 'set': + $table->setColumn(new SchemaColumn($column_name, 'set', $nullable, $second_arg_array)); + break; + + case 'smallIncrements': + $table->setColumn(new SchemaColumn($column_name, 'int', $nullable)); + break; + + case 'smallInteger': + $table->setColumn(new SchemaColumn($column_name, 'int', $nullable)); + break; + + case 'softDeletes': + $table->setColumn(new SchemaColumn($column_name, 'string', true)); + break; + + case 'softDeletesTz': + $table->setColumn(new SchemaColumn($column_name, 'string', true)); + break; + + case 'spatialIndex': + break; + + case 'string': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'text': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'time': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'timestamp': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'timestamps': + $table->setColumn(new SchemaColumn('created_at', 'string', true)); + $table->setColumn(new SchemaColumn('updated_at', 'string', true)); + break; + + case 'timestampsTz': + $table->setColumn(new SchemaColumn('created_at', 'string', true)); + $table->setColumn(new SchemaColumn('updated_at', 'string', true)); + break; + + case 'timestampTz': + $table->setColumn(new SchemaColumn($column_name, 'string', true)); + break; + + case 'timeTz': + $table->setColumn(new SchemaColumn($column_name, 'string', true)); + break; + + case 'tinyIncrements': + $table->setColumn(new SchemaColumn($column_name, 'int', $nullable)); + break; + + case 'tinyInteger': + $table->setColumn(new SchemaColumn($column_name, 'int', $nullable)); + break; + + case 'unique': + break; + + case 'unsignedBigInteger': + $table->setColumn(new SchemaColumn($column_name, 'int', $nullable)); + break; + + case 'unsignedDecimal': + $table->setColumn(new SchemaColumn($column_name, 'float', $nullable)); + break; + + case 'unsignedInteger': + $table->setColumn(new SchemaColumn($column_name, 'int', $nullable)); + break; + + case 'unsignedMediumInteger': + $table->setColumn(new SchemaColumn($column_name, 'int', $nullable)); + break; + + case 'unsignedSmallInteger': + $table->setColumn(new SchemaColumn($column_name, 'int', $nullable)); + break; + + case 'unsignedTinyInteger': + $table->setColumn(new SchemaColumn($column_name, 'int', $nullable)); + break; + + case 'uuid': + $table->setColumn(new SchemaColumn($column_name, 'string', $nullable)); + break; + + case 'uuidMorphs': + $table->setColumn(new SchemaColumn($column_name . '_type', 'string', $nullable)); + $table->setColumn(new SchemaColumn($column_name . '_id', 'string', $nullable)); + break; + + case 'year': + $table->setColumn(new SchemaColumn($column_name, 'string', true)); + break; + } + } + + + } + } + } +} diff --git a/src/SchemaColumn.php b/src/SchemaColumn.php new file mode 100644 index 0000000..a589ba2 --- /dev/null +++ b/src/SchemaColumn.php @@ -0,0 +1,29 @@ + */ + public $options; + + public function __construct( + string $name, + string $type, + bool $nullable = false, + ?array $options = null + ) { + $this->name = $name; + $this->type = $type; + $this->nullable = $nullable; + $this->options = $options; + } +} diff --git a/src/SchemaTable.php b/src/SchemaTable.php new file mode 100644 index 0000000..78f3de5 --- /dev/null +++ b/src/SchemaTable.php @@ -0,0 +1,41 @@ + */ + public $columns = []; + + public function __construct(string $name) + { + $this->name = $name; + } + + public function setColumn(SchemaColumn $column) : void + { + $this->columns[$column->name] = $column; + } + + public function renameColumn(string $old_name, string $new_name) : void + { + if (!isset($this->columns[$column->name])) { + return; + } + + $old_column = $this->columns[$old_name]; + + unset($this->columns[$old_name]); + + $old_column->name = $new_name; + + $this->columns[$new_name] = $old_column; + } + + public function dropColumn(string $column_name) : void + { + unset($this->columns[$column_name]); + } +}