Eloquent JSON Relations

repository·main·Indexed 22 days ago

https://github.com/staudenmeir/eloquent-json-relations

A Laravel Eloquent extension that enables the use of JSON columns as foreign keys for various Eloquent relationships. It supports standard relationships like BelongsTo, HasOne, and HasMany, as well as specialized Many-to-Many and Has-Many-Through relationships using JSON arrays. Compatible with MySQL 5.7+, MariaDB 10.2+, PostgreSQL 9.3+, SQLite 3.38+, and SQL Server 2016+.

Tokens
4.9K
Snippets
14
Records
21
Agent score
77%

What's inside eloquent-json-relations

  1. Overview of supported Eloquent relationships

    main

    Eloquent JSON Relations extends Laravel's Eloquent ORM to allow relationships to be defined using JSON foreign keys.

    It adds support for the following standard relationships:

    • BelongsTo
    • HasOne
    • HasMany
    • HasOneThrough
    • HasManyThrough
    • MorphTo
    • MorphOne
    • MorphMany

    Additionally, it provides specialized support for:

    • Many-to-Many relationships using JSON arrays.
    • Has-Many-Through relationships using JSON arrays.
  2. Ensure Referential Integrity for JSON Foreign Keys

    main

    To maintain referential integrity for foreign keys stored in JSON, you can use generated/computed columns. This allows you to apply standard database foreign key constraints to values inside a JSON field.

    MySQL / MariaDB

    Use storedAs with a wrapped JSON path in your migration:

    $table->json('options');
    $locale_id = DB::connection()->getQueryGrammar()->wrap('options->locale_id');
    $table->unsignedBigInteger('locale_id')->storedAs($locale_id);
    $table->foreign('locale_id')->references('id')->on('locales');

    SQL Server

    Use computed with a CAST to the appropriate type and ensure it is persisted():

    $table->json('options');
    $locale_id = DB::connection()->getQueryGrammar()->wrap('options->locale_id');
    $locale_id = 'CAST('.$locale_id.' AS INT)';
    $table->computed('locale_id', $locale_id)->persisted();
    $table->foreign('locale_id')->references('id')->on('locales');
  3. Implement Many-To-Many Relationships with JSON Arrays of IDs

    main

    The package provides BelongsToJson and HasManyJson to handle many-to-many relationships where the pivot data is stored as a simple array of IDs within a JSON field.

    Include the \Staudenmeir\\EloquentJsonRelations\\HasJsonRelationships trait in both models. The BelongsToJson relationship supports standard pivot methods: attach(), detach(), sync(), and toggle().

    class User extends Model
    {
        use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
    
        protected $casts = [
           'options' => 'json',
        ];
        
        public function roles(): \Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson
        {
            return $this->belongsToJson(Role::class, 'options->role_ids');
        }
    }
    
    class Role extends Model
    {
        use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
    
        public function users(): \Staudenmeir\EloquentJsonRelations\Relations\HasManyJson
        {
            return $this->hasManyJson(User::class, 'options->role_ids');
        }
    }
    
    // Usage on BelongsToJson side:
    $user->roles()->attach([1, 2])->save();
    $user->roles()->detach([2])->save();
    $user->roles()->sync([1, 3])->save();
    $user->roles()->toggle([2, 3])->save();
  4. Implement Has-Many-Through-Json Relationships

    main

    You can define HasManyThroughJson relationships when the JSON column resides in an intermediate table. This requires the staudenmeir/eloquent-has-many-deep package.

    1. Install staudenmeir/eloquent-has-many-deep.
    2. Add the \Staudenmeir\\EloquentHasManyDeep\\HasRelationships trait to the parent model.
    3. Pass the JSON column path as a \Staudenmeir\\EloquentJsonRelations\\JsonKey object.
    class Role extends Model
    {
        use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
    
        public function projects()
        {
            return $this->hasManyThroughJson(
                Project::class,
                User::class,
                new \Staudenmeir\EloquentJsonRelations\JsonKey('options->role_ids')
            );
        }
    }
  5. Concatenate JSON Relationships into Deep Relationships

    main

    By combining this package with staudenmeir/eloquent-has-many-deep, you can include JSON-based relationships within a deep relationship chain.

    To do this, add the \Staudenmeir\\EloquentHasManyDeep\\HasRelationships trait to the parent model and use hasManyDeepFromRelations() to concatenate existing relationships.

    class User extends Model
    {
        use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
        use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
    
        public function permissions(): \Staudenmeir\EloquentHasManyDeep\HasManyDeep
        {
            return $this->hasManyDeepFromRelations(
                $this->roles(),
                (new Role)->permissions()
            );
        }
        
        public function roles(): \Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson
        {
            return $this->belongsToJson(Role::class, 'options->role_ids');
        }
    }
  6. Implement One-To-Many Relationships using JSON

    main

    You can define BelongsTo and hasMany relationships where the foreign key is stored within a JSON field instead of a dedicated column. To do this, include the \[Staudenmeir\\EloquentJsonRelations\\HasJsonRelationships] trait in both the parent and the related model. When defining the relationship, use the -> syntax to specify the path to the key within the JSON object.

    class User extends Model
    {
        use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
    
        protected $casts = [
            'options' => 'json',
        ];
    
        public function locale()
        {
            // Use the path to the foreign key inside the JSON field
            return $this->belongsTo(Locale::class, 'options->locale_id');
        }
    }
    
    class Locale extends Model
    {
        use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
    
        public function users()
        {
            return $this->hasMany(User::class, 'options->locale_id');
        }
    }
  7. Implement Many-To-Many Relationships with JSON Arrays of Objects

    main

    If you need to store additional attributes alongside your foreign keys in a JSON array (e.g., a pivot record with extra metadata), use the [] syntax in the relationship path.

    In the BelongsToJson definition, the path should point to the array, and the key name should be the property inside the object.

    Note: These relationships only work partially on SQLite and SQL Server.

    Example Path: options->roles[]->role_id where options->roles is the array and role_id is the key inside each object.

    class User extends Model
    {
        use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
    
        protected $casts = [
           'options' => 'json',
        ];
        
        public function roles(): \Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson
        {
            return $this->belongsToJson(Role::class, 'options->roles[]->role_id');
        }
    }
    
    // Usage:
    $user->roles()->attach([1 => ['active' => true], 2 => ['active' => false]])->save();
    // Result: [{"role_id":1,"active":true},{"role_id":2,"active":false}]
  8. Install Eloquent JSON Relations via Composer

    main

    Install the package using Composer to enable support for JSON foreign keys in Laravel Eloquent relationships.

    For standard environments (bash, zsh, etc.):

    composer require "staudenmeir/eloquent-json-relations:^1.1"

    If you are using PowerShell on Windows (e.g., within VS Code), use the following command to ensure correct version constraint parsing:

    composer require "staudenmeir/eloquent-json-relations:^^^^1.1"
    composer require "staudenmeir/eloquent-json-relations:^1.1"
  9. Configure the test environment with Docker Compose

    main

    The project provides a docker-compose.yml file to orchestrate a testing environment containing multiple PHP versions and various database engines. This allows for testing Eloquent JSON relations across different runtime and database combinations.

    Available Services

    PHP Runtimes

    The configuration includes pre-built images for different PHP versions:

    • php8.3 (image: ghcr.io/staudenmeir/php:8.3)
    • php8.4 (image: ghcr.io/staudenmeir/php:8.4)
    • php8.5 (image: ghcr.io/staudenmeir/php:8.5)

    Database Engines

    You can run tests against the following databases:

    • MySQL: mysql:latest
    • MariaDB: mariadb:latest
    • PostgreSQL: postgres:latest
    • SQL Server: mcr.microsoft.com/mssql/server:2022-latest
    services:
      php8.3:
        image: ghcr.io/staudenmeir/php:8.3
        # ...
      mysql:
        image: 'mysql:latest'
        environment:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: test
  10. Use HasOneJson for single related instances

    main

    If you want to retrieve only a single related instance from a JSON array (for example, the most recent related record), use the HasOneJson relationship type.

    class Role extends Model
    {
        use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
    
        public function latestUser(): \Staudenmeir\EloquentJsonRelations\Relations\HasOneJson
        {
            return $this->hasOneJson(User::class, 'options->roles[]->role_id')
                ->latest();
        }
    }
  11. Define Composite Keys for JSON Relationships

    main

    When a relationship requires matching multiple columns (some of which may be in JSON), you can pass an array of keys to belongsToJson and hasManyJson. The array must start with the JSON path(s).

    class Employee extends Model
    {
        public function tasks(): \Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson
        {
            return $this->belongsToJson(
                Task::class,
                ['options->work_stream_ids', 'team_id'], // JSON path + standard column
                ['work_stream_id', 'team_id']           // Corresponding keys in the target
            );
        }
    }
  12. Configure Laravel IDE Helper to support JSON relations

    main

    The package automatically registers Staudenmeir\EloquentJsonRelations\IdeHelper\JsonRelationsHook with the laravel-ide-helper configuration. This ensures that IDE autocomplete and type-hinting work correctly for JSON-based Eloquent relations.

    If you are manually managing your ide-helper configuration, ensure that Staudenmeir\EloquentJsonRelations\IdeHelper\JsonRelationsHook is included in the model_hooks array in your config/ide-helper.php file.