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 @@ -224,6 +224,18 @@ return true; } + /** + * Checks that a model is soft-deletable + * + * @param string $class Model class name + * + * @return bool + */ + protected function isSoftDeletable($class) + { + return class_exists($class) && method_exists($class, 'forceDelete'); + } + /** * Return a string for output, with any additional attributes specified as well. * 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 @@ -2,8 +2,6 @@ namespace App\Console; -use Illuminate\Database\Eloquent\SoftDeletes; - /** * This abstract class provides a means to treat objects in our model using CRUD. */ @@ -19,9 +17,7 @@ $this->objectName ); - $classes = class_uses_recursive($this->objectClass); - - if (in_array(SoftDeletes::class, $classes)) { + if ($this->isSoftDeletable($this->objectClass)) { $this->signature .= " {--with-deleted : Consider deleted {$this->objectName}s}"; } diff --git a/src/app/Console/ObjectListCommand.php b/src/app/Console/ObjectListCommand.php --- a/src/app/Console/ObjectListCommand.php +++ b/src/app/Console/ObjectListCommand.php @@ -2,8 +2,6 @@ namespace App\Console; -use Illuminate\Database\Eloquent\SoftDeletes; - /** * This abstract class provides a means to treat objects in our model using CRUD, with the exception that * this particular abstract class lists objects. @@ -22,9 +20,7 @@ $this->signature .= "{$this->objectName}s"; } - $classes = class_uses_recursive($this->objectClass); - - if (in_array(SoftDeletes::class, $classes)) { + if ($this->isSoftDeletable($this->objectClass)) { $this->signature .= " {--with-deleted : Include deleted {$this->objectName}s}"; } @@ -41,10 +37,7 @@ */ public function handle() { - $classes = class_uses_recursive($this->objectClass); - - // @phpstan-ignore-next-line - if (in_array(SoftDeletes::class, $classes) && $this->option('with-deleted')) { + if ($this->isSoftDeletable($this->objectClass) && $this->option('with-deleted')) { $objects = $this->objectClass::withTrashed(); } else { $objects = new $this->objectClass(); diff --git a/src/app/Console/ObjectRelationListCommand.php b/src/app/Console/ObjectRelationListCommand.php --- a/src/app/Console/ObjectRelationListCommand.php +++ b/src/app/Console/ObjectRelationListCommand.php @@ -2,6 +2,7 @@ namespace App\Console; +use Illuminate\Database\Eloquent\SoftDeletingScope; use Illuminate\Support\Str; /** @@ -24,6 +25,13 @@ */ protected $objectRelationArgs = []; + /** + * The "relation" model class. + * + * @var string + */ + protected $objectRelationClass; + /** * Supplement the base command constructor with a derived or generated signature and * description. @@ -42,6 +50,14 @@ $this->objectName ); + if (empty($this->objectRelationClass)) { + $this->objectRelationClass = "App\\" . rtrim(ucfirst($this->objectRelation), 's'); + } + + if ($this->isSoftDeletable($this->objectRelationClass)) { + $this->signature .= " {--with-deleted : Include deleted objects}"; + } + $this->signature .= " {--attr=* : Attributes other than the primary unique key to include}"; parent::__construct(); @@ -59,7 +75,8 @@ $object = $this->getObject( $this->objectClass, $argument, - $this->objectTitle + $this->objectTitle, + true ); if (!$object) { @@ -81,6 +98,11 @@ ($result instanceof \Illuminate\Database\Eloquent\Relations\Relation) || ($result instanceof \Illuminate\Database\Eloquent\Builder) ) { + // @phpstan-ignore-next-line + if ($this->isSoftDeletable($this->objectRelationClass) && $this->option('with-deleted')) { + $result->withoutGlobalScope(SoftDeletingScope::class); + } + $result = $result->get(); } diff --git a/src/tests/Feature/Console/User/UsersTest.php b/src/tests/Feature/Console/User/UsersTest.php --- a/src/tests/Feature/Console/User/UsersTest.php +++ b/src/tests/Feature/Console/User/UsersTest.php @@ -2,10 +2,30 @@ namespace Tests\Feature\Console\User; +use Illuminate\Support\Facades\Queue; use Tests\TestCase; class UsersTest extends TestCase { + /** + * {@inheritDoc} + */ + public function setUp(): void + { + parent::setUp(); + + $this->deleteTestUser('user@force-delete.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestUser('user@force-delete.com'); + + parent::tearDown(); + } /** * Test command runs */ @@ -26,5 +46,27 @@ $this->assertStringContainsString("ned@kolab.org", $output); $this->assertStringContainsString("joe@kolab.org", $output); $this->assertStringContainsString("jack@kolab.org", $output); + + // Test behaviour with deleted users + Queue::fake(); + + // Note: User:users() uses entitlements to get the owned users, + // so we add a single entitlement, then we can soft-delete the user + $user = $this->getTestUser('user@force-delete.com'); + $storage = \App\Sku::withEnvTenantContext()->where('title', 'storage')->first(); + $user->assignSku($storage, 1); + $user->delete(); + + $code = \Artisan::call("user:users {$user->email} --attr=email"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame('', trim($output)); + + $code = \Artisan::call("user:users {$user->email} --with-deleted --attr=email"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame("{$user->id} {$user->email}", trim($output)); } }