Page MenuHomePhorge

No OneTemporary

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

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)

Event Timeline