diff --git a/src/app/Console/Command.php b/src/app/Console/Command.php --- a/src/app/Console/Command.php +++ b/src/app/Console/Command.php @@ -199,6 +199,11 @@ return $this->getObject(\App\Wallet::class, $wallet, null); } + /** + * Execute the console command. + * + * @return mixed + */ public function handle() { if ($this->dangerous) { diff --git a/src/app/Console/Commands/Scalpel/TenantSetting/CreateCommand.php b/src/app/Console/Commands/Scalpel/TenantSetting/CreateCommand.php --- a/src/app/Console/Commands/Scalpel/TenantSetting/CreateCommand.php +++ b/src/app/Console/Commands/Scalpel/TenantSetting/CreateCommand.php @@ -6,6 +6,8 @@ class CreateCommand extends ObjectCreateCommand { + protected $hidden = true; + protected $commandPrefix = 'scalpel'; protected $objectClass = \App\TenantSetting::class; protected $objectName = 'tenant-setting'; diff --git a/src/app/Console/Commands/Scalpel/TenantSetting/UpdateCommand.php b/src/app/Console/Commands/Scalpel/TenantSetting/UpdateCommand.php --- a/src/app/Console/Commands/Scalpel/TenantSetting/UpdateCommand.php +++ b/src/app/Console/Commands/Scalpel/TenantSetting/UpdateCommand.php @@ -6,6 +6,8 @@ class UpdateCommand extends ObjectUpdateCommand { + protected $hidden = true; + protected $commandPrefix = 'scalpel'; protected $objectClass = \App\TenantSetting::class; protected $objectName = 'tenant-setting'; diff --git a/src/app/Console/Commands/Scalpel/Wallet/SettingsCommand.php b/src/app/Console/Commands/Scalpel/Wallet/SettingsCommand.php --- a/src/app/Console/Commands/Scalpel/Wallet/SettingsCommand.php +++ b/src/app/Console/Commands/Scalpel/Wallet/SettingsCommand.php @@ -6,6 +6,8 @@ class SettingsCommand extends ObjectRelationListCommand { + protected $hidden = true; + protected $commandPrefix = 'scalpel'; protected $objectClass = \App\Wallet::class; protected $objectName = 'wallet'; diff --git a/src/app/Console/Commands/Scalpel/WalletSetting/CreateCommand.php b/src/app/Console/Commands/Scalpel/WalletSetting/CreateCommand.php --- a/src/app/Console/Commands/Scalpel/WalletSetting/CreateCommand.php +++ b/src/app/Console/Commands/Scalpel/WalletSetting/CreateCommand.php @@ -6,6 +6,8 @@ class CreateCommand extends ObjectCreateCommand { + protected $hidden = true; + protected $commandPrefix = 'scalpel'; protected $objectClass = \App\WalletSetting::class; protected $objectName = 'wallet-setting'; diff --git a/src/app/Console/Commands/Scalpel/WalletSetting/UpdateCommand.php b/src/app/Console/Commands/Scalpel/WalletSetting/UpdateCommand.php --- a/src/app/Console/Commands/Scalpel/WalletSetting/UpdateCommand.php +++ b/src/app/Console/Commands/Scalpel/WalletSetting/UpdateCommand.php @@ -6,6 +6,8 @@ class UpdateCommand extends ObjectUpdateCommand { + protected $hidden = true; + protected $commandPrefix = 'scalpel'; protected $objectClass = \App\WalletSetting::class; protected $objectName = 'wallet-setting'; diff --git a/src/app/Console/ObjectCommand.php b/src/app/Console/ObjectCommand.php --- a/src/app/Console/ObjectCommand.php +++ b/src/app/Console/ObjectCommand.php @@ -2,8 +2,6 @@ namespace App\Console; -use Illuminate\Support\Facades\Cache; - abstract class ObjectCommand extends Command { /** diff --git a/src/app/Console/ObjectCreateCommand.php b/src/app/Console/ObjectCreateCommand.php --- a/src/app/Console/ObjectCreateCommand.php +++ b/src/app/Console/ObjectCreateCommand.php @@ -25,7 +25,7 @@ parent::__construct(); } - public function getProperties() + protected function getProperties() { if (!empty($this->properties)) { return $this->properties; @@ -44,19 +44,13 @@ /** * Execute the console command. - * - * @return mixed */ public function handle() { - $this->getProperties(); - - $class = new $this->objectClass(); - - $object = $this->objectClass::create($this->properties); + $object = $this->objectClass::create($this->getProperties()); if ($object) { - $this->info($object->{$class->getKeyName()}); + $this->info($object->{$object->getKeyName()}); } else { $this->error("Object could not be created."); } diff --git a/src/app/Console/ObjectDeleteCommand.php b/src/app/Console/ObjectDeleteCommand.php --- a/src/app/Console/ObjectDeleteCommand.php +++ b/src/app/Console/ObjectDeleteCommand.php @@ -3,7 +3,6 @@ namespace App\Console; use Illuminate\Database\Eloquent\SoftDeletes; -use Illuminate\Support\Facades\Schema; /** * This abstract class provides a means to treat objects in our model using CRUD. @@ -20,20 +19,6 @@ $this->objectName ); - $class = new $this->objectClass(); - - try { - foreach (Schema::getColumnListing($class->getTable()) as $column) { - if ($column == "id") { - continue; - } - - $this->signature .= " {--{$column}=}"; - } - } catch (\Exception $e) { - \Log::error("Could not extract options: {$e->getMessage()}"); - } - $classes = class_uses_recursive($this->objectClass); if (in_array(SoftDeletes::class, $classes)) { @@ -43,29 +28,6 @@ parent::__construct(); } - public function getProperties() - { - if (!empty($this->properties)) { - return $this->properties; - } - - $class = new $this->objectClass(); - - $this->properties = []; - - foreach (Schema::getColumnListing($class->getTable()) as $column) { - if ($column == "id") { - continue; - } - - if (($value = $this->option($column)) !== null) { - $this->properties[$column] = $value; - } - } - - return $this->properties; - } - /** * Execute the console command. * diff --git a/src/app/Console/ObjectUpdateCommand.php b/src/app/Console/ObjectUpdateCommand.php --- a/src/app/Console/ObjectUpdateCommand.php +++ b/src/app/Console/ObjectUpdateCommand.php @@ -2,9 +2,6 @@ namespace App\Console; -use Illuminate\Database\Eloquent\SoftDeletes; -use Illuminate\Support\Facades\Schema; - /** * This abstract class provides a means to treat objects in our model using CRUD. */ @@ -20,29 +17,66 @@ $this->objectName ); - $class = new $this->objectClass(); + // This constructor is called for every ObjectUpdateCommand command, + // no matter which command is being executed. We should not use database + // access from here. And it should be as fast as possible. - try { - foreach (Schema::getColumnListing($class->getTable()) as $column) { - if ($column == "id") { - continue; - } + $class = new $this->objectClass(); - $this->signature .= " {--{$column}=}"; + foreach ($this->getClassProperties() as $property) { + if ($property == 'id') { + continue; } - } catch (\Exception $e) { - \Log::error("Could not extract options: {$e->getMessage()}"); - } - $classes = class_uses_recursive($this->objectClass); + $this->signature .= " {--{$property}=}"; + } - if (in_array(SoftDeletes::class, $classes)) { + if (method_exists($class, 'restore')) { $this->signature .= " {--with-deleted : Include deleted {$this->objectName}s}"; } parent::__construct(); } + /** + * Get all properties (sql table columns) of the model class + */ + protected function getClassProperties(): array + { + // We are not using table information schema, because it makes + // all artisan commands slow. We depend on the @property definitions + // in the class documentation comment. + + $reflector = new \ReflectionClass($this->objectClass); + $list = []; + + if (preg_match_all('/@property\s+([^$\s]+)\s+\$([a-z_]+)/', $reflector->getDocComment(), $matches)) { + foreach ($matches[1] as $key => $type) { + $type = preg_replace('/[\?]/', '', $type); + if (preg_match('/^(int|string|float|bool|\\Carbon\\Carbon)$/', $type)) { + $list[] = $matches[2][$key]; + } + } + } + + // Add created_at, updated_at, deleted_at where applicable + if ($this->commandPrefix == 'scalpel') { + $class = new $this->objectClass(); + + if ($class->timestamps && !in_array('created_at', $list)) { + $list[] = 'created_at'; + } + if ($class->timestamps && !in_array('updated_at', $list)) { + $list[] = 'updated_at'; + } + if (method_exists($class, 'restore') && !in_array('deleted_at', $list)) { + $list[] = 'deleted_at'; + } + } + + return $list; + } + public function getProperties() { if (!empty($this->properties)) { @@ -53,13 +87,13 @@ $this->properties = []; - foreach (Schema::getColumnListing($class->getTable()) as $column) { - if ($column == "id") { + foreach ($this->getClassProperties() as $property) { + if ($property == 'id') { continue; } - if (($value = $this->option($column)) !== null) { - $this->properties[$column] = $value; + if (($value = $this->option($property)) !== null) { + $this->properties[$property] = $value; } } @@ -83,15 +117,13 @@ } foreach ($this->getProperties() as $property => $value) { - if ($property == "deleted_at" && $value == "null") { + if ($property == 'deleted_at' && $value === 'null') { $value = null; } $object->{$property} = $value; } - $object->timestamps = false; - if ($this->commandPrefix == 'scalpel') { $object->saveQuietly(); } else { diff --git a/src/tests/Feature/Console/Scalpel/Domain/UpdateCommandTest.php b/src/tests/Feature/Console/Scalpel/Domain/UpdateCommandTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/Scalpel/Domain/UpdateCommandTest.php @@ -0,0 +1,70 @@ +deleteTestDomain('domain-delete.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestDomain('domain-delete.com'); + $this->deleteTestDomain('domain-delete-mod.com'); + + parent::tearDown(); + } + + /** + * Test the command execution + */ + public function testHandle(): void + { + // Test unknown domain + $this->artisan("scalpel:domain:update unknown") + ->assertExitCode(1) + ->expectsOutput("No such domain unknown"); + + $domain = $this->getTestDomain('domain-delete.com', [ + 'status' => Domain::STATUS_NEW, + 'type' => Domain::TYPE_HOSTED, + ]); + + // Test successful update + $this->artisan("scalpel:domain:update {$domain->id}" + . " --namespace=domain-delete-mod.com --type=" . Domain::TYPE_PUBLIC) + ->assertExitCode(0); + + $domain->refresh(); + + $this->assertSame('domain-delete-mod.com', $domain->namespace); + $this->assertSame(Domain::TYPE_PUBLIC, $domain->type); + + // Test --help argument + $code = \Artisan::call("scalpel:domain:update --help"); + $output = trim(\Artisan::output()); + + $this->assertStringContainsString('--with-deleted', $output); + $this->assertStringContainsString('--namespace[=NAMESPACE]', $output); + $this->assertStringContainsString('--type[=TYPE]', $output); + $this->assertStringContainsString('--status[=STATUS]', $output); + $this->assertStringContainsString('--tenant_id[=TENANT_ID]', $output); + $this->assertStringContainsString('--created_at[=CREATED_AT]', $output); + $this->assertStringContainsString('--updated_at[=UPDATED_AT]', $output); + $this->assertStringContainsString('--deleted_at[=DELETED_AT]', $output); + $this->assertStringNotContainsString('--id', $output); + } +}