Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F256673
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
25 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/app/Console/Command.php b/src/app/Console/Command.php
index 18cf47ae..6186e14d 100644
--- a/src/app/Console/Command.php
+++ b/src/app/Console/Command.php
@@ -1,264 +1,269 @@
<?php
namespace App\Console;
use Illuminate\Support\Facades\DB;
abstract class Command extends \Illuminate\Console\Command
{
/**
* This needs to be here to be used.
*
* @var string
*/
protected $commandPrefix = '';
/**
* Annotate this command as being dangerous for any potential unintended consequences.
*
* Commands are considered dangerous if;
*
* * observers are deliberately not triggered, meaning that the deletion of an object model that requires the
* associated observer to clean some things up, or charge a wallet or something, are deliberately not triggered,
*
* * deletion of objects and their relations rely on database foreign keys with obscure cascading,
*
* * a command will result in the permanent, irrecoverable loss of data.
*
* @var boolean
*/
protected $dangerous = false;
/**
* Shortcut to creating a progress bar of a particular format with a particular message.
*
* @param int $count Number of progress steps
* @param string $message The description
*
* @return \Symfony\Component\Console\Helper\ProgressBar
*/
protected function createProgressBar($count, $message = null)
{
$bar = $this->output->createProgressBar($count);
$bar->setFormat(
'%current:7s%/%max:7s% [%bar%] %percent:3s%% %elapsed:7s%/%estimated:-7s% %message% '
);
if ($message) {
$bar->setMessage("{$message}...");
}
$bar->start();
return $bar;
}
/**
* Find the domain.
*
* @param string $domain Domain ID or namespace
* @param bool $withDeleted Include deleted
*
* @return \App\Domain|null
*/
public function getDomain($domain, $withDeleted = false)
{
return $this->getObject(\App\Domain::class, $domain, 'namespace', $withDeleted);
}
/**
* Find a group.
*
* @param string $group Group ID or email
* @param bool $withDeleted Include deleted
*
* @return \App\Group|null
*/
public function getGroup($group, $withDeleted = false)
{
return $this->getObject(\App\Group::class, $group, 'email', $withDeleted);
}
/**
* Find an object.
*
* @param string $objectClass The name of the class
* @param string $objectIdOrTitle The name of a database field to match.
* @param string|null $objectTitle An additional database field to match.
* @param bool $withDeleted Act as if --with-deleted was used
*
* @return mixed
*/
public function getObject($objectClass, $objectIdOrTitle, $objectTitle = null, $withDeleted = false)
{
if (!$withDeleted) {
// @phpstan-ignore-next-line
$withDeleted = $this->hasOption('with-deleted') && $this->option('with-deleted');
}
$object = $this->getObjectModel($objectClass, $withDeleted)->find($objectIdOrTitle);
if (!$object && !empty($objectTitle)) {
$object = $this->getObjectModel($objectClass, $withDeleted)
->where($objectTitle, $objectIdOrTitle)->first();
}
return $object;
}
/**
* Returns a preconfigured Model object for a specified class.
*
* @param string $objectClass The name of the class
* @param bool $withDeleted Include withTrashed() query
*
* @return mixed
*/
protected function getObjectModel($objectClass, $withDeleted = false)
{
if ($withDeleted) {
$model = $objectClass::withTrashed();
} else {
$model = new $objectClass();
}
if ($this->commandPrefix == 'scalpel') {
return $model;
}
$modelsWithOwner = [
\App\Wallet::class,
];
$tenantId = \config('app.tenant_id');
// Add tenant filter
if (in_array(\App\Traits\BelongsToTenantTrait::class, class_uses($objectClass))) {
$model = $model->withEnvTenantContext();
} elseif (in_array($objectClass, $modelsWithOwner)) {
$model = $model->whereExists(function ($query) use ($tenantId) {
$query->select(DB::raw(1))
->from('users')
->whereRaw('wallets.user_id = users.id')
->whereRaw('users.tenant_id ' . ($tenantId ? "= $tenantId" : 'is null'));
});
}
return $model;
}
/**
* Find a resource.
*
* @param string $resource Resource ID or email
* @param bool $withDeleted Include deleted
*
* @return \App\Resource|null
*/
public function getResource($resource, $withDeleted = false)
{
return $this->getObject(\App\Resource::class, $resource, 'email', $withDeleted);
}
/**
* Find a shared folder.
*
* @param string $folder Folder ID or email
* @param bool $withDeleted Include deleted
*
* @return \App\SharedFolder|null
*/
public function getSharedFolder($folder, $withDeleted = false)
{
return $this->getObject(\App\SharedFolder::class, $folder, 'email', $withDeleted);
}
/**
* Find the user.
*
* @param string $user User ID or email
* @param bool $withDeleted Include deleted
*
* @return \App\User|null
*/
public function getUser($user, $withDeleted = false)
{
return $this->getObject(\App\User::class, $user, 'email', $withDeleted);
}
/**
* Find the wallet.
*
* @param string $wallet Wallet ID
*
* @return \App\Wallet|null
*/
public function getWallet($wallet)
{
return $this->getObject(\App\Wallet::class, $wallet, null);
}
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
public function handle()
{
if ($this->dangerous) {
$this->warn(
"This command is a dangerous scalpel command with potentially significant unintended consequences"
);
$confirmation = $this->confirm("Are you sure you understand what's about to happen?");
if (!$confirmation) {
$this->info("Better safe than sorry.");
return false;
}
$this->info("Vámonos!");
}
return true;
}
/**
* Return a string for output, with any additional attributes specified as well.
*
* @param mixed $entry An object
*
* @return string
*/
protected function toString($entry)
{
/**
* Haven't figured out yet, how to test if this command implements an option for additional
* attributes.
if (!in_array('attr', $this->options())) {
return $entry->{$entry->getKeyName()};
}
*/
$str = [
$entry->{$entry->getKeyName()}
];
// @phpstan-ignore-next-line
foreach ($this->option('attr') as $attr) {
if ($attr == $entry->getKeyName()) {
$this->warn("Specifying {$attr} is not useful.");
continue;
}
if (!array_key_exists($attr, $entry->toArray())) {
$this->error("Attribute {$attr} isn't available");
continue;
}
if (is_numeric($entry->{$attr})) {
$str[] = $entry->{$attr};
} else {
$str[] = !empty($entry->{$attr}) ? $entry->{$attr} : "null";
}
}
return implode(" ", $str);
}
}
diff --git a/src/app/Console/Commands/Scalpel/TenantSetting/CreateCommand.php b/src/app/Console/Commands/Scalpel/TenantSetting/CreateCommand.php
index 44b590c0..daa73d16 100644
--- a/src/app/Console/Commands/Scalpel/TenantSetting/CreateCommand.php
+++ b/src/app/Console/Commands/Scalpel/TenantSetting/CreateCommand.php
@@ -1,13 +1,15 @@
<?php
namespace App\Console\Commands\Scalpel\TenantSetting;
use App\Console\ObjectCreateCommand;
class CreateCommand extends ObjectCreateCommand
{
+ protected $hidden = true;
+
protected $commandPrefix = 'scalpel';
protected $objectClass = \App\TenantSetting::class;
protected $objectName = 'tenant-setting';
protected $objectTitle = null;
}
diff --git a/src/app/Console/Commands/Scalpel/TenantSetting/UpdateCommand.php b/src/app/Console/Commands/Scalpel/TenantSetting/UpdateCommand.php
index b566b80e..6255f05d 100644
--- a/src/app/Console/Commands/Scalpel/TenantSetting/UpdateCommand.php
+++ b/src/app/Console/Commands/Scalpel/TenantSetting/UpdateCommand.php
@@ -1,13 +1,15 @@
<?php
namespace App\Console\Commands\Scalpel\TenantSetting;
use App\Console\ObjectUpdateCommand;
class UpdateCommand extends ObjectUpdateCommand
{
+ protected $hidden = true;
+
protected $commandPrefix = 'scalpel';
protected $objectClass = \App\TenantSetting::class;
protected $objectName = 'tenant-setting';
protected $objectTitle = null;
}
diff --git a/src/app/Console/Commands/Scalpel/Wallet/SettingsCommand.php b/src/app/Console/Commands/Scalpel/Wallet/SettingsCommand.php
index 47ca881a..9fd90d82 100644
--- a/src/app/Console/Commands/Scalpel/Wallet/SettingsCommand.php
+++ b/src/app/Console/Commands/Scalpel/Wallet/SettingsCommand.php
@@ -1,14 +1,16 @@
<?php
namespace App\Console\Commands\Scalpel\Wallet;
use App\Console\ObjectRelationListCommand;
class SettingsCommand extends ObjectRelationListCommand
{
+ protected $hidden = true;
+
protected $commandPrefix = 'scalpel';
protected $objectClass = \App\Wallet::class;
protected $objectName = 'wallet';
protected $objectTitle = null;
protected $objectRelation = 'settings';
}
diff --git a/src/app/Console/Commands/Scalpel/WalletSetting/CreateCommand.php b/src/app/Console/Commands/Scalpel/WalletSetting/CreateCommand.php
index 1cd541c0..4792799e 100644
--- a/src/app/Console/Commands/Scalpel/WalletSetting/CreateCommand.php
+++ b/src/app/Console/Commands/Scalpel/WalletSetting/CreateCommand.php
@@ -1,13 +1,15 @@
<?php
namespace App\Console\Commands\Scalpel\WalletSetting;
use App\Console\ObjectCreateCommand;
class CreateCommand extends ObjectCreateCommand
{
+ protected $hidden = true;
+
protected $commandPrefix = 'scalpel';
protected $objectClass = \App\WalletSetting::class;
protected $objectName = 'wallet-setting';
protected $objectTitle = null;
}
diff --git a/src/app/Console/Commands/Scalpel/WalletSetting/UpdateCommand.php b/src/app/Console/Commands/Scalpel/WalletSetting/UpdateCommand.php
index 2193767e..555827e0 100644
--- a/src/app/Console/Commands/Scalpel/WalletSetting/UpdateCommand.php
+++ b/src/app/Console/Commands/Scalpel/WalletSetting/UpdateCommand.php
@@ -1,13 +1,15 @@
<?php
namespace App\Console\Commands\Scalpel\WalletSetting;
use App\Console\ObjectUpdateCommand;
class UpdateCommand extends ObjectUpdateCommand
{
+ protected $hidden = true;
+
protected $commandPrefix = 'scalpel';
protected $objectClass = \App\WalletSetting::class;
protected $objectName = 'wallet-setting';
protected $objectTitle = null;
}
diff --git a/src/app/Console/ObjectCommand.php b/src/app/Console/ObjectCommand.php
index fa3dbc21..48d78ec8 100644
--- a/src/app/Console/ObjectCommand.php
+++ b/src/app/Console/ObjectCommand.php
@@ -1,55 +1,53 @@
<?php
namespace App\Console;
-use Illuminate\Support\Facades\Cache;
-
abstract class ObjectCommand extends Command
{
/**
* Specify a command prefix, if any.
*
* For example, \App\Console\Commands\Scalpel\User\CreateCommand uses prefix 'scalpel'.
*
* @var string
*/
protected $commandPrefix = '';
/**
* The object class that we are operating on, for example \App\User::class
*
* @var string
*/
protected $objectClass;
/**
* The (simple) object name, such as 'domain' or 'user'. Corresponds with the mandatory command-line option
* to identify the object from its corresponding model.
*
* @var string
*/
protected $objectName;
/**
* The plural of the object name, if something specific (goose -> geese).
*
* @var string
*/
protected $objectNamePlural;
/**
* A column name other than the primary key can be used to identify an object, such as 'email' for users,
* 'namespace' for domains, and 'title' for SKUs.
*
* @var string
*/
protected $objectTitle;
/**
* Placeholder for column name attributes for objects, from which command-line switches and attribute names can be
* generated.
*
* @var array
*/
protected $properties;
}
diff --git a/src/app/Console/ObjectCreateCommand.php b/src/app/Console/ObjectCreateCommand.php
index c38e5d84..05d9440a 100644
--- a/src/app/Console/ObjectCreateCommand.php
+++ b/src/app/Console/ObjectCreateCommand.php
@@ -1,64 +1,58 @@
<?php
namespace App\Console;
/**
* This abstract class provides a means to treat objects in our model using CRUD.
*/
abstract class ObjectCreateCommand extends ObjectCommand
{
public function __construct()
{
$this->description = "Create a {$this->objectName}";
$this->signature = sprintf(
"%s%s:create",
$this->commandPrefix ? $this->commandPrefix . ":" : "",
$this->objectName
);
$class = new $this->objectClass();
foreach ($class->getFillable() as $fillable) {
$this->signature .= " {--{$fillable}=}";
}
parent::__construct();
}
- public function getProperties()
+ protected function getProperties()
{
if (!empty($this->properties)) {
return $this->properties;
}
$class = new $this->objectClass();
$this->properties = [];
foreach ($class->getFillable() as $fillable) {
$this->properties[$fillable] = $this->option($fillable);
}
return $this->properties;
}
/**
* 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
index f9e0d2d5..efcbb3f1 100644
--- a/src/app/Console/ObjectDeleteCommand.php
+++ b/src/app/Console/ObjectDeleteCommand.php
@@ -1,105 +1,67 @@
<?php
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.
*/
abstract class ObjectDeleteCommand extends ObjectCommand
{
public function __construct()
{
$this->description = "Delete a {$this->objectName}";
$this->signature = sprintf(
"%s%s:delete {%s}",
$this->commandPrefix ? $this->commandPrefix . ":" : "",
$this->objectName,
$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)) {
$this->signature .= " {--with-deleted : Consider deleted {$this->objectName}s}";
}
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.
*
* @return mixed
*/
public function handle()
{
$result = parent::handle();
if (!$result) {
return 1;
}
$argument = $this->argument($this->objectName);
$object = $this->getObject($this->objectClass, $argument, $this->objectTitle);
if (!$object) {
$this->error("No such {$this->objectName} {$argument}");
return 1;
}
if ($this->commandPrefix == 'scalpel') {
if ($object->deleted_at) {
$object->forceDeleteQuietly();
} else {
$object->deleteQuietly();
}
} else {
if ($object->deleted_at) {
$object->forceDelete();
} else {
$object->delete();
}
}
}
}
diff --git a/src/app/Console/ObjectUpdateCommand.php b/src/app/Console/ObjectUpdateCommand.php
index 17f12fef..250067be 100644
--- a/src/app/Console/ObjectUpdateCommand.php
+++ b/src/app/Console/ObjectUpdateCommand.php
@@ -1,101 +1,133 @@
<?php
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.
*/
abstract class ObjectUpdateCommand extends ObjectCommand
{
public function __construct()
{
$this->description = "Update a {$this->objectName}";
$this->signature = sprintf(
"%s%s:update {%s}",
$this->commandPrefix ? $this->commandPrefix . ":" : "",
$this->objectName,
$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)) {
return $this->properties;
}
$class = new $this->objectClass();
$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;
}
}
return $this->properties;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$argument = $this->argument($this->objectName);
$object = $this->getObject($this->objectClass, $argument, $this->objectTitle);
if (!$object) {
$this->error("No such {$this->objectName} {$argument}");
return 1;
}
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 {
$object->save();
}
}
}
diff --git a/src/tests/Feature/Console/Scalpel/Domain/UpdateCommandTest.php b/src/tests/Feature/Console/Scalpel/Domain/UpdateCommandTest.php
new file mode 100644
index 00000000..f9d34ea7
--- /dev/null
+++ b/src/tests/Feature/Console/Scalpel/Domain/UpdateCommandTest.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Tests\Feature\Console\Scalpel\Domain;
+
+use App\Domain;
+use Tests\TestCase;
+
+class UpdateCommandTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->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);
+ }
+}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Jun 9, 3:37 AM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
196745
Default Alt Text
(25 KB)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment