Skip to content

chore: Record the 1.2.0 changelog - #24

Merged
nfebe merged 1 commit into
devfrom
chore/release-1-2-0
Sep 20, 2026
Merged

nfebe merged 1 commit into
devfrom
chore/release-1-2-0

Conversation

@nfebe

@nfebe nfebe commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Adds the 1.2.0 entry. The release itself is the dev to main sync that follows.

Releases 1.2.0.
@sourceant

sourceant Bot commented Sep 20, 2026

Copy link
Copy Markdown

Code Review Summary

✨ Release 1.2.0 syncs main with dev, shipping the polymorphic morph-name fixes for workspace membership. Read paths that previously compared context_type/assignable_type against configured class names now compare against the model's morph name: WorkspaceController uses $workspace->getMorphClass() for the member listing and for every hasRole() access/manage/owner/role resolution, Workspace::members(), roleAssignments(), owners and admins do the same, and HasWorkspaces::workspaces() filters the user side with $this->getMorphClass(). Two new helpers centralise this — HasWorkspaces::workspaceMorphClass() and a Workspace::userMorphClass() that resolves the configured user model's morph name. Coverage adds a RenamedWorkspace subclass that keeps the name already stored and a morph-mapped user case, with a tearDown resetting the global morph map. composer.json bumps to 1.2.0 and CHANGELOG.md records both fixes. Review feedback is limited to consistency/documentation polish rather than defects.

🚀 Key Improvements

  • src/Models/Workspace.php, src/Http/Controllers/WorkspaceController.php, src/Traits/HasWorkspaces.php: role and member queries resolve the polymorphic context_type/assignable_type via getMorphClass() / the new morph-name helpers, so hosts with subclasses or a registered morph map no longer see empty workspace and member lists.
  • src/Traits/HasWorkspaces.php: the user side of workspaces() now matches on the runtime instance's morph name, keeping a user's own membership lookups consistent with how the assignment was written.
  • tests/Feature/SubclassedWorkspaceTest.php: new cases cover a subclass that preserves the already-stored morph name and a user model behind a morph map, and the suite now resets the global morph map in tearDown so later tests do not inherit it.

💡 Minor Suggestions

  • The new helper is private, yet this release is explicitly about making subclasses work (see the RenamedWorkspace/subclass tests). A host that subclasses Workspace and needs to customise how the related user morph name is resolved (e.g. a subclassed or morph-mapped user model) cannot override a private method. Prefer protected so it is consistent with the subclass-friendly intent of the change and with HasWorkspaces::workspaceMorphClass() being overridable.
  • HasWorkspaces::workspaceMorphClass() is public static and overridable, but the symmetric userMorphClass() here is private. Because the release is explicitly about subclassing/morph-map support, a Workspace subclass should be able to override how the user-side morph name is resolved (e.g. when the user model is subclassed and the configured class differs from the concrete type). Raising visibility to protected restores that symmetry without changing behaviour.
  • The write path stores the role assignment's context_type from static::workspaceMorphClass() (a fresh instance of the configured model), whereas the read paths that must find it — Workspace::members(), Workspace::roleAssignments() and WorkspaceController::hasRole() — all compare context_type against the actual $workspace instance's getMorphClass(). Storing the name of the instance being joined keeps the write and read sides of the contract on the same value, mirroring the fix already applied on the read side. In the common case (config == concrete type) the values are identical, so this is a robustness alignment rather than a behaviour change.
  • Same alignment as joinWorkspace: removal should target the morph name the assignment was stored under. Using the $workspace instance here matches the context_type value used by members() and the controller's hasRole() checks, so assignments created and removed through the same workspace instance resolve consistently.
  • The added helper resolves the user's polymorphic name from config('workspaces.user_model'), but the reverse relation HasWorkspaces::workspaces() matches the same column with the runtime instance's $this->getMorphClass(). The two directions of the same membership therefore agree only while workspaces.user_model names the exact runtime user class; a host whose runtime user is a subclass (or morph-mapped model) that config does not name will still get an empty members() result. members() cannot see the runtime user, so the assumption is inherent — but it should be stated so the coupling is discoverable rather than re-discovered as a bug. Documenting the required config value is a safe, behavior-preserving clarification.

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

Comment thread src/Models/Workspace.php
* 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new helper is private, yet this release is explicitly about making subclasses work (see the RenamedWorkspace/subclass tests). A host that subclasses Workspace and needs to customise how the related user morph name is resolved (e.g. a subclassed or morph-mapped user model) cannot override a private method. Prefer protected so it is consistent with the subclass-friendly intent of the change and with HasWorkspaces::workspaceMorphClass() being overridable.

Suggested change
private function userMorphClass(): string
protected function userMorphClass(): string

Comment thread src/Models/Workspace.php
* 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HasWorkspaces::workspaceMorphClass() is public static and overridable, but the symmetric userMorphClass() here is private. Because the release is explicitly about subclassing/morph-map support, a Workspace subclass should be able to override how the user-side morph name is resolved (e.g. when the user model is subclassed and the configured class differs from the concrete type). Raising visibility to protected restores that symmetry without changing behaviour.

Suggested change
private function userMorphClass(): string
+ protected function userMorphClass(): string


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The write path stores the role assignment's context_type from static::workspaceMorphClass() (a fresh instance of the configured model), whereas the read paths that must find it — Workspace::members(), Workspace::roleAssignments() and WorkspaceController::hasRole() — all compare context_type against the actual $workspace instance's getMorphClass(). Storing the name of the instance being joined keeps the write and read sides of the contract on the same value, mirroring the fix already applied on the read side. In the common case (config == concrete type) the values are identical, so this is a robustness alignment rather than a behaviour change.

Suggested change
$this->assignRole($role, static::workspaceMorphClass(), $workspace->id);
+ $this->assignRole($role, $workspace->getMorphClass(), $workspace->id);

if (method_exists($this, 'removeRole')) {
foreach (Role::values() as $role) {
$this->removeRole($role, static::workspaceModel(), $workspace->id);
$this->removeRole($role, static::workspaceMorphClass(), $workspace->id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same alignment as joinWorkspace: removal should target the morph name the assignment was stored under. Using the $workspace instance here matches the context_type value used by members() and the controller's hasRole() checks, so assignments created and removed through the same workspace instance resolve consistently.

Suggested change
$this->removeRole($role, static::workspaceMorphClass(), $workspace->id);
+ $this->removeRole($role, $workspace->getMorphClass(), $workspace->id);

Comment thread src/Models/Workspace.php
return $this;
}

/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added helper resolves the user's polymorphic name from config('workspaces.user_model'), but the reverse relation HasWorkspaces::workspaces() matches the same column with the runtime instance's $this->getMorphClass(). The two directions of the same membership therefore agree only while workspaces.user_model names the exact runtime user class; a host whose runtime user is a subclass (or morph-mapped model) that config does not name will still get an empty members() result. members() cannot see the runtime user, so the assumption is inherent — but it should be stated so the coupling is discoverable rather than re-discovered as a bug. Documenting the required config value is a safe, behavior-preserving clarification.

Suggested change
/**
+ /**
+ * 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.
+ *
+ * This derives the value from `workspaces.user_model`, whereas
+ * `HasWorkspaces::workspaces()` derives it from the runtime user instance.
+ * The two agree only when `workspaces.user_model` names the runtime user
+ * class (morph map aliases included); otherwise `members()` can miss rows
+ * that `$user->workspaces()` still returns.
+ */
+ private function userMorphClass(): string
+ {
+ $model = config('workspaces.user_model', 'App\\Models\\User');
+
+ return (new $model)->getMorphClass();
+ }

@nfebe
nfebe changed the base branch from main to dev September 20, 2026 10:27
@nfebe nfebe changed the title chore: Sync main with dev chore: Record the 1.2.0 changelog Sep 20, 2026
@nfebe
nfebe merged commit 017e332 into dev Sep 20, 2026
3 checks passed
@nfebe
nfebe deleted the chore/release-1-2-0 branch September 20, 2026 10:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant