Skip to content

♻️ refactor(ownership): move to polymorphic relationship for governed models#120

Open
mikebronner wants to merge 4 commits intomasterfrom
feature/issue-46-polymorphic-governed-models
Open

♻️ refactor(ownership): move to polymorphic relationship for governed models#120
mikebronner wants to merge 4 commits intomasterfrom
feature/issue-46-polymorphic-governed-models

Conversation

@mikebronner
Copy link
Copy Markdown
Owner

@mikebronner mikebronner commented Mar 30, 2026

Summary

Refactors model ownership tracking from the governor_owned_by column approach to a polymorphic relationship via a new governor_ownables pivot table. The old column is deprecated but preserved for backward compatibility.

Changes

  • GovernorOwnable model — new Eloquent model for the governor_ownables polymorphic pivot table with ownable() MorphTo and owner() BelongsTo relationships
  • Governable trait — added governorOwner() MorphOne relationship; getGovernorOwnedByAttribute and getOwnedByAttribute accessors now read from polymorphic table with fallback to deprecated column
  • N+1 fix — removed unsetRelation('governorOwner') calls from accessors and BasePolicy so eager loading works correctly
  • CreatedListener — creates GovernorOwnable record on model creation via firstOrCreate; handles auth user, explicit column value, and no-owner paths
  • CreatingListener — maintains backward compat by still setting the deprecated governor_owned_by column
  • CreatingInvitationListener — uses conditional auth check instead of direct ownedBy() association
  • TeamtransferOwnership() creates/updates polymorphic record; getOwnerNameAttribute falls back through polymorphic → deprecated owner
  • Governing trait — new governorOwnedTeams() method for polymorphic team ownership lookup
  • TeamInvitation notification — null-safe accessors for owner/team names
  • LaravelGovernorUpgradeTo0130 seeder — migrates existing governor_owned_by column data to the polymorphic table
  • Migrationcreate_governor_ownables_table with morphs columns and unique constraint
  • Binary sqlite removedtests/database/database.sqlite removed from Git tracking

Acceptance Criteria

  • Governed models use polymorphic relationships instead of direct foreign keys
  • Migration is provided to move existing data to the new structure without data loss
  • All Governor queries and policies work correctly after the migration

Test Coverage

  • Unit test: governed model can be attached/detached via polymorphic relationship
  • Integration test: migration runs cleanly on existing data
  • Regression test: all existing policy and permission checks pass after refactor

Test Coverage

  • PolymorphicOwnershipTest (16 tests) — GovernorOwnable model, governorOwner() MorphOne, attach/detach, accessors, eager loading, fallback to deprecated column
  • UpgradeTo0130SeederTest (3 tests) — migration seeder execution, idempotency, empty model set handling
  • CreatedListenerTest — polymorphic ownership creation for governable/non-governable models, explicit owner, no-auth paths
  • CreatingInvitationListenerTest (3 tests) — auth-conditional ownership, polymorphic record creation, no-auth behavior
  • TransferOwnershipTest — polymorphic record creation/update on transfer, relation clearing, ownerName fallback
  • GoverningTest (3 tests) — governorOwnedTeams() filtering, exclusion, empty collection
  • TeamInvitationTest — null-safe owner/team name handling in notification
  • 349 total tests, 596 assertions — all passing across PHP 8.2–8.5, Laravel 11–13

Fixes #46

…les table

- Create governor_ownables pivot table with morphs relationship
- Update Governable trait: governorOwner() MorphOne, getOwnedByAttribute accessor
- Create GovernorOwnable model for ownership records
- Update CreatingListener to avoid overriding explicit governor_owned_by column
- Update CreatedListener to create polymorphic records with correct ownership
- Update CreatingInvitationListener to set deprecated column for backward compat
- Update BasePolicy to check ownership via polymorphic relationship
- Update Team::transferOwnership() to update polymorphic record
- Add Governing::governorOwnedTeams() for polymorphic team lookup
- Mark ownedBy() relationship as deprecated, maintain backward compat via accessor
- Add migration seeder LaravelGovernorUpgradeTo0130 to migrate existing data
- Mark GovernorOwnedByField trait as deprecated
- Tolerate null ownership in TeamInvitation notification
- Register GovernorOwnable model in config

Breaking Changes:
- ownedBy() no longer returns a BelongsTo relationship; use getOwnedByAttribute accessor
- Tests updated to work with new polymorphic ownership model
- All AC verified: polymorphic relationships work, migrations preserve data, policies use new structure
@mikebronner mikebronner marked this pull request as ready for review March 30, 2026 23:51
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 30, 2026

Codecov Report

❌ Patch coverage is 89.10891% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.59%. Comparing base (e3cc99c) to head (e633c18).

Files with missing lines Patch % Lines
src/Listeners/CreatedListener.php 79.48% 8 Missing ⚠️
src/Traits/Governable.php 85.00% 3 Missing ⚠️

❌ Your patch check has failed because the patch coverage (89.10%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.
❌ Your project check has failed because the head coverage (71.59%) is below the target coverage (90.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##             master     #120      +/-   ##
============================================
+ Coverage     70.65%   71.59%   +0.94%     
- Complexity      373      390      +17     
============================================
  Files            53       54       +1     
  Lines          1537     1609      +72     
============================================
+ Hits           1086     1152      +66     
- Misses          451      457       +6     
Files with missing lines Coverage Δ
src/GovernorOwnable.php 100.00% <100.00%> (ø)
src/Listeners/CreatingInvitationListener.php 100.00% <100.00%> (ø)
src/Listeners/CreatingListener.php 94.73% <100.00%> (+0.29%) ⬆️
src/Notifications/TeamInvitation.php 82.60% <100.00%> (+1.65%) ⬆️
src/Policies/BasePolicy.php 88.54% <100.00%> (+0.24%) ⬆️
src/Team.php 84.21% <100.00%> (+3.77%) ⬆️
src/Traits/Governing.php 90.56% <100.00%> (-9.44%) ⬇️
src/Traits/Governable.php 88.57% <85.00%> (+1.90%) ⬆️
src/Listeners/CreatedListener.php 83.33% <79.48%> (+20.17%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Owner Author

@mikebronner mikebronner left a comment

Choose a reason for hiding this comment

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

Review Checklist

  • Test gap: no new tests for polymorphic ownership — The diff introduces GovernorOwnable, the governorOwner() relationship, the getGovernorOwnedByAttribute accessor, and the LaravelGovernorUpgradeTo0130 seeder — none of which have dedicated test coverage. The issue AC explicitly requires: (1) unit test for attach/detach via polymorphic relationship, (2) integration test for migration on existing data, (3) regression tests for policy checks post-refactor. The existing 321 tests pass (good), but they don't exercise the new polymorphic code paths directly. Codecov patch/project checks are failing, confirming coverage dropped.
  • Performance: getGovernorOwnedByAttribute forces fresh query on every access — Line 113 of Governable.php calls $this->unsetRelation('governorOwner') before every access, preventing eager-loading from ever being used. If governor_owned_by is accessed in a loop (e.g., a policy check across a collection), this creates an N+1 query. Consider removing the unsetRelation call and letting normal eager-loading work, or document why forced-fresh is necessary.
  • Binary sqlite committedtests/database/database.sqlite is a binary diff. This makes schema changes invisible in review and causes merge conflicts. Consider generating the test DB from migrations at test setup instead.

…ry sqlite

- Add PolymorphicOwnershipTest with 16 tests covering GovernorOwnable model,
  governorOwner() MorphOne relationship, attach/detach, accessors,
  eager loading, and fallback to deprecated column
- Add UpgradeTo0130SeederTest with 3 tests covering migration seeder
- Remove unsetRelation() calls from getOwnedByAttribute and
  getGovernorOwnedByAttribute accessors to allow eager loading and
  prevent N+1 queries in collection loops
- Remove binary tests/database/database.sqlite from Git tracking
  (already in .gitignore, generated fresh by AlwaysRunFirstTest)
Copy link
Copy Markdown
Owner Author

@mikebronner mikebronner left a comment

Choose a reason for hiding this comment

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

Review Checklist

  • CI: codecov/patch and codecov/project checks are failing. All test matrix jobs pass (10/10 PHP/Laravel combos green), but coverage gates must be green before approval. The new code in CreatedListener, CreatingListener, BasePolicy, Governable trait, Team, and GovernorOwnable needs sufficient test coverage to satisfy the patch threshold.

…verage

- Remove unsetRelation('governorOwner') in BasePolicy to allow eager loading
- Add tests for CreatedListener polymorphic ownership creation
- Add tests for CreatingInvitationListener auth-conditional ownership
- Add tests for Team::transferOwnership polymorphic record updates
- Add tests for Team::getOwnerNameAttribute polymorphic fallback
- Add tests for Governing::governorOwnedTeams()
- Add test for TeamInvitation notification null-safe accessors

Addresses PR #120 review feedback on test coverage gaps and N+1 query.
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.

Move To Polymorphic Relationship For Governed Models

1 participant