Skip to content

Nested Attributes

Elasticsearch and OpenSearch documents are nested JSON. ElasticBridge lets you work with nested fields using dot notation — casts, accessors, and mutators all apply, and you can assign and retrieve nested values directly.

php
use App\Bridges\Product;

$product = Product::find(1);

$product->getAttribute('hotel.location.lat'); // float, cast
$product->getAttribute('hotel.opened_at');    // Carbon, cast

Casting nested fields

Declare casts with a dotted key. Every cast type is supported at any depth:

php
class Product extends ElasticBridge
{
    protected $casts = [
        'hotel.location.lat' => 'float',
        'hotel.location.lon' => 'float',
        'hotel.stars'        => 'integer',
        'hotel.opened_at'    => 'datetime',
    ];
}

The cast applies when the value is read, when it is assigned, and when the model is serialized.

Assigning and retrieving

Use getAttribute() / setAttribute() with the dotted path (or the {$path} curly-brace form). Values are stored in the nested structure, and casts are applied both ways:

php
$product = new Product;

// stored as float 5.6037
$product->setAttribute('hotel.location.lat', '5.6037');

// stored as int 5
$product->setAttribute('hotel.stars', '5');

// equivalent curly-brace form; stored as an ISO-8601 string
$product->{'hotel.opened_at'} = '2020-03-15 12:00:00';

$product->getAttribute('hotel.location.lat'); // 5.6037 (float)
$product->getAttribute('hotel.opened_at');    // Carbon instance

Assigning a nested key creates the intermediate objects automatically, and only the addressed leaf is written — sibling fields under the same parent are preserved.

Access contract

Nested casts apply to the dotted key. Reading a parent as an object ($product->hotel->location->lat) returns the raw stored value without casting. Use getAttribute('hotel.location.lat') (or $product->{'hotel.location.lat'}) to get the cast value.

Nested accessors & mutators

Define an accessor or mutator for a nested key by naming the method the camelCase of the underscored path: hotel.location.lathotelLocationLat().

php
use Illuminate\Database\Eloquent\Casts\Attribute;

class Product extends ElasticBridge
{
    // appended accessor for "hotel.badge"
    protected $appends = ['hotel.badge'];

    protected function hotelBadge(): Attribute
    {
        return Attribute::make(
            get: fn ($value, array $attributes) => strtoupper(
                (string) data_get($attributes, 'hotel.name', '')
            ),
        );
    }

    // mutator for "hotel.slug"
    protected function hotelSlug(): Attribute
    {
        return Attribute::make(
            set: fn ($value) => str($value)->slug()->value(),
        );
    }
}
php
// stored as "grand-palace-hotel"
$product->setAttribute('hotel.slug', 'Grand Palace Hotel');

$product->getAttribute('hotel.badge'); // "GRAND PALACE"

A nested accessor is serialized into toArray()/toJson() when its key is listed in $appends.

Serialization

toArray() and toJson() return the nested casts and appended nested accessors in place, at their dotted path, leaving unrelated sibling fields untouched:

php
$product->toArray();
// [
//   'hotel' => [
//     'name'      => 'Grand Palace',   // untouched
//     'location'  => ['lat' => 5.6037, 'lon' => -0.187],
//     'opened_at' => '2020-03-15T12:00:00+00:00',
//     'badge'     => 'GRAND PALACE',
//   ],
// ]

Querying nested fields

Filters and full-text queries accept dotted field paths directly — they are passed to Elasticsearch/OpenSearch as-is:

php
Product::asBoolean()
    ->filterByRange('hotel.location.lat', 5.0, 'gte')
    ->mustMatch('hotel.name', 'palace')
    ->get();

Mapping note

This works with object-type fields (the default), where hotel.location.lat addresses a sub-field. If you map a field as the Elasticsearch nested type, querying it requires the nested query DSL — build that with asRaw() / raw().

Released under the MIT License.