Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "Workspace management package for Laravel applications",
"type": "library",
"license": "MIT",
"version": "1.1.1",
"version": "1.2.0",
"authors": [
{
"name": "WhileSmart",
Expand Down
10 changes: 5 additions & 5 deletions src/Http/Controllers/WorkspaceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ public function members(Workspace $workspace): JsonResponse

$members = $workspace->members()
->with(['roleAssignments' => function ($query) use ($workspace) {
$query->where('context_type', config('workspaces.workspace_model', Workspace::class))
$query->where('context_type', $workspace->getMorphClass())
->where('context_id', $workspace->id)
->with('role');
}])
Expand Down Expand Up @@ -446,7 +446,7 @@ protected function userCanAccess(Workspace $workspace): bool
}

foreach (Role::cases() as $role) {
if ($role->canAccess() && $user->hasRole($role->value, config('workspaces.workspace_model', Workspace::class), $workspace->id)) {
if ($role->canAccess() && $user->hasRole($role->value, $workspace->getMorphClass(), $workspace->id)) {
return true;
}
}
Expand All @@ -463,7 +463,7 @@ protected function userCanManage(Workspace $workspace): bool
}

foreach (Role::cases() as $role) {
if ($role->canManage() && $user->hasRole($role->value, config('workspaces.workspace_model', Workspace::class), $workspace->id)) {
if ($role->canManage() && $user->hasRole($role->value, $workspace->getMorphClass(), $workspace->id)) {
return true;
}
}
Expand All @@ -479,7 +479,7 @@ protected function userIsOwner(Workspace $workspace): bool
return false;
}

return $user->hasRole(Role::OWNER->value, config('workspaces.workspace_model', Workspace::class), $workspace->id);
return $user->hasRole(Role::OWNER->value, $workspace->getMorphClass(), $workspace->id);
}

protected function getUserRole(Workspace $workspace): ?string
Expand All @@ -491,7 +491,7 @@ protected function getUserRole(Workspace $workspace): ?string
}

foreach (Role::byPrecedence() as $role) {
if ($user->hasRole($role->value, config('workspaces.workspace_model', Workspace::class), $workspace->id)) {
if ($user->hasRole($role->value, $workspace->getMorphClass(), $workspace->id)) {
return $role->value;
}
}
Expand Down
23 changes: 18 additions & 5 deletions src/Models/Workspace.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ public function members()
'id',
'id',
'assignable_id'
)->where('role_assignments.context_type', static::class)
->where('role_assignments.assignable_type', config('workspaces.user_model', 'App\\Models\\User'));
)->where('role_assignments.context_type', $this->getMorphClass())
Comment thread
nfebe marked this conversation as resolved.
->where('role_assignments.assignable_type', $this->userMorphClass());
}

public function invitations(): HasMany
Expand All @@ -93,7 +93,7 @@ public function pendingInvitations(): HasMany
public function roleAssignments()
{
return $this->hasMany('Whilesmart\\Roles\\Models\\RoleAssignment', 'context_id')
->where('context_type', static::class);
->where('context_type', $this->getMorphClass());
}

public function getOwnersAttribute()
Expand All @@ -103,7 +103,7 @@ public function getOwnersAttribute()
$query->whereHas('role', function ($q) {
$q->where('slug', 'workspace-owner');
})
->where('context_type', static::class)
->where('context_type', $this->getMorphClass())
->where('context_id', $this->id);
})
->get();
Expand All @@ -116,7 +116,7 @@ public function getAdminsAttribute()
$query->whereHas('role', function ($q) {
$q->whereIn('slug', ['workspace-owner', 'workspace-admin']);
})
->where('context_type', static::class)
->where('context_type', $this->getMorphClass())
->where('context_id', $this->id);
})
->get();
Expand Down Expand Up @@ -148,4 +148,17 @@ public function setSetting(string $key, mixed $value): self

return $this;
}

/**
* The name a user is stored under in a polymorphic column.
*
* A role assignment is written through the user's own morph relation, so
* this is what was stored, which is not always the configured class.
*/
private function userMorphClass(): string
{
$model = config('workspaces.user_model', 'App\\Models\\User');

return (new $model)->getMorphClass();
}
}
22 changes: 18 additions & 4 deletions src/Traits/HasWorkspaces.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ public static function workspaceModel(): string
return config('workspaces.workspace_model', Workspace::class);
}

/**
* The name a workspace is stored under in a polymorphic column.
*
* Not the class name. A model may answer with something else, which is how
* a morph map works and how a subclass keeps the name its table already
* holds, and a comparison against the class name misses both.
*/
public static function workspaceMorphClass(): string
{
$model = static::workspaceModel();

return (new $model)->getMorphClass();
}

public function ownedWorkspaces(): MorphMany
{
return $this->morphMany(static::workspaceModel(), 'owner');
Expand All @@ -31,8 +45,8 @@ public function workspaces()
'id',
'id',
'context_id'
)->where('role_assignments.assignable_type', static::class)
->where('role_assignments.context_type', static::workspaceModel());
)->where('role_assignments.assignable_type', $this->getMorphClass())
->where('role_assignments.context_type', static::workspaceMorphClass());
}

public function pendingWorkspaceInvitations()
Expand Down Expand Up @@ -90,15 +104,15 @@ public function joinWorkspace(Workspace $workspace, ?string $role = null): void
$role = $role ?? Role::default()->value;

if (method_exists($this, 'assignRole')) {
$this->assignRole($role, static::workspaceModel(), $workspace->id);
$this->assignRole($role, static::workspaceMorphClass(), $workspace->id);
}
}

public function leaveWorkspace(Workspace $workspace): void
{
if (method_exists($this, 'removeRole')) {
foreach (Role::values() as $role) {
$this->removeRole($role, static::workspaceModel(), $workspace->id);
$this->removeRole($role, static::workspaceMorphClass(), $workspace->id);
}
}
}
Expand Down
78 changes: 78 additions & 0 deletions tests/Feature/SubclassedWorkspaceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tests\Feature;

use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Hash;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
Expand All @@ -15,8 +16,30 @@ class SubWorkspace extends Workspace
protected $table = 'workspaces';
}

class RenamedWorkspace extends Workspace
{
protected $table = 'workspaces';

public function getMorphClass(): string
{
return Workspace::class;
}
}

class SubclassedWorkspaceTest extends TestCase
{
/**
* A morph map is global and outlives the test that set one, so anything
* after it would resolve against a map it never asked for.
*/
protected function tearDown(): void
{
Relation::morphMap([], false);
Relation::requireMorphMap(false);

parent::tearDown();
}

#[Test]
public function members_resolve_when_the_workspace_model_is_subclassed(): void
{
Expand Down Expand Up @@ -47,4 +70,59 @@ public function members_resolve_when_the_workspace_model_is_subclassed(): void
// base Workspace class while the assignment stores the subclass.
$this->assertCount(1, $workspace->members()->get());
}

#[Test]
public function members_resolve_when_a_subclass_keeps_the_name_already_stored(): void
{
config(['workspaces.workspace_model' => RenamedWorkspace::class]);

$user = User::create([
'name' => 'Renamed Owner',
'email' => 'renamed-'.uniqid().'@example.com',
'password' => Hash::make('password'),
]);

$workspace = RenamedWorkspace::create([
'name' => 'Renamed Workspace',
'type' => 'team',
'owner_type' => User::class,
'owner_id' => $user->id,
]);

RoleAssignment::create([
'assignable_type' => User::class,
'assignable_id' => $user->id,
'role_id' => Role::where('slug', 'owner')->first()->id,
'context_type' => Workspace::class,
'context_id' => $workspace->id,
]);

$this->assertCount(1, $workspace->members()->get());
$this->assertContains($workspace->id, $user->workspaces()->pluck('workspaces.id')->all());
}

#[Test]
public function members_resolve_when_the_user_model_is_behind_a_morph_map(): void
{
Relation::morphMap(['user' => User::class]);

$user = User::create([
'name' => 'Mapped Owner',
'email' => 'mapped-'.uniqid().'@example.com',
'password' => Hash::make('password'),
]);

$workspace = Workspace::create([
'name' => 'Mapped Workspace',
'type' => 'team',
'owner_type' => 'user',
'owner_id' => $user->id,
]);

// Written the way a role assignment is written, through the user's own
// morph relation, which stores the alias rather than the class.
$user->assignRole('owner', $workspace->getMorphClass(), $workspace->id);

$this->assertCount(1, $workspace->members()->get());
}
}
Loading