feat(entities): configurable package entity ID type (bigint/uuid/ulid)#127
Merged
Conversation
Proof-of-shape for the (b) half of UUID/ULID support: lets host apps choose the primary-key column type for the package's own entities so package IDs can be exposed as UUIDs/ULIDs on a public API surface (christoph-kluge's ask on PR #124). Wires: - `level-up.entities.id_type` config (default `bigint`, accepts `uuid`/`ulid`) - `Blueprint::entityId()` / `Blueprint::entityForeignId()` macros registered in `LevelUpServiceProvider::bootingPackage()`. Now safe in tests after PR #125 moved package migrations to run after providers boot. - `Concerns\HasConfigurableIds` trait — composes Laravel's `HasUniqueIds`, reads config at runtime, no-ops in bigint mode. - Applied to the `Experience` model + `create_experiences_table` stub as the shape proof; remaining 12 models + migrations follow next. BC: with default config, the trait's `uniqueIds()` returns `[]` so `HasUniqueIds`' creating-hook no-ops, `getKeyType()` returns `int`, and `getIncrementing()` returns `true` — byte-identical to today. Existing installs see no change.
…package Completes the (b) half of UUID/ULID support after the scaffold (9471843). Trait rollout: applies `HasConfigurableIds` to the remaining 11 package models so every entity honours `level-up.entities.id_type` at runtime — Achievement, Activity, Challenge, ExperienceAudit, Level, Multiplier, MultiplierScope, Streak, StreakHistory, Tier, plus the two pivot models. Macro rollout: swaps `\$table->id()` -> `entityId()` and FKs to package entities -> `entityForeignId()` across 16 migration stubs. User FKs stay as `foreignId()` (host-driven, governed by the (a) PR). Polymorphic morph fix: `multiplier_scopes.scopeable_id` becomes a string column so it can store both package-entity keys and host-User keys under any combination of the two ID-type knobs. Three sites in `Multiplier.php` cast `(string) \$key` for Postgres strict-mode correctness. BC: with default `bigint`, byte-identical to today — trait's `uniqueIds()` returns `[]` so the HasUniqueIds boot hook no-ops, `getKeyType()` returns 'int', `getIncrementing()` returns true. The scopeable_id string column accepts integers under SQLite type affinity. Tests: 216/216 pass on default config. UUID/ULID matrix follow-up.
`HasUniqueIds` only generates IDs when its `$usesUniqueIds` flag is true; the flag must be flipped per-instance via an `initialize*` trait hook (this is how `HasUuids`/`HasUlids` do it). Without this, `Model::save()` skips the `setUniqueIds()` call so package entities in uuid/ulid mode insert with a NULL primary key and trip a NOT NULL constraint. Latent in the scaffold commit (9471843) — undetected because the default bigint mode doesn't need the flag set. Surfaced once uuid/ulid mode was actually run via LEVELUP_TEST_KEY_TYPE.
`tests/TestCase.php` reads `LEVELUP_TEST_KEY_TYPE` (default `bigint`) and writes it to `level-up.entities.id_type` config before package migrations run. This lets the same 216-test suite exercise all three ID-type modes from a single invocation. `tests/Pest.php` captures the seeded levels into `\$this->levels` (keyed by level number) so per-test assertions can resolve a real level id instead of hardcoding `1`/`2`/`3` — which only matched the bigint auto-increment sequence. Test files that compared `level_id` to integer literals now reference `\$this->levels[N]->id`. CI: adds two extra cells to the existing matrix on PHP 8.3 + Laravel 12.* + prefer-stable, one for `uuid` and one for `ulid`. The base matrix continues to run `bigint` across PHP 8.3/8.4/8.5 + Laravel 12/13 unchanged. Verified locally: 216/216 pass under each of bigint, uuid, ulid.
README: adds a "Customizing Identifiers" section explaining `level-up.entities.id_type` and a collapsible accordion with an AI prompt that users can paste into their assistant to generate the conversion migrations for switching an existing install from bigint to uuid/ulid. Existing-install conversion is intentionally not automated by the package — it depends on data volume, downtime tolerance, and DB driver specifics. The published-config snippet in the README also gains the new `entities` block so users see the knob immediately on install. UPGRADE.md: adds a brief "New: Configurable Entity ID Type" entry under v2.0 pointing at the README section. Existing installs are unaffected on `composer update` since the default is `bigint`. v2 upgrade skill (resources/boost/skills/level-up-upgrade-v2): inserts Step 9 that asks whether the user wants to switch IDs and explicitly delegates the conversion to the README accordion rather than attempting it inside the skill. CHANGELOG: left untouched — handled at release time.
- Tighten HasConfigurableIds: only flip usesUniqueIds for uuid/ulid; newUniqueId() throws InvalidArgumentException on unknown types instead of silently returning an empty string. - Extract $resolveIdType closure in LevelUpServiceProvider so both Blueprint macros share a single validate-and-throw site. - Cast (string) on scopeable_id test inserts for parity with production casts (Postgres-strict safe). - Inline the multiplier_scopes clarifier next to the table list in the README AI prompt.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Resolves the (b) half of christoph-kluge's UUID/ULID feedback on #124. Lets host apps choose how the package's own primary keys are stored:
bigint(default),uuid, orulid. Useful when you want to expose Experience or Achievement records on a public API without sequential IDs leaking row counts.level-up.entities.id_type(default'bigint', accepts'uuid'/'ulid')$table->entityId()and$table->entityForeignId('xxx_id'), registered inLevelUpServiceProvider::bootingPackage()(safe after refactor(tests): run package migrations after providers boot #125's lifecycle fix)LevelUp\Experience\Concerns\HasConfigurableIdsapplied to all 13 package models — composes Laravel'sHasUniqueIds, reads config at runtime, no-ops inbigintmodemultiplier_scopes.scopeable_idis now astringcolumn with(string) $keycasts at the three write/match sites inMultiplier.php— this is the follow-up feat(user): configurable foreign key type (bigint/uuid/ulid) #124 flagged in its "Out of scope" notes, addressed here because it's needed for (b) to work under any combination of the two ID-type knobsLEVELUP_TEST_KEY_TYPEenv var drivesTestCase::getEnvironmentSetUp, and the CI matrix gainsuuid+ulidcells on PHP 8.3 + Laravel 12.* — the same 216-test suite runs under each of the three modesWhy two knobs, not one
user.foreign_key_type(feat(user): configurable foreign key type (bigint/uuid/ulid) #124, the (a) half) is dictated by whatever the host's User PK uses. Out of the package's control.entities.id_type(this PR) is the package's own decision about what its API exposes.Coupling them forces hacks in legitimate mismatched scenarios (e.g. a bigint User table but UUID package IDs). They land as siblings.
Backwards compatibility
Default
'bigint'is byte-identical to today. Existing installs see no change oncomposer update:HasConfigurableIds::uniqueIds()returns[]in bigint mode, soHasUniqueIds' creating hook is a no-opgetKeyType()returns'int',getIncrementing()returnstrueentityId()/entityForeignId()macros expand to the sameid()/foreignId()callsmultiplier_scopes.scopeable_idbecomesstringbut SQLite/MySQL type affinity accepts ints transparently, and the(string)casts inMultiplier.phpkeep Postgres strict mode correctExisting installs
This setting only changes how new migrations build their tables. Existing installs can't be flipped automatically because column types are baked in. The README's new "Customizing Identifiers" section contains a copy-paste-able AI prompt that generates the conversion migrations tailored to your schema and database driver. The v2 upgrade skill gains a Step 9 that asks the user whether to switch and points to that section.
Out of scope
scopeable_typemorph map for short aliasesSchema::getColumnType()matches config