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.
use App\Bridges\Product;
$product = Product::find(1);
$product->getAttribute('hotel.location.lat'); // float, cast
$product->getAttribute('hotel.opened_at'); // Carbon, castCasting nested fields
Declare casts with a dotted key. Every cast type is supported at any depth:
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:
$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 instanceAssigning 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.lat → hotelLocationLat().
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(),
);
}
}// 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:
$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:
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().