atldays/laravel-eloquent-join-relation
Composer 安装命令:
composer require atldays/laravel-eloquent-join-relation
包简介
Join related Eloquent models and hydrate them as loaded relations.
README 文档
README
Laravel is great at working with relations, but as soon as a query becomes more complex and starts using join, loading relations through with() or lazy loading often leads to additional queries. The required data was already returned by the main SQL query.
This package solves that problem by letting you keep a single SQL query with join while still getting fully hydrated relations, as if they had been loaded through Eloquent in the usual way.
If you already joined a related table for filtering, sorting, or conditional checks, the package can build the relation directly from that joined data and set it on the model without touching the database again.
It is especially useful in complex queries that involve multiple related tables, where you still want to work with them afterward as normal nested Laravel relations.
Highlights
- Hydrates
BelongsToandHasOnerelations directly from data returned byjoin. - Lets you work with joined data as normal Eloquent relations without extra SQL queries.
- Supports nested relation paths such as
author.team.organization. - Supports manual hydration for custom join scenarios through
hydrate. - Correctly returns
nullfor missing records onleft join. - Fails explicitly when nested relation paths are joined out of order.
- Especially useful for complex queries that filter across multiple related tables.
Support
- PHP:
8.2+ - Laravel:
11.x,12.x,13.x
Current boundaries
- Supported relation types:
BelongsToHasOne
- Not supported yet:
HasManyBelongsToManyMorphTo,MorphOne,MorphManyHasOneThrough
- Nested relation paths must be called in order:
- first
author - then
author.team - then
author.team.organization
- first
Installation
composer require atldays/laravel-eloquent-join-relation
Basic usage
If you already join a related table, you can hydrate that relation without an extra query.
use Atldays\JoinRelation\HasJoinRelation; use App\Models\Author; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Post extends Model { use HasJoinRelation; public function author(): BelongsTo { return $this->belongsTo(Author::class, 'author_id'); } }
$posts = Post::query() ->select('posts.*') ->joinRelation( relation: 'author', columns: ['id', 'name', 'email'], ) ->where('authors.active', true) ->get(); $post = $posts->first(); $post->author; // already hydrated, no extra query
The generated SQL will look roughly like this:
select `posts`.*, `authors`.`id` as `join_author__id`, `authors`.`name` as `join_author__name`, `authors`.`email` as `join_author__email` from `posts` inner join `authors` on `authors`.`id` = `posts`.`author_id` where `authors`.`active` = 1
That means:
- one SQL query
- hydrated
$post->author - no follow-up query when you access the relation
Nested relation paths
For common BelongsTo chains, you can hydrate nested relations step by step.
$posts = Post::query() ->select('posts.*') ->joinRelation( relation: 'author', columns: ['id', 'team_id', 'name'], ) ->joinRelation( relation: 'author.team', columns: ['id', 'organization_id', 'name'], ) ->joinRelation( relation: 'author.team.organization', columns: ['id', 'name'], ) ->where('posts.published', true) ->where('authors.active', true) ->where('teams.active', true) ->where('organizations.active', true) ->get(); $post = $posts->first(); $post->author; $post->author->team; $post->author->team->organization;
The generated SQL will look roughly like this:
select `posts`.*, `authors`.`id` as `join_author__id`, `authors`.`team_id` as `join_author__team_id`, `authors`.`name` as `join_author__name`, `teams`.`id` as `join_author_team__id`, `teams`.`organization_id` as `join_author_team__organization_id`, `teams`.`name` as `join_author_team__name`, `organizations`.`id` as `join_author_team_organization__id`, `organizations`.`name` as `join_author_team_organization__name` from `posts` inner join `authors` on `authors`.`id` = `posts`.`author_id` inner join `teams` on `teams`.`id` = `authors`.`team_id` inner join `organizations` on `organizations`.`id` = `teams`.`organization_id` where `posts`.`published` = 1 and `authors`.`active` = 1 and `teams`.`active` = 1 and `organizations`.`active` = 1
Important:
authormust be joined beforeauthor.teamauthor.teammust be joined beforeauthor.team.organization
If you skip an earlier level, the package throws an exception instead of silently falling back to lazy loading.
Left joins and nullable relations
For optional relations, use type: 'left'.
$posts = Post::query() ->select('posts.*') ->joinRelation( relation: 'author', type: 'left', columns: ['id', 'name'], ) ->get(); $post = $posts->first(); $post->author; // User model or null
The generated SQL will look roughly like this:
select `posts`.*, `authors`.`id` as `join_author__id`, `authors`.`name` as `join_author__name` from `posts` left join `authors` on `authors`.`id` = `posts`.`author_id`
The same applies to nested paths:
Post::query() ->select('posts.*') ->joinRelation(relation: 'author', columns: ['id', 'team_id', 'name']) ->joinRelation( relation: 'author.team', type: 'left', columns: ['id', 'organization_id', 'name'], ) ->get();
The generated SQL will look roughly like this:
select `posts`.*, `authors`.`id` as `join_author__id`, `authors`.`team_id` as `join_author__team_id`, `authors`.`name` as `join_author__name`, `teams`.`id` as `join_author_team__id`, `teams`.`organization_id` as `join_author_team__organization_id`, `teams`.`name` as `join_author_team__name` from `posts` inner join `authors` on `authors`.`id` = `posts`.`author_id` left join `teams` on `teams`.`id` = `authors`.`team_id`
If the joined team row is missing, author->team becomes null.
Manual hydrate mode
When the join is custom, or when you want to put the hydrated model somewhere non-standard, use related + join + hydrate.
This is especially useful when you already joined several tables and want to attach a model deeper in the tree yourself.
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Query\JoinClause; $posts = Post::query() ->select('posts.*') ->joinRelation( relation: 'author', columns: ['id', 'name', 'email'], ) ->joinRelation( related: Profile::class, type: 'left', join: function (JoinClause $join): void { $join->on('authors.id', '=', 'profiles.user_id'); }, hydrate: function (Model $model, ?Profile $profile): void { $model->author?->setRelation('profile', $profile); }, columns: ['id', 'user_id', 'bio'], ) ->get(); $post = $posts->first(); $post->author->profile;
The generated SQL will look roughly like this:
select `posts`.*, `authors`.`id` as `join_author__id`, `authors`.`name` as `join_author__name`, `authors`.`email` as `join_author__email`, `profiles`.`id` as `join_profile__id`, `profiles`.`user_id` as `join_profile__user_id`, `profiles`.`bio` as `join_profile__bio` from `posts` inner join `authors` on `authors`.`id` = `posts`.`author_id` left join `profiles` on `authors`.`id` = `profiles`.`user_id`
If the profile is missing on a left join, the callback receives null.
Advanced example
Here is the same style of query for a deeper chain where every level is required.
$posts = Post::query() ->select('posts.*') ->joinRelation( relation: 'author', type: 'inner', columns: ['id', 'team_id', 'name', 'active', 'deleted_at'], ) ->joinRelation( relation: 'author.team', type: 'inner', columns: ['id', 'organization_id', 'name', 'active', 'deleted_at'], ) ->joinRelation( relation: 'author.team.organization', type: 'inner', columns: ['id', 'name', 'active', 'deleted_at'], ) ->where('posts.active', true) ->whereNull('posts.deleted_at') ->where('authors.active', true) ->whereNull('authors.deleted_at') ->where('teams.active', true) ->whereNull('teams.deleted_at') ->where('organizations.active', true) ->whereNull('organizations.deleted_at') ->get();
This gives you one SQL query and fully hydrated nested relations with no follow-up lazy-loading queries.
The resulting SQL will look roughly like this:
select `posts`.*, `authors`.`id` as `join_author__id`, `authors`.`team_id` as `join_author__team_id`, `authors`.`name` as `join_author__name`, `authors`.`active` as `join_author__active`, `authors`.`deleted_at` as `join_author__deleted_at`, `teams`.`id` as `join_author_team__id`, `teams`.`organization_id` as `join_author_team__organization_id`, `teams`.`name` as `join_author_team__name`, `teams`.`active` as `join_author_team__active`, `teams`.`deleted_at` as `join_author_team__deleted_at`, `organizations`.`id` as `join_author_team_organization__id`, `organizations`.`name` as `join_author_team_organization__name`, `organizations`.`active` as `join_author_team_organization__active`, `organizations`.`deleted_at` as `join_author_team_organization__deleted_at` from `posts` inner join `authors` on `authors`.`id` = `posts`.`author_id` inner join `teams` on `teams`.`id` = `authors`.`team_id` inner join `organizations` on `organizations`.`id` = `teams`.`organization_id` where `posts`.`active` = 1 and `posts`.`deleted_at` is null and `authors`.`active` = 1 and `authors`.`deleted_at` is null and `teams`.`active` = 1 and `teams`.`deleted_at` is null and `organizations`.`active` = 1 and `organizations`.`deleted_at` is null
That is the main point of the package:
- you keep the join-heavy query you already need
- you still get normal nested Eloquent relations
- you do it with one SQL query instead of a join plus follow-up eager-load queries
What you save compared to with()
For queries that already depend on joins, a classic eager-loading approach often turns into:
- one query for the root records
- one query for
author - one query for
author.team - one query for
author.team.organization
With joinRelation(...), those joined records are hydrated from the same SQL result set, so relation access does not need those extra follow-up queries.
How it differs from with()
with() is still great when you want classic eager loading.
This package is useful when:
- you already need SQL joins for filtering or sorting
- you want to avoid follow-up relation queries
- you still want to work with normal Eloquent relation objects
Testing status
The package test suite covers:
- direct
BelongsTo - direct
HasOne - nested relation paths
- manual hydrate mode
left join => null- ordered nested path enforcement
- no lazy-loading fallback for hydrated relations
atldays/laravel-eloquent-join-relation 适用场景与选型建议
atldays/laravel-eloquent-join-relation 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 0 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 04 月 15 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「laravel」 「relation」 「join」 「eloquent」 「atldays」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 atldays/laravel-eloquent-join-relation 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 atldays/laravel-eloquent-join-relation 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 atldays/laravel-eloquent-join-relation 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
A CMS form field for adding has_one relationships using autocomplete
This package introduces the join magic for eloquent models and relations.
Test easier your Eloquent Models in your Laravel Project
Twig filter that joins an array using a different separator for the last element
This package introduces the join magic for eloquent models and relations.
Micro Active Record library in PHP, support chain calls, events, and relations.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 33
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-04-15