diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f1ce98..13b789c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `level-up` will be documented in this file. +## [Unreleased] + +### Added +- Configurable table names: new `table_prefix` config key applies a prefix to every package table; new `tables` array allows per-table overrides for all 13 package tables. See the README "Customizing Table Names" section for usage. + +### Deprecated +- The top-level `'table'` config key is deprecated in favour of `'tables.experiences'`. The old key still works as a fallback for existing installs — no action required. + ## v2.0.0 - 2026-04-03 ### Added diff --git a/README.md b/README.md index 2c41e62..72079d0 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,56 @@ return [ ]; ``` +# Customizing Table Names + +If you're installing into an app that already has tables called `experiences`, `levels`, `tiers`, `multipliers`, `challenges`, or any of the package's other defaults, you can rename them via config — no need to patch published migrations. + +**Option 1 — Apply a single prefix to every package table:** + +Set an env var (no config publish required): + +```dotenv +LEVEL_UP_TABLE_PREFIX=levelup_ +``` + +…or publish the config and edit the prefix line: + +```bash +php artisan vendor:publish --tag=level-up-config +``` + +```php +// config/level-up.php +'table_prefix' => 'levelup_', +``` + +All package tables now use the prefix: `levelup_experiences`, `levelup_levels`, `levelup_tiers`, `levelup_multipliers`, `levelup_challenges`, and so on. + +**Option 2 — Rename specific tables:** + +Edit the `tables` array in the published config: + +```php +'tables' => [ + 'experiences' => 'xp_log', // renamed + 'levels' => 'user_tiers', // renamed + 'tiers' => 'rank_brackets', // renamed + // leave the rest at their defaults +], +``` + +**Combining both:** any value left equal to the default receives the `table_prefix`; any value you change is taken verbatim and the prefix is NOT applied. This lets you prefix everything but override one or two outliers: + +```php +'table_prefix' => 'lvl_', +'tables' => [ + 'levels' => 'xp_levels', // → 'xp_levels' (NO prefix) + // others stay default → all become 'lvl_' +], +``` + +> **Upgrading from v1.x or earlier v2:** the previous top-level `'table'` config key (used to override only the experiences table) still works as a fallback. New installations should prefer `'tables.experiences'` instead. + # Usage ## 💯 Experience Points (XP) diff --git a/UPGRADE.md b/UPGRADE.md index 56da481..1020109 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -184,6 +184,12 @@ class User extends Model The package's own primary keys can now be configured as `uuid` or `ulid` via `level-up.entities.id_type` (default remains `bigint`). Existing installs are unaffected on `composer update` — the setting only changes how *new* migrations build their tables. See the [Customizing Identifiers section in the README](README.md#customizing-identifiers) for the conversion path if you want to migrate an existing install. +### New: Configurable Table Names + +**Likelihood of Impact: Low** + +A new `table_prefix` and `tables` config block lets you rename any of the package's 13 tables. The previous top-level `'table'` key is now deprecated — if you customised it before, it still works as a fallback for `tables.experiences`. No action required; consider migrating to `tables.experiences` on your next config publish. See the [Customizing Table Names section in the README](README.md#customizing-table-names) for examples. + ### Bug Fixes Included - `levelUp()` now correctly fires `UserLevelledUp` events for all intermediate levels (previously only fired for the final level) diff --git a/config/level-up.php b/config/level-up.php index 82fe3ec..5a6ad9b 100644 --- a/config/level-up.php +++ b/config/level-up.php @@ -62,10 +62,49 @@ /* |-------------------------------------------------------------------------- - | Experience Table + | Table Prefix |-------------------------------------------------------------------------- | - | This value is the name of the table that will be used to store experience data. + | Prepended to every default package table name. Leave empty for no prefix. + | Per-table overrides in 'tables' below are taken verbatim and are NOT + | prefixed. + | + */ + 'table_prefix' => env('LEVEL_UP_TABLE_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Tables + |-------------------------------------------------------------------------- + | + | The table name used for each of the package's models. Leave a value + | equal to the default to apply table_prefix above; set it to any other + | string to override that table's name exactly (no prefix applied). + | + */ + 'tables' => [ + 'experiences' => 'experiences', + 'experience_audits' => 'experience_audits', + 'levels' => 'levels', + 'achievements' => 'achievements', + 'achievement_user' => 'achievement_user', + 'streaks' => 'streaks', + 'streak_histories' => 'streak_histories', + 'streak_activities' => 'streak_activities', + 'tiers' => 'tiers', + 'multipliers' => 'multipliers', + 'multiplier_scopes' => 'multiplier_scopes', + 'challenges' => 'challenges', + 'challenge_user' => 'challenge_user', + ], + + /* + |-------------------------------------------------------------------------- + | Experience Table (deprecated — use tables.experiences) + |-------------------------------------------------------------------------- + | + | Kept for backwards compatibility. If you customised this key in v1.x, + | it still works. New installs should prefer 'tables.experiences' above. | */ 'table' => 'experiences', diff --git a/database/migrations/add_level_relationship_to_users_table.php.stub b/database/migrations/add_level_relationship_to_users_table.php.stub index 15429a1..4433cd2 100644 --- a/database/migrations/add_level_relationship_to_users_table.php.stub +++ b/database/migrations/add_level_relationship_to_users_table.php.stub @@ -11,7 +11,7 @@ return new class extends Migration { $table->entityForeignId('level_id') ->after('remember_token') ->nullable() - ->constrained(); + ->constrained(table: config('level-up.tables.levels')); }); } diff --git a/database/migrations/add_multipliers_column_to_experience_audits_table.php.stub b/database/migrations/add_multipliers_column_to_experience_audits_table.php.stub index 970f144..31eb233 100644 --- a/database/migrations/add_multipliers_column_to_experience_audits_table.php.stub +++ b/database/migrations/add_multipliers_column_to_experience_audits_table.php.stub @@ -7,14 +7,14 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { - Schema::table('experience_audits', function (Blueprint $table) { + Schema::table(config('level-up.tables.experience_audits'), function (Blueprint $table) { $table->json('multipliers')->nullable()->after('reason'); }); } public function down(): void { - Schema::table('experience_audits', function (Blueprint $table) { + Schema::table(config('level-up.tables.experience_audits'), function (Blueprint $table) { $table->dropColumn('multipliers'); }); } diff --git a/database/migrations/add_streak_freeze_feature_columns_to_streaks_table.php.stub b/database/migrations/add_streak_freeze_feature_columns_to_streaks_table.php.stub index d4aea0d..8b74eb6 100644 --- a/database/migrations/add_streak_freeze_feature_columns_to_streaks_table.php.stub +++ b/database/migrations/add_streak_freeze_feature_columns_to_streaks_table.php.stub @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { - Schema::table('streaks', function (Blueprint $table) { + Schema::table(config('level-up.tables.streaks'), function (Blueprint $table) { $table->after('activity_at', function (Blueprint $table) { $table->timestamp('frozen_until')->nullable(); }); @@ -16,7 +16,7 @@ return new class extends Migration { public function down(): void { - Schema::table('streaks', function (Blueprint $table) { + Schema::table(config('level-up.tables.streaks'), function (Blueprint $table) { $table->dropColumn('frozen_until'); }); } diff --git a/database/migrations/add_tier_id_to_achievements_table.php.stub b/database/migrations/add_tier_id_to_achievements_table.php.stub index 09dd83e..197b47e 100644 --- a/database/migrations/add_tier_id_to_achievements_table.php.stub +++ b/database/migrations/add_tier_id_to_achievements_table.php.stub @@ -7,14 +7,14 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { - Schema::table('achievements', function (Blueprint $table) { - $table->entityForeignId('tier_id')->nullable()->constrained('tiers')->nullOnDelete(); + Schema::table(config('level-up.tables.achievements'), function (Blueprint $table) { + $table->entityForeignId('tier_id')->nullable()->constrained(table: config('level-up.tables.tiers'))->nullOnDelete(); }); } public function down(): void { - Schema::table('achievements', function (Blueprint $table) { + Schema::table(config('level-up.tables.achievements'), function (Blueprint $table) { $table->dropConstrainedForeignId('tier_id'); }); } diff --git a/database/migrations/add_tier_id_to_experiences_table.php.stub b/database/migrations/add_tier_id_to_experiences_table.php.stub index 69e07a3..37d27a1 100644 --- a/database/migrations/add_tier_id_to_experiences_table.php.stub +++ b/database/migrations/add_tier_id_to_experiences_table.php.stub @@ -7,14 +7,14 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { - Schema::table(config('level-up.table'), function (Blueprint $table) { - $table->entityForeignId('tier_id')->nullable()->constrained('tiers')->nullOnDelete(); + Schema::table(config('level-up.tables.experiences'), function (Blueprint $table) { + $table->entityForeignId('tier_id')->nullable()->constrained(table: config('level-up.tables.tiers'))->nullOnDelete(); }); } public function down(): void { - Schema::table(config('level-up.table'), function (Blueprint $table) { + Schema::table(config('level-up.tables.experiences'), function (Blueprint $table) { $table->dropConstrainedForeignId('tier_id'); }); } diff --git a/database/migrations/alter_experience_audits_type_to_string.php.stub b/database/migrations/alter_experience_audits_type_to_string.php.stub index b27324c..7da141e 100644 --- a/database/migrations/alter_experience_audits_type_to_string.php.stub +++ b/database/migrations/alter_experience_audits_type_to_string.php.stub @@ -7,14 +7,14 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { - Schema::table('experience_audits', function (Blueprint $table) { + Schema::table(config('level-up.tables.experience_audits'), function (Blueprint $table) { $table->string('type')->change(); }); } public function down(): void { - Schema::table('experience_audits', function (Blueprint $table) { + Schema::table(config('level-up.tables.experience_audits'), function (Blueprint $table) { $table->enum('type', ['add', 'remove', 'reset', 'level_up', 'tier_up', 'tier_down'])->change(); }); } diff --git a/database/migrations/create_achievement_user_pivot_table.php.stub b/database/migrations/create_achievement_user_pivot_table.php.stub index 426d74e..d7f9477 100644 --- a/database/migrations/create_achievement_user_pivot_table.php.stub +++ b/database/migrations/create_achievement_user_pivot_table.php.stub @@ -8,10 +8,10 @@ use LevelUp\Experience\Support\UserForeignKey; return new class extends Migration { public function up(): void { - Schema::create('achievement_user', function (Blueprint $table) { + Schema::create(config('level-up.tables.achievement_user'), function (Blueprint $table) { $table->entityId(); UserForeignKey::on($table)->constrained(config('level-up.user.users_table')); - $table->entityForeignId(column: 'achievement_id')->constrained(); + $table->entityForeignId(column: 'achievement_id')->constrained(table: config('level-up.tables.achievements')); $table->integer(column: 'progress')->nullable()->index(); $table->timestamps(); }); @@ -19,6 +19,6 @@ return new class extends Migration { public function down(): void { - Schema::dropIfExists('achievement_user'); + Schema::dropIfExists(config('level-up.tables.achievement_user')); } }; diff --git a/database/migrations/create_achievements_table.php.stub b/database/migrations/create_achievements_table.php.stub index 4c8f9ad..39e59e8 100644 --- a/database/migrations/create_achievements_table.php.stub +++ b/database/migrations/create_achievements_table.php.stub @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { - Schema::create('achievements', function (Blueprint $table) { + Schema::create(config('level-up.tables.achievements'), function (Blueprint $table) { $table->entityId(); $table->string('name'); $table->boolean('is_secret')->default(false); @@ -19,6 +19,6 @@ return new class extends Migration { public function down(): void { - Schema::dropIfExists('achievements'); + Schema::dropIfExists(config('level-up.tables.achievements')); } }; diff --git a/database/migrations/create_challenge_user_table.php.stub b/database/migrations/create_challenge_user_table.php.stub index 84df060..ca8b2b6 100644 --- a/database/migrations/create_challenge_user_table.php.stub +++ b/database/migrations/create_challenge_user_table.php.stub @@ -8,10 +8,10 @@ use LevelUp\Experience\Support\UserForeignKey; return new class extends Migration { public function up(): void { - Schema::create('challenge_user', function (Blueprint $table) { + Schema::create(config('level-up.tables.challenge_user'), function (Blueprint $table) { $table->entityId(); UserForeignKey::on($table)->constrained(config('level-up.user.users_table')); - $table->entityForeignId(column: 'challenge_id')->constrained(); + $table->entityForeignId(column: 'challenge_id')->constrained(table: config('level-up.tables.challenges')); $table->json(column: 'progress')->nullable(); $table->timestamp(column: 'completed_at')->nullable(); $table->timestamps(); @@ -23,6 +23,6 @@ return new class extends Migration { public function down(): void { - Schema::dropIfExists('challenge_user'); + Schema::dropIfExists(config('level-up.tables.challenge_user')); } }; diff --git a/database/migrations/create_challenges_table.php.stub b/database/migrations/create_challenges_table.php.stub index 414924e..5c368d9 100644 --- a/database/migrations/create_challenges_table.php.stub +++ b/database/migrations/create_challenges_table.php.stub @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { - Schema::create('challenges', function (Blueprint $table) { + Schema::create(config('level-up.tables.challenges'), function (Blueprint $table) { $table->entityId(); $table->string(column: 'name'); $table->text(column: 'description')->nullable(); @@ -25,6 +25,6 @@ return new class extends Migration { public function down(): void { - Schema::dropIfExists('challenges'); + Schema::dropIfExists(config('level-up.tables.challenges')); } }; diff --git a/database/migrations/create_experience_audits_table.php.stub b/database/migrations/create_experience_audits_table.php.stub index 0afd42d..0ff6b45 100644 --- a/database/migrations/create_experience_audits_table.php.stub +++ b/database/migrations/create_experience_audits_table.php.stub @@ -8,7 +8,7 @@ use LevelUp\Experience\Support\UserForeignKey; return new class extends Migration { public function up(): void { - Schema::create('experience_audits', function (Blueprint $table) { + Schema::create(config('level-up.tables.experience_audits'), function (Blueprint $table) { $table->entityId(); UserForeignKey::on($table)->constrained(config('level-up.user.users_table')); $table->integer('points')->index(); @@ -22,6 +22,6 @@ return new class extends Migration { public function down(): void { - Schema::dropIfExists('experience_audits'); + Schema::dropIfExists(config('level-up.tables.experience_audits')); } }; diff --git a/database/migrations/create_experiences_table.php.stub b/database/migrations/create_experiences_table.php.stub index be993ee..976b34c 100644 --- a/database/migrations/create_experiences_table.php.stub +++ b/database/migrations/create_experiences_table.php.stub @@ -9,10 +9,10 @@ return new class extends Migration { public function up() { - Schema::create(config('level-up.table'), function (Blueprint $table) { + Schema::create(config('level-up.tables.experiences'), function (Blueprint $table) { $table->entityId(); UserForeignKey::on($table)->constrained(config('level-up.user.users_table')); - $table->entityForeignId('level_id')->constrained(); + $table->entityForeignId('level_id')->constrained(table: config('level-up.tables.levels')); $table->integer('experience_points')->default(0)->index(); $table->timestamps(); }); @@ -20,6 +20,6 @@ return new class extends Migration public function down() { - Schema::dropIfExists(config('level-up.table')); + Schema::dropIfExists(config('level-up.tables.experiences')); } }; diff --git a/database/migrations/create_levels_table.php.stub b/database/migrations/create_levels_table.php.stub index ab19a65..8c41d91 100644 --- a/database/migrations/create_levels_table.php.stub +++ b/database/migrations/create_levels_table.php.stub @@ -8,7 +8,7 @@ return new class extends Migration { public function up() { - Schema::create('levels', function (Blueprint $table) { + Schema::create(config('level-up.tables.levels'), function (Blueprint $table) { $table->entityId(); $table->integer('level')->unique(); $table->integer('next_level_experience')->nullable()->index(); @@ -18,6 +18,6 @@ return new class extends Migration public function down() { - Schema::dropIfExists('levels'); + Schema::dropIfExists(config('level-up.tables.levels')); } }; diff --git a/database/migrations/create_multiplier_scopes_table.php.stub b/database/migrations/create_multiplier_scopes_table.php.stub index 39de1e6..8cc25ac 100644 --- a/database/migrations/create_multiplier_scopes_table.php.stub +++ b/database/migrations/create_multiplier_scopes_table.php.stub @@ -7,9 +7,9 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { - Schema::create('multiplier_scopes', function (Blueprint $table) { + Schema::create(config('level-up.tables.multiplier_scopes'), function (Blueprint $table) { $table->entityId(); - $table->entityForeignId('multiplier_id')->constrained()->cascadeOnDelete(); + $table->entityForeignId('multiplier_id')->constrained(table: config('level-up.tables.multipliers'))->cascadeOnDelete(); $table->string('scopeable_type'); $table->string('scopeable_id'); $table->timestamps(); @@ -21,6 +21,6 @@ return new class extends Migration { public function down(): void { - Schema::dropIfExists('multiplier_scopes'); + Schema::dropIfExists(config('level-up.tables.multiplier_scopes')); } }; diff --git a/database/migrations/create_multipliers_table.php.stub b/database/migrations/create_multipliers_table.php.stub index 2729316..96c1240 100644 --- a/database/migrations/create_multipliers_table.php.stub +++ b/database/migrations/create_multipliers_table.php.stub @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { - Schema::create('multipliers', function (Blueprint $table) { + Schema::create(config('level-up.tables.multipliers'), function (Blueprint $table) { $table->entityId(); $table->string('name'); $table->text('description')->nullable(); @@ -21,6 +21,6 @@ return new class extends Migration { public function down(): void { - Schema::dropIfExists('multipliers'); + Schema::dropIfExists(config('level-up.tables.multipliers')); } }; diff --git a/database/migrations/create_streak_activities_table.php.stub b/database/migrations/create_streak_activities_table.php.stub index 7ca4e6f..0bcd36a 100644 --- a/database/migrations/create_streak_activities_table.php.stub +++ b/database/migrations/create_streak_activities_table.php.stub @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { - Schema::create('streak_activities', function (Blueprint $table) { + Schema::create(config('level-up.tables.streak_activities'), function (Blueprint $table) { $table->entityId(); $table->string('name')->unique(); $table->text('description')->nullable(); @@ -17,6 +17,6 @@ return new class extends Migration { public function down(): void { - Schema::dropIfExists('streak_activities'); + Schema::dropIfExists(config('level-up.tables.streak_activities')); } }; diff --git a/database/migrations/create_streak_histories_table.php.stub b/database/migrations/create_streak_histories_table.php.stub index b4ace7d..3586fe7 100644 --- a/database/migrations/create_streak_histories_table.php.stub +++ b/database/migrations/create_streak_histories_table.php.stub @@ -9,10 +9,10 @@ return new class extends Migration { public function up(): void { - Schema::create(table: 'streak_histories', callback: function (Blueprint $table) { + Schema::create(table: config('level-up.tables.streak_histories'), callback: function (Blueprint $table) { $table->entityId(); UserForeignKey::on(table: $table)->constrained(table: config(key: 'level-up.user.users_table'))->cascadeOnDelete(); - $table->entityForeignId(column: 'activity_id')->constrained(table: 'streak_activities'); + $table->entityForeignId(column: 'activity_id')->constrained(table: config('level-up.tables.streak_activities')); $table->integer(column: 'count')->default(value: 1); $table->timestamp(column: 'started_at'); $table->timestamp(column: 'ended_at')->nullable(); @@ -22,6 +22,6 @@ return new class extends Migration public function down(): void { - Schema::dropIfExists('streak_histories'); + Schema::dropIfExists(config('level-up.tables.streak_histories')); } }; diff --git a/database/migrations/create_streaks_table.php.stub b/database/migrations/create_streaks_table.php.stub index 0f54809..9a0b1f8 100644 --- a/database/migrations/create_streaks_table.php.stub +++ b/database/migrations/create_streaks_table.php.stub @@ -8,10 +8,10 @@ use LevelUp\Experience\Support\UserForeignKey; return new class extends Migration { public function up(): void { - Schema::create('streaks', function (Blueprint $table) { + Schema::create(config('level-up.tables.streaks'), function (Blueprint $table) { $table->entityId(); UserForeignKey::on($table)->constrained()->onDelete('cascade'); - $table->entityForeignId(column: 'activity_id')->constrained('streak_activities')->onDelete('cascade'); + $table->entityForeignId(column: 'activity_id')->constrained(table: config('level-up.tables.streak_activities'))->onDelete('cascade'); $table->integer(column: 'count')->default(1); $table->timestamp(column: 'activity_at'); $table->timestamps(); @@ -20,6 +20,6 @@ return new class extends Migration { public function down(): void { - Schema::dropIfExists('streaks'); + Schema::dropIfExists(config('level-up.tables.streaks')); } }; diff --git a/database/migrations/create_tiers_table.php.stub b/database/migrations/create_tiers_table.php.stub index 187b2d8..40c909c 100644 --- a/database/migrations/create_tiers_table.php.stub +++ b/database/migrations/create_tiers_table.php.stub @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { - Schema::create('tiers', function (Blueprint $table) { + Schema::create(config('level-up.tables.tiers'), function (Blueprint $table) { $table->entityId(); $table->string('name')->unique(); $table->unsignedBigInteger('experience')->unique(); @@ -18,6 +18,6 @@ return new class extends Migration { public function down(): void { - Schema::dropIfExists('tiers'); + Schema::dropIfExists(config('level-up.tables.tiers')); } }; diff --git a/database/migrations/remove_level_id_column_from_users_table.php.stub b/database/migrations/remove_level_id_column_from_users_table.php.stub index d03ec04..e842562 100644 --- a/database/migrations/remove_level_id_column_from_users_table.php.stub +++ b/database/migrations/remove_level_id_column_from_users_table.php.stub @@ -15,7 +15,7 @@ return new class extends Migration { public function down(): void { Schema::table(config('level-up.user.users_table'), function (Blueprint $table) { - $table->entityForeignId('level_id')->nullable()->constrained(); + $table->entityForeignId('level_id')->nullable()->constrained(table: config('level-up.tables.levels')); }); } }; diff --git a/src/Concerns/HasAchievements.php b/src/Concerns/HasAchievements.php index a63d847..4c19f80 100644 --- a/src/Concerns/HasAchievements.php +++ b/src/Concerns/HasAchievements.php @@ -47,15 +47,13 @@ public function grantAchievement(Achievement $achievement, ?int $progress = null public function allAchievements(): BelongsToMany { - return $this->belongsToMany(related: config(key: 'level-up.models.achievement')) - ->withPivot(columns: 'progress'); + return $this->achievementsRelation(); } public function achievements(): BelongsToMany { - return $this->belongsToMany(related: config(key: 'level-up.models.achievement')) + return $this->achievementsRelation() ->withTimestamps() - ->withPivot(columns: 'progress') ->where('is_secret', false) ->using(config(key: 'level-up.models.achievement_user')); } @@ -82,8 +80,7 @@ public function getUserAchievements(): Collection public function achievementsWithProgress(): BelongsToMany { - return $this->belongsToMany(related: config(key: 'level-up.models.achievement')) - ->withPivot(columns: 'progress') + return $this->achievementsRelation() ->where('is_secret', false) ->wherePivotNotNull(column: 'progress'); } @@ -96,8 +93,7 @@ public function achievementsWithSpecificProgress(int $progress): BelongsToMany public function secretAchievements(): BelongsToMany { - return $this->belongsToMany(related: config(key: 'level-up.models.achievement')) - ->withPivot(columns: 'progress') + return $this->achievementsRelation() ->where('is_secret', true); } @@ -109,4 +105,12 @@ public function revokeAchievement(Achievement $achievement): void event(new AchievementRevoked(achievement: $achievement, user: $this)); } + + private function achievementsRelation(): BelongsToMany + { + return $this->belongsToMany( + related: config(key: 'level-up.models.achievement'), + table: config('level-up.tables.achievement_user'), + )->withPivot(columns: 'progress'); + } } diff --git a/src/Concerns/HasChallenges.php b/src/Concerns/HasChallenges.php index 82b55a7..52da9e5 100644 --- a/src/Concerns/HasChallenges.php +++ b/src/Concerns/HasChallenges.php @@ -95,13 +95,13 @@ public function unenrollFromChallenge(Challenge $challenge): void public function activeChallenges(): BelongsToMany { return $this->challengesRelation() - ->whereNull(columns: 'challenge_user.completed_at'); + ->whereNull(columns: config('level-up.tables.challenge_user').'.completed_at'); } public function completedChallenges(): BelongsToMany { return $this->challengesRelation() - ->whereNotNull(columns: 'challenge_user.completed_at'); + ->whereNotNull(columns: config('level-up.tables.challenge_user').'.completed_at'); } public function getChallengeProgress(Challenge $challenge): ?array @@ -126,7 +126,7 @@ public function getChallengeCompletionPercentage(Challenge $challenge): ?float private function challengesRelation(): BelongsToMany { - return $this->belongsToMany(related: config(key: 'level-up.models.challenge'), table: 'challenge_user') + return $this->belongsToMany(related: config(key: 'level-up.models.challenge'), table: config('level-up.tables.challenge_user')) ->using(config(key: 'level-up.models.challenge_user')) ->withPivot(columns: ['progress', 'completed_at']) ->withTimestamps(); diff --git a/src/Concerns/ResolvesConfiguredTable.php b/src/Concerns/ResolvesConfiguredTable.php new file mode 100644 index 0000000..8f388c1 --- /dev/null +++ b/src/Concerns/ResolvesConfiguredTable.php @@ -0,0 +1,15 @@ +setTable(config('level-up.tables.'.$this->configuredTableKey())); + } +} diff --git a/src/LevelUpServiceProvider.php b/src/LevelUpServiceProvider.php index fdaa0db..c40d4c7 100644 --- a/src/LevelUpServiceProvider.php +++ b/src/LevelUpServiceProvider.php @@ -14,11 +14,67 @@ class LevelUpServiceProvider extends PackageServiceProvider { + public static function resolveTables(string $prefix, array $overrides, mixed $legacyName): array + { + $defaults = [ + 'experiences' => 'experiences', + 'experience_audits' => 'experience_audits', + 'levels' => 'levels', + 'achievements' => 'achievements', + 'achievement_user' => 'achievement_user', + 'streaks' => 'streaks', + 'streak_histories' => 'streak_histories', + 'streak_activities' => 'streak_activities', + 'tiers' => 'tiers', + 'multipliers' => 'multipliers', + 'multiplier_scopes' => 'multiplier_scopes', + 'challenges' => 'challenges', + 'challenge_user' => 'challenge_user', + ]; + + $resolved = []; + + foreach ($defaults as $key => $default) { + $override = $overrides[$key] ?? null; + $overrideIsExplicit = is_string($override) && $override !== '' && $override !== $default; + + if ($key === 'experiences' + && ! $overrideIsExplicit + && is_string($legacyName) + && $legacyName !== '' + && $legacyName !== $default + ) { + $resolved[$key] = $legacyName; + + continue; + } + + if ($overrideIsExplicit) { + $resolved[$key] = $override; + + continue; + } + + $resolved[$key] = $prefix.$default; + } + + return $resolved; + } + public function bootingPackage(): void { $this->registerEntityKeyMacros(); } + public function packageBooted(): void + { + config()->set('level-up.tables', static::resolveTables( + prefix: (string) config('level-up.table_prefix', ''), + overrides: (array) config('level-up.tables', []), + legacyName: config('level-up.table'), + )); + } + public function configurePackage(Package $package): void { $package diff --git a/src/Models/Achievement.php b/src/Models/Achievement.php index ee72a30..3801290 100644 --- a/src/Models/Achievement.php +++ b/src/Models/Achievement.php @@ -9,20 +9,26 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use LevelUp\Experience\Concerns\HasConfigurableIds; +use LevelUp\Experience\Concerns\ResolvesConfiguredTable; class Achievement extends Model { - use HasConfigurableIds, HasFactory; + use HasConfigurableIds, HasFactory, ResolvesConfiguredTable; protected $guarded = []; public function users(): BelongsToMany { - return $this->belongsToMany(related: config(key: 'level-up.user.model')); + return $this->belongsToMany(related: config(key: 'level-up.user.model'), table: config('level-up.tables.achievement_user')); } public function tier(): BelongsTo { return $this->belongsTo(related: config(key: 'level-up.models.tier')); } + + protected function configuredTableKey(): string + { + return 'achievements'; + } } diff --git a/src/Models/Activity.php b/src/Models/Activity.php index 9957810..a797c76 100644 --- a/src/Models/Activity.php +++ b/src/Models/Activity.php @@ -8,12 +8,11 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use LevelUp\Experience\Concerns\HasConfigurableIds; +use LevelUp\Experience\Concerns\ResolvesConfiguredTable; class Activity extends Model { - use HasConfigurableIds, HasFactory; - - protected $table = 'streak_activities'; + use HasConfigurableIds, HasFactory, ResolvesConfiguredTable; protected $guarded = []; @@ -21,4 +20,9 @@ public function streaks(): HasMany { return $this->hasMany(related: config(key: 'level-up.models.streak')); } + + protected function configuredTableKey(): string + { + return 'streak_activities'; + } } diff --git a/src/Models/Challenge.php b/src/Models/Challenge.php index a0aaa73..bef8805 100644 --- a/src/Models/Challenge.php +++ b/src/Models/Challenge.php @@ -11,11 +11,12 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use InvalidArgumentException; use LevelUp\Experience\Concerns\HasConfigurableIds; +use LevelUp\Experience\Concerns\ResolvesConfiguredTable; use LevelUp\Experience\Contracts\ChallengeCondition; class Challenge extends Model { - use HasConfigurableIds, HasFactory; + use HasConfigurableIds, HasFactory, ResolvesConfiguredTable; protected $guarded = []; @@ -45,7 +46,7 @@ class Challenge extends Model public function users(): BelongsToMany { - return $this->belongsToMany(related: config(key: 'level-up.user.model'), table: 'challenge_user') + return $this->belongsToMany(related: config(key: 'level-up.user.model'), table: config('level-up.tables.challenge_user')) ->using(config(key: 'level-up.models.challenge_user')) ->withPivot(columns: ['progress', 'completed_at']) ->withTimestamps(); @@ -97,6 +98,11 @@ protected function validateRewards(): void ); } + protected function configuredTableKey(): string + { + return 'challenges'; + } + /** * @param array> $entries * @param array> $rules diff --git a/src/Models/Experience.php b/src/Models/Experience.php index a4ff7dd..a4034b1 100755 --- a/src/Models/Experience.php +++ b/src/Models/Experience.php @@ -7,19 +7,14 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use LevelUp\Experience\Concerns\HasConfigurableIds; +use LevelUp\Experience\Concerns\ResolvesConfiguredTable; class Experience extends Model { - use HasConfigurableIds; + use HasConfigurableIds, ResolvesConfiguredTable; protected $guarded = []; - public function __construct(array $attributes = []) - { - parent::__construct($attributes); - $this->table = config(key: 'level-up.table'); - } - public function user(): BelongsTo { return $this->belongsTo(config(key: 'level-up.user.model')); @@ -34,4 +29,9 @@ public function tier(): BelongsTo { return $this->belongsTo(related: config('level-up.models.tier')); } + + protected function configuredTableKey(): string + { + return 'experiences'; + } } diff --git a/src/Models/ExperienceAudit.php b/src/Models/ExperienceAudit.php index 0151366..ac7d5bc 100644 --- a/src/Models/ExperienceAudit.php +++ b/src/Models/ExperienceAudit.php @@ -7,11 +7,12 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use LevelUp\Experience\Concerns\HasConfigurableIds; +use LevelUp\Experience\Concerns\ResolvesConfiguredTable; use LevelUp\Experience\Enums\AuditType; class ExperienceAudit extends Model { - use HasConfigurableIds; + use HasConfigurableIds, ResolvesConfiguredTable; protected $guarded = []; @@ -24,4 +25,9 @@ public function user(): BelongsTo { return $this->belongsTo(config(key: 'level-up.user.model')); } + + protected function configuredTableKey(): string + { + return 'experience_audits'; + } } diff --git a/src/Models/Level.php b/src/Models/Level.php index 6b47798..0be0a3f 100644 --- a/src/Models/Level.php +++ b/src/Models/Level.php @@ -8,11 +8,12 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\UniqueConstraintViolationException; use LevelUp\Experience\Concerns\HasConfigurableIds; +use LevelUp\Experience\Concerns\ResolvesConfiguredTable; use LevelUp\Experience\Exceptions\LevelExistsException; class Level extends Model { - use HasConfigurableIds; + use HasConfigurableIds, ResolvesConfiguredTable; protected $guarded = []; @@ -43,4 +44,9 @@ public function users(): HasMany { return $this->hasMany(related: config(key: 'level-up.user.model')); } + + protected function configuredTableKey(): string + { + return 'levels'; + } } diff --git a/src/Models/Multiplier.php b/src/Models/Multiplier.php index 863a08f..34ad6e7 100644 --- a/src/Models/Multiplier.php +++ b/src/Models/Multiplier.php @@ -11,10 +11,11 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany; use InvalidArgumentException; use LevelUp\Experience\Concerns\HasConfigurableIds; +use LevelUp\Experience\Concerns\ResolvesConfiguredTable; class Multiplier extends Model { - use HasConfigurableIds; + use HasConfigurableIds, ResolvesConfiguredTable; protected $guarded = []; @@ -32,12 +33,14 @@ public function scopes(): HasMany public function tiers(): MorphToMany { - return $this->morphedByMany(config(key: 'level-up.models.tier'), 'scopeable', 'multiplier_scopes'); + return $this->morphedByMany(config(key: 'level-up.models.tier'), 'scopeable', config('level-up.tables.multiplier_scopes')) + ->using(config(key: 'level-up.models.multiplier_scope')); } public function users(): MorphToMany { - return $this->morphedByMany(config(key: 'level-up.user.model'), 'scopeable', 'multiplier_scopes'); + return $this->morphedByMany(config(key: 'level-up.user.model'), 'scopeable', config('level-up.tables.multiplier_scopes')) + ->using(config(key: 'level-up.models.multiplier_scope')); } public function scopeTo(Model ...$models): static @@ -127,4 +130,9 @@ protected function expired(Builder $query): void ->whereNotNull('expires_at') ->where('expires_at', '<', now()); } + + protected function configuredTableKey(): string + { + return 'multipliers'; + } } diff --git a/src/Models/MultiplierScope.php b/src/Models/MultiplierScope.php index 1c646c2..6b158be 100644 --- a/src/Models/MultiplierScope.php +++ b/src/Models/MultiplierScope.php @@ -4,14 +4,17 @@ namespace LevelUp\Experience\Models; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\MorphPivot; use Illuminate\Database\Eloquent\Relations\MorphTo; use LevelUp\Experience\Concerns\HasConfigurableIds; +use LevelUp\Experience\Concerns\ResolvesConfiguredTable; -class MultiplierScope extends Model +class MultiplierScope extends MorphPivot { - use HasConfigurableIds; + use HasConfigurableIds, ResolvesConfiguredTable; + + public $incrementing = true; protected $guarded = []; @@ -24,4 +27,9 @@ public function scopeable(): MorphTo { return $this->morphTo(); } + + protected function configuredTableKey(): string + { + return 'multiplier_scopes'; + } } diff --git a/src/Models/Pivots/AchievementUser.php b/src/Models/Pivots/AchievementUser.php index 4b29ad1..e08782a 100644 --- a/src/Models/Pivots/AchievementUser.php +++ b/src/Models/Pivots/AchievementUser.php @@ -6,8 +6,14 @@ use Illuminate\Database\Eloquent\Relations\Pivot; use LevelUp\Experience\Concerns\HasConfigurableIds; +use LevelUp\Experience\Concerns\ResolvesConfiguredTable; class AchievementUser extends Pivot { - use HasConfigurableIds; + use HasConfigurableIds, ResolvesConfiguredTable; + + protected function configuredTableKey(): string + { + return 'achievement_user'; + } } diff --git a/src/Models/Pivots/ChallengeUser.php b/src/Models/Pivots/ChallengeUser.php index bafea0f..3f18e4b 100644 --- a/src/Models/Pivots/ChallengeUser.php +++ b/src/Models/Pivots/ChallengeUser.php @@ -6,13 +6,19 @@ use Illuminate\Database\Eloquent\Relations\Pivot; use LevelUp\Experience\Concerns\HasConfigurableIds; +use LevelUp\Experience\Concerns\ResolvesConfiguredTable; class ChallengeUser extends Pivot { - use HasConfigurableIds; + use HasConfigurableIds, ResolvesConfiguredTable; protected $casts = [ 'completed_at' => 'datetime', 'progress' => 'array', ]; + + protected function configuredTableKey(): string + { + return 'challenge_user'; + } } diff --git a/src/Models/Streak.php b/src/Models/Streak.php index 0c7a170..4d08343 100644 --- a/src/Models/Streak.php +++ b/src/Models/Streak.php @@ -8,10 +8,11 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use LevelUp\Experience\Concerns\HasConfigurableIds; +use LevelUp\Experience\Concerns\ResolvesConfiguredTable; class Streak extends Model { - use HasConfigurableIds, HasFactory; + use HasConfigurableIds, HasFactory, ResolvesConfiguredTable; protected $guarded = []; @@ -29,4 +30,9 @@ public function activity(): BelongsTo { return $this->belongsTo(related: config(key: 'level-up.models.activity')); } + + protected function configuredTableKey(): string + { + return 'streaks'; + } } diff --git a/src/Models/StreakHistory.php b/src/Models/StreakHistory.php index 6aa572c..9b0904f 100644 --- a/src/Models/StreakHistory.php +++ b/src/Models/StreakHistory.php @@ -6,10 +6,11 @@ use Illuminate\Database\Eloquent\Model; use LevelUp\Experience\Concerns\HasConfigurableIds; +use LevelUp\Experience\Concerns\ResolvesConfiguredTable; class StreakHistory extends Model { - use HasConfigurableIds; + use HasConfigurableIds, ResolvesConfiguredTable; protected $guarded = []; @@ -17,4 +18,9 @@ class StreakHistory extends Model 'started_at' => 'datetime', 'ended_at' => 'datetime', ]; + + protected function configuredTableKey(): string + { + return 'streak_histories'; + } } diff --git a/src/Models/Tier.php b/src/Models/Tier.php index 8f4999c..e49165d 100644 --- a/src/Models/Tier.php +++ b/src/Models/Tier.php @@ -9,11 +9,12 @@ use Illuminate\Database\UniqueConstraintViolationException; use Illuminate\Support\Facades\DB; use LevelUp\Experience\Concerns\HasConfigurableIds; +use LevelUp\Experience\Concerns\ResolvesConfiguredTable; use LevelUp\Experience\Exceptions\TierExistsException; class Tier extends Model { - use HasConfigurableIds, HasFactory; + use HasConfigurableIds, HasFactory, ResolvesConfiguredTable; protected $guarded = []; @@ -47,6 +48,11 @@ public static function forPoints(int $points): ?static ->first(); } + protected function configuredTableKey(): string + { + return 'tiers'; + } + private static function createTier(array $tier): static { try { diff --git a/src/Services/ChallengeService.php b/src/Services/ChallengeService.php index dff1932..5243c85 100644 --- a/src/Services/ChallengeService.php +++ b/src/Services/ChallengeService.php @@ -90,7 +90,7 @@ protected function getEnrolledChallenges(Model $user, array $conditionTypes): Co ->active() ->whereHas('users', fn ($q) => $q ->where(column: config(key: 'level-up.user.foreign_key'), operator: '=', value: $user->id) - ->whereNull(columns: 'challenge_user.completed_at') + ->whereNull(columns: config('level-up.tables.challenge_user').'.completed_at') ) ->get() ->filter(fn (Challenge $challenge): bool => $this->hasMatchingCondition( @@ -148,7 +148,7 @@ protected function preloadConditionData(Model $user, Collection $challenges): ar return [ 'achievement_ids' => method_exists($user, 'allAchievements') - ? $user->allAchievements()->pluck('achievements.id')->all() + ? $user->allAchievements()->pluck(config('level-up.tables.achievements').'.id')->all() : [], 'activities' => $activityNames !== [] ? $activityModel::whereIn('name', $activityNames)->get()->keyBy('name') @@ -160,7 +160,7 @@ protected function evaluateChallenge(Model $user, Challenge $challenge, array $p { $pivot = $challenge->users() ->where(column: config(key: 'level-up.user.foreign_key'), operator: '=', value: $user->id) - ->whereNull(columns: 'challenge_user.completed_at') + ->whereNull(columns: config('level-up.tables.challenge_user').'.completed_at') ->first() ?->pivot; @@ -279,7 +279,7 @@ protected function checkCustomCondition(Model $user, array $condition): bool protected function completeChallenge(Model $user, Challenge $challenge): void { - $affected = DB::table('challenge_user') + $affected = DB::table(config('level-up.tables.challenge_user')) ->where(config(key: 'level-up.user.foreign_key'), $user->id) ->where('challenge_id', $challenge->id) ->whereNull('completed_at') @@ -345,7 +345,7 @@ protected function rewardAchievement(Model $user, array $reward): void return; } - if ($user->allAchievements()->where('achievements.id', $achievement->id)->exists()) { + if ($user->allAchievements()->where(config('level-up.tables.achievements').'.id', $achievement->id)->exists()) { return; } diff --git a/tests/Concerns/ResolvesConfiguredTableTest.php b/tests/Concerns/ResolvesConfiguredTableTest.php new file mode 100644 index 0000000..74658df --- /dev/null +++ b/tests/Concerns/ResolvesConfiguredTableTest.php @@ -0,0 +1,24 @@ +set('level-up.tables.experiences', 'resolved_xp_table'); + + $model = new ConfiguredTableModel; + + expect($model->getTable())->toBe('resolved_xp_table'); +}); + +it('reflects later changes to the config when a new instance is built', function (): void { + config()->set('level-up.tables.experiences', 'first'); + $first = new ConfiguredTableModel; + + config()->set('level-up.tables.experiences', 'second'); + $second = new ConfiguredTableModel; + + expect($first->getTable())->toBe('first') + ->and($second->getTable())->toBe('second'); +}); diff --git a/tests/Config/ResolveTablesTest.php b/tests/Config/ResolveTablesTest.php new file mode 100644 index 0000000..1c1615d --- /dev/null +++ b/tests/Config/ResolveTablesTest.php @@ -0,0 +1,96 @@ +toBe([ + 'experiences' => 'experiences', + 'experience_audits' => 'experience_audits', + 'levels' => 'levels', + 'achievements' => 'achievements', + 'achievement_user' => 'achievement_user', + 'streaks' => 'streaks', + 'streak_histories' => 'streak_histories', + 'streak_activities' => 'streak_activities', + 'tiers' => 'tiers', + 'multipliers' => 'multipliers', + 'multiplier_scopes' => 'multiplier_scopes', + 'challenges' => 'challenges', + 'challenge_user' => 'challenge_user', + ]); +}); + +it('applies the prefix to every default table', function (): void { + $resolved = LevelUpServiceProvider::resolveTables( + prefix: 'lvl_', + overrides: [], + legacyName: null, + ); + + expect($resolved['experiences'])->toBe('lvl_experiences') + ->and($resolved['levels'])->toBe('lvl_levels') + ->and($resolved['streak_activities'])->toBe('lvl_streak_activities') + ->and($resolved['multiplier_scopes'])->toBe('lvl_multiplier_scopes') + ->and($resolved['challenge_user'])->toBe('lvl_challenge_user'); +}); + +it('uses an explicit override verbatim, ignoring the prefix', function (): void { + $resolved = LevelUpServiceProvider::resolveTables( + prefix: 'lvl_', + overrides: ['experiences' => 'xp_log', 'tiers' => 'rank_brackets'], + legacyName: null, + ); + + expect($resolved['experiences'])->toBe('xp_log') + ->and($resolved['tiers'])->toBe('rank_brackets') + ->and($resolved['levels'])->toBe('lvl_levels'); +}); + +it('falls back to the legacy table key for experiences when it differs from the default', function (): void { + $resolved = LevelUpServiceProvider::resolveTables( + prefix: '', + overrides: [], + legacyName: 'custom_xp', + ); + + expect($resolved['experiences'])->toBe('custom_xp'); +}); + +it('ignores the legacy table key when it equals the default', function (): void { + $resolved = LevelUpServiceProvider::resolveTables( + prefix: 'lvl_', + overrides: [], + legacyName: 'experiences', + ); + + expect($resolved['experiences'])->toBe('lvl_experiences'); +}); + +it('prefers tables.experiences over the legacy table key when both are set', function (): void { + $resolved = LevelUpServiceProvider::resolveTables( + prefix: '', + overrides: ['experiences' => 'new_xp'], + legacyName: 'old_xp', + ); + + expect($resolved['experiences'])->toBe('new_xp'); +}); + +it('treats an empty-string override as unset and falls through to the prefix', function (): void { + $resolved = LevelUpServiceProvider::resolveTables( + prefix: 'lvl_', + overrides: ['experiences' => '', 'tiers' => ''], + legacyName: '', + ); + + expect($resolved['experiences'])->toBe('lvl_experiences') + ->and($resolved['tiers'])->toBe('lvl_tiers'); +}); diff --git a/tests/Fixtures/ConfiguredTableModel.php b/tests/Fixtures/ConfiguredTableModel.php new file mode 100644 index 0000000..4d365c7 --- /dev/null +++ b/tests/Fixtures/ConfiguredTableModel.php @@ -0,0 +1,18 @@ +assertTrue(Schema::hasTable('pfx_experiences')); + $this->assertTrue(Schema::hasTable('pfx_experience_audits')); + $this->assertTrue(Schema::hasTable('pfx_levels')); + $this->assertTrue(Schema::hasTable('pfx_achievement_user')); + $this->assertTrue(Schema::hasTable('pfx_streaks')); + $this->assertTrue(Schema::hasTable('pfx_streak_histories')); + $this->assertTrue(Schema::hasTable('pfx_streak_activities')); + $this->assertTrue(Schema::hasTable('pfx_tiers')); + $this->assertTrue(Schema::hasTable('pfx_multipliers')); + $this->assertTrue(Schema::hasTable('pfx_multiplier_scopes')); + $this->assertTrue(Schema::hasTable('pfx_challenges')); + $this->assertTrue(Schema::hasTable('pfx_challenge_user')); + } + + public function test_explicit_override_escapes_the_prefix(): void + { + $this->assertTrue(Schema::hasTable('custom_achievements')); + $this->assertFalse(Schema::hasTable('pfx_achievements')); + $this->assertFalse(Schema::hasTable('pfx_custom_achievements')); + } + + public function test_models_write_to_prefixed_tables_with_working_foreign_keys(): void + { + $user = new User; + $user->fill([ + 'name' => 'Test', + 'email' => 'test@example.com', + 'password' => bcrypt('password'), + 'email_verified_at' => now(), + ])->save(); + + Level::add( + ['level' => 1, 'next_level_experience' => null], + ['level' => 2, 'next_level_experience' => 100], + ); + + Experience::create([ + 'user_id' => $user->id, + 'level_id' => 1, + 'experience_points' => 50, + ]); + + $this->assertSame(1, DB::table('pfx_experiences')->count()); + $this->assertFalse(Schema::hasTable('experiences')); + } + + public function test_grant_achievement_writes_to_prefixed_pivot_table(): void + { + $user = User::query()->create([ + 'name' => 'Achievement User', + 'email' => 'ach@example.com', + 'password' => bcrypt('password'), + ]); + + $achievement = Achievement::factory()->create(); + + $user->grantAchievement($achievement); + + $this->assertSame(1, DB::table('pfx_achievement_user')->count()); + $this->assertFalse(Schema::hasTable('achievement_user')); + } + + public function test_enroll_in_challenge_writes_to_prefixed_pivot_table(): void + { + $user = User::query()->create([ + 'name' => 'Challenge User', + 'email' => 'chal@example.com', + 'password' => bcrypt('password'), + ]); + + $challenge = Challenge::factory()->create(); + + $user->enrollInChallenge($challenge); + + $this->assertSame(1, DB::table('pfx_challenge_user')->count()); + $this->assertFalse(Schema::hasTable('challenge_user')); + } + + public function test_multiplier_users_relation_writes_to_prefixed_pivot_table(): void + { + $user = User::query()->create([ + 'name' => 'Multiplier User', + 'email' => 'mult@example.com', + 'password' => bcrypt('password'), + ]); + + $multiplier = Multiplier::query()->create([ + 'name' => 'Test Multiplier', + 'multiplier' => 2, + 'is_active' => true, + ]); + + $multiplier->users()->attach($user); + + $this->assertSame(1, DB::table('pfx_multiplier_scopes')->count()); + $this->assertFalse(Schema::hasTable('multiplier_scopes')); + } + + protected function getEnvironmentSetUp($app): void + { + $app['config']->set('level-up.table_prefix', 'pfx_'); + $app['config']->set('level-up.tables.achievements', 'custom_achievements'); + + $app['config']->set('level-up.tables', \LevelUp\Experience\LevelUpServiceProvider::resolveTables( + prefix: 'pfx_', + overrides: $app['config']->get('level-up.tables', []), + legacyName: $app['config']->get('level-up.table'), + )); + + parent::getEnvironmentSetUp($app); + } +}