Skip to content

refactor: trait relations through private helpers so host apps can alias#123

Merged
cjmellor merged 2 commits into
mainfrom
refactor/trait-relation-helpers
May 17, 2026
Merged

refactor: trait relations through private helpers so host apps can alias#123
cjmellor merged 2 commits into
mainfrom
refactor/trait-relation-helpers

Conversation

@cjmellor
Copy link
Copy Markdown
Owner

@cjmellor cjmellor commented May 14, 2026

Summary

A reviewer hit a wall trying to add HasChallenges to their user model because they already defined their own challenges() relation. The PHP escape hatch for that collision is trait aliasing:

use HasChallenges {
    challenges as packageChallenges;
}

…but our traits were self-calling $this->challenges(), $this->streaks(), $this->experience(), $this->experienceHistory() internally. After the alias, those calls land on the host's method and every internal flow breaks.

This PR makes each trait route its own internals through a private helper, leaving the public method as a thin façade that's safe to alias out of the way.

  • HasChallengeschallengesRelation()
  • HasStreaksstreaksRelation()
  • GiveExperienceexperienceRelation(), experienceHistoryRelation()

GiveExperience also leaned on the $this->experience magic property in addPoints, deductPoints, setPoints, levelUp, getLevel, getPoints, and the PointsIncreased dispatch path. Eloquent resolves that property by invoking the public method name, so aliasing broke it too. A new loadedExperience() helper resolves the model once, caches it under the canonical experience relation slot via setRelation(), and is used by all of those callsites. The dispatch path now receives the resolved model explicitly instead of reading it back off $this.

Pattern mirrors HasAchievements::achievementsRelation() from #122 / commit a000bf4.

Scope notes

  • Branches off main, not off feat/configurable-table-names, so the package's beta testers on that branch aren't disturbed.
  • HasAchievements already has its private helper landing via feat: configurable table names #122, so it's deliberately untouched here.
  • HasTiers::getTier() calls $this->experience() — that's a cross-trait public-API dependency on GiveExperience, which is correct and outside this PR's scope.

@cjmellor cjmellor self-assigned this May 14, 2026
cjmellor added 2 commits May 17, 2026 21:40
Host applications that already expose `challenges()`, `streaks()`,
`experience()`, or `experienceHistory()` on their User model previously
could not adopt this package without a method-name collision. PHP's
`use Trait { method as alias; }` syntax handles the collision, but the
trait's own internals were still self-calling `$this->challenges()`
etc., which after aliasing resolves to the host's method and breaks
every flow.

Each trait now exposes its relation through a private helper that all
internal callers use, leaving the public method as a thin façade safe
to alias away:

- HasChallenges     -> challengesRelation()
- HasStreaks        -> streaksRelation()
- GiveExperience    -> experienceRelation(), experienceHistoryRelation()

GiveExperience also previously relied on the `$this->experience`
magic property in several places, which Eloquent resolves via the
public method name and so breaks under aliasing as well. A new
`loadedExperience()` helper resolves the model once, caches it under
the canonical `experience` relation slot, and is used by addPoints,
deductPoints, setPoints, levelUp, getLevel, and getPoints. The
PointsIncreased dispatch path now receives the resolved model
explicitly instead of reading it back off the user.

Adds tests/Fixtures/AliasingUser that aliases every relation method
and throws from a host-defined stub, plus tests/Concerns/
TraitAliasingTest exercising enrollInChallenge, recordStreak,
addPoints, levelUp, and the inverse paths end-to-end against it.
Rector's TablePropertyToTableAttributeRector converted the fixture to
use the #[Table(name: 'users')] attribute. That attribute is honored on
Laravel 13 but silently ignored on Laravel 12.x, so the model falls
back to its class-name default (`aliasing_users`), which has no
matching table and breaks CI's prefer-lowest job.
@cjmellor cjmellor force-pushed the refactor/trait-relation-helpers branch from 522764b to 981b413 Compare May 17, 2026 20:51
@cjmellor cjmellor merged commit 3a9d06b into main May 17, 2026
11 checks passed
@cjmellor cjmellor deleted the refactor/trait-relation-helpers branch May 17, 2026 20:52
cjmellor added a commit that referenced this pull request May 24, 2026
[2.x] revert: drop trait method aliasing (#123)
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