Skip to content

Custom Key Mapping

Ancestry supports custom polymorphic key mappings, allowing you to use any column as the identifier in hierarchy relationships instead of the default primary key.

By default, Ancestry uses each model’s primary key (id) to store ancestor/descendant relationships. However, you may need to:

  • Use UUIDs or ULIDs stored in a different column
  • Reference models by a unique business identifier (e.g., email, slug)
  • Support mixed key types across different models

Define key mappings in config/ancestry.php:

'morphKeyMap' => [
\App\Models\User::class => 'uuid',
\App\Models\Seller::class => 'ulid',
\App\Models\Organization::class => 'external_id',
],

Models not in the map fall back to their default primary key.

For stricter control, use enforceMorphKeyMap to require all models be explicitly mapped:

'enforceMorphKeyMap' => [
\App\Models\User::class => 'uuid',
\App\Models\Seller::class => 'ulid',
],

Using an unmapped model throws MorphKeyViolationException:

use App\Models\Post;
// Throws MorphKeyViolationException: Model [App\Models\Post] is not mapped
Ancestry::addToAncestry($post, 'category');

Note: Configure either morphKeyMap or enforceMorphKeyMap, not both.

You can also configure mappings at runtime via ModelRegistry:

use Cline\Ancestry\Database\ModelRegistry;
$registry = app(ModelRegistry::class);
// Optional mapping
$registry->morphKeyMap([
User::class => 'email',
]);
// Strict mapping
$registry->enforceMorphKeyMap([
User::class => 'email',
Seller::class => 'ulid',
]);
// Enable strict mode separately
$registry->morphKeyMap([User::class => 'email']);
$registry->requireKeyMap();
config/ancestry.php
'morphKeyMap' => [
\App\Models\User::class => 'email',
],
$manager = User::create(['name' => 'Manager', 'email' => 'manager@company.com']);
$seller = User::create(['name' => 'Seller', 'email' => 'seller@company.com']);
Ancestry::addToAncestry($manager, 'sales');
Ancestry::addToAncestry($seller, 'sales', $manager);
// Database stores 'manager@company.com' and 'seller@company.com' as the IDs
// Queries automatically use the email column
$ancestors = Ancestry::getAncestors($seller, 'sales');
// Returns the manager User model

Different models can use different key columns:

'morphKeyMap' => [
\App\Models\User::class => 'uuid', // Users identified by UUID
\App\Models\Team::class => 'slug', // Teams identified by slug
\App\Models\Department::class => 'id', // Departments use standard ID
],
$team = Team::create(['name' => 'Sales', 'slug' => 'sales-team']);
$user = User::create(['name' => 'John', 'uuid' => 'abc-123']);
Ancestry::addToAncestry($team, 'organization');
Ancestry::addToAncestry($user, 'organization', $team);
// team's slug 'sales-team' stored as ancestor_id
// user's uuid 'abc-123' stored as descendant_id

When using custom keys, ensure your morph type configuration matches:

Key TypeRecommended Morph Type
Integer IDsmorph or numericMorph
UUIDsuuidMorph
ULIDsulidMorph
Strings (email, slug)morph (varchar)
config/ancestry.php
'ancestor_morph_type' => 'morph', // varchar - flexible for any string
'descendant_morph_type' => 'morph',

Add indexes on your custom key columns for optimal query performance:

Schema::table('users', function (Blueprint $table) {
$table->index('email');
$table->index('uuid');
});

Reset the registry between tests to prevent state leakage:

use Cline\Ancestry\Database\ModelRegistry;
beforeEach(function () {
app(ModelRegistry::class)->reset();
});
test('uses custom key mapping', function () {
app(ModelRegistry::class)->morphKeyMap([
User::class => 'email',
]);
$parent = User::create(['email' => 'parent@test.com']);
$child = User::create(['email' => 'child@test.com']);
Ancestry::addToAncestry($parent, 'test');
Ancestry::addToAncestry($child, 'test', $parent);
expect(Ancestry::getAncestors($child, 'test'))->toHaveCount(1);
});
use Cline\Ancestry\Exceptions\MorphKeyViolationException;
try {
Ancestry::addToAncestry($unmappedModel, 'hierarchy');
} catch (MorphKeyViolationException $e) {
// Handle unmapped model when enforcement is enabled
Log::warning($e->getMessage());
}