@@ -160,6 +160,225 @@ protected function isMorphRelation($relation)
160160 return $ isMorph ;
161161 }
162162
163+ /**
164+ * Check if a relation is a HasManyDeep relationship.
165+ *
166+ * @param Relation $model
167+ * @return bool
168+ */
169+ protected function isHasManyDeep ($ model ): bool
170+ {
171+ return class_exists ('Staudenmeir\EloquentHasManyDeep\HasManyDeep ' )
172+ && $ model instanceof \Staudenmeir \EloquentHasManyDeep \HasManyDeep;
173+ }
174+
175+ /**
176+ * Get the foreign key name for a HasManyDeep relationship.
177+ * This is the foreign key on the final related table that points to the intermediate table.
178+ *
179+ * @param Relation $model
180+ * @return string
181+ */
182+ protected function getHasManyDeepForeignKey ($ model ): string
183+ {
184+ // Try to get from relationship definition using reflection
185+ try {
186+ $ reflection = new \ReflectionClass ($ model );
187+ if ($ reflection ->hasProperty ('foreignKeys ' )) {
188+ $ property = $ reflection ->getProperty ('foreignKeys ' );
189+ $ property ->setAccessible (true );
190+ $ foreignKeys = $ property ->getValue ($ model );
191+
192+ if (is_array ($ foreignKeys ) && !empty ($ foreignKeys )) {
193+ // Get the last foreign key (for the final join)
194+ $ lastFK = end ($ foreignKeys );
195+ if (is_string ($ lastFK ) && str_contains ($ lastFK , '. ' )) {
196+ $ parts = explode ('. ' , $ lastFK );
197+ return end ($ parts );
198+ }
199+ return $ lastFK ;
200+ }
201+ }
202+ } catch (\Exception $ e ) {
203+ // Fallback
204+ }
205+
206+ // Try to get the foreign key using common HasManyDeep methods
207+ if (method_exists ($ model , 'getForeignKeyName ' )) {
208+ return $ model ->getForeignKeyName ();
209+ }
210+
211+ // HasManyDeep may use getQualifiedForeignKeyName() and extract the column
212+ if (method_exists ($ model , 'getQualifiedForeignKeyName ' )) {
213+ $ qualified = $ model ->getQualifiedForeignKeyName ();
214+ $ parts = explode ('. ' , $ qualified );
215+ return end ($ parts );
216+ }
217+
218+ // Fallback: try to infer from intermediate model
219+ $ intermediateTable = $ this ->getHasManyDeepIntermediateTable ($ model , '' );
220+ if ($ intermediateTable ) {
221+ // Assume the related table has a foreign key named {intermediate_table}_id
222+ return $ intermediateTable .'_id ' ;
223+ }
224+
225+ // Final fallback: use the related model's key name
226+ return $ model ->getRelated ()->getKeyName ();
227+ }
228+
229+ /**
230+ * Get the local key name for a HasManyDeep relationship.
231+ * This is the local key on the intermediate table (or parent if no intermediate).
232+ *
233+ * @param Relation $model
234+ * @return string
235+ */
236+ protected function getHasManyDeepLocalKey ($ model ): string
237+ {
238+ // Try to get from relationship definition using reflection
239+ try {
240+ $ reflection = new \ReflectionClass ($ model );
241+ if ($ reflection ->hasProperty ('localKeys ' )) {
242+ $ property = $ reflection ->getProperty ('localKeys ' );
243+ $ property ->setAccessible (true );
244+ $ localKeys = $ property ->getValue ($ model );
245+
246+ if (is_array ($ localKeys ) && !empty ($ localKeys )) {
247+ // Get the last local key (for the final join)
248+ $ lastLK = end ($ localKeys );
249+ if (is_string ($ lastLK ) && str_contains ($ lastLK , '. ' )) {
250+ $ parts = explode ('. ' , $ lastLK );
251+ return end ($ parts );
252+ }
253+ return $ lastLK ;
254+ }
255+ }
256+ } catch (\Exception $ e ) {
257+ // Fallback
258+ }
259+
260+ // Try to get the local key using common HasManyDeep methods
261+ if (method_exists ($ model , 'getLocalKeyName ' )) {
262+ return $ model ->getLocalKeyName ();
263+ }
264+
265+ // HasManyDeep may use getQualifiedLocalKeyName() and extract the column
266+ if (method_exists ($ model , 'getQualifiedLocalKeyName ' )) {
267+ $ qualified = $ model ->getQualifiedLocalKeyName ();
268+ $ parts = explode ('. ' , $ qualified );
269+ return end ($ parts );
270+ }
271+
272+ // Fallback: use the intermediate model's key name, or parent if no intermediate
273+ $ intermediateTable = $ this ->getHasManyDeepIntermediateTable ($ model , '' );
274+ if ($ intermediateTable ) {
275+ try {
276+ $ reflection = new \ReflectionClass ($ model );
277+ if ($ reflection ->hasProperty ('through ' )) {
278+ $ property = $ reflection ->getProperty ('through ' );
279+ $ property ->setAccessible (true );
280+ $ through = $ property ->getValue ($ model );
281+ if (is_array ($ through ) && !empty ($ through )) {
282+ $ firstThrough = is_string ($ through [0 ]) ? $ through [0 ] : get_class ($ through [0 ]);
283+ if (class_exists ($ firstThrough )) {
284+ $ throughModel = new $ firstThrough ;
285+ return $ throughModel ->getKeyName ();
286+ }
287+ }
288+ }
289+ } catch (\Exception $ e ) {
290+ // Fallback
291+ }
292+ }
293+
294+ // Final fallback: use the parent model's key name
295+ return $ model ->getParent ()->getKeyName ();
296+ }
297+
298+ /**
299+ * Get the intermediate table name for a HasManyDeep relationship.
300+ *
301+ * @param Relation $model
302+ * @param string $lastAlias
303+ * @return string|null
304+ */
305+ protected function getHasManyDeepIntermediateTable ($ model , $ lastAlias ): ?string
306+ {
307+ // Try to get intermediate models from the relationship
308+ // HasManyDeep stores intermediate models in a protected property
309+ try {
310+ $ reflection = new \ReflectionClass ($ model );
311+ if ($ reflection ->hasProperty ('through ' )) {
312+ $ property = $ reflection ->getProperty ('through ' );
313+ $ property ->setAccessible (true );
314+ $ through = $ property ->getValue ($ model );
315+
316+ if (is_array ($ through ) && !empty ($ through )) {
317+ // Get the first intermediate model
318+ $ firstThrough = is_string ($ through [0 ]) ? $ through [0 ] : get_class ($ through [0 ]);
319+ if (class_exists ($ firstThrough )) {
320+ $ throughModel = new $ firstThrough ;
321+ return $ throughModel ->getTable ();
322+ }
323+ }
324+ }
325+ } catch (\Exception $ e ) {
326+ // Fallback if reflection fails
327+ }
328+
329+ return null ;
330+ }
331+
332+ /**
333+ * Get the foreign key for joining to the intermediate table.
334+ *
335+ * @param Relation $model
336+ * @return string
337+ */
338+ protected function getHasManyDeepIntermediateForeignKey ($ model ): string
339+ {
340+ // The foreign key on the intermediate table that points to the parent
341+ // For User -> Posts -> Comments, this would be posts.user_id
342+ $ parent = $ model ->getParent ();
343+ $ parentKey = $ parent ->getKeyName ();
344+
345+ // Try to get from relationship definition
346+ try {
347+ $ reflection = new \ReflectionClass ($ model );
348+ if ($ reflection ->hasProperty ('foreignKeys ' )) {
349+ $ property = $ reflection ->getProperty ('foreignKeys ' );
350+ $ property ->setAccessible (true );
351+ $ foreignKeys = $ property ->getValue ($ model );
352+
353+ if (is_array ($ foreignKeys ) && !empty ($ foreignKeys )) {
354+ $ firstFK = $ foreignKeys [0 ];
355+ if (is_string ($ firstFK ) && str_contains ($ firstFK , '. ' )) {
356+ $ parts = explode ('. ' , $ firstFK );
357+ return end ($ parts );
358+ }
359+ return $ firstFK ;
360+ }
361+ }
362+ } catch (\Exception $ e ) {
363+ // Fallback
364+ }
365+
366+ // Default: assume intermediate table has a foreign key named {parent_table}_id
367+ return $ parent ->getTable ().'_id ' ;
368+ }
369+
370+ /**
371+ * Get the local key for joining from the parent to the intermediate table.
372+ *
373+ * @param Relation $model
374+ * @return string
375+ */
376+ protected function getHasManyDeepIntermediateLocalKey ($ model ): string
377+ {
378+ // The local key on the parent table
379+ return $ model ->getParent ()->getKeyName ();
380+ }
381+
163382 /**
164383 * {@inheritDoc}
165384 *
@@ -269,6 +488,53 @@ protected function joinEagerLoadedColumn($relation, $relationColumn)
269488 $ other = $ tableAlias .'. ' .$ model ->getOwnerKeyName ();
270489 break ;
271490
491+ case $ this ->isHasManyDeep ($ model ):
492+ // HasManyDeep relationships can traverse multiple intermediate models
493+ // We need to join through all intermediate models to reach the final related table
494+ $ related = $ model ->getRelated ();
495+
496+ // Get the qualified parent key to determine the first intermediate model
497+ $ qualifiedParentKey = $ model ->getQualifiedParentKeyName ();
498+ $ parentTable = explode ('. ' , $ qualifiedParentKey )[0 ];
499+
500+ // For HasManyDeep, we need to join through intermediate models
501+ // The relationship query already knows the structure, so we'll use it
502+ // First, join to the first intermediate model (if not already joined)
503+ $ intermediateTable = $ this ->getHasManyDeepIntermediateTable ($ model , $ lastAlias );
504+
505+ if ($ intermediateTable && $ intermediateTable !== $ lastAlias ) {
506+ // Join to intermediate table first
507+ if ($ this ->enableEagerJoinAliases ) {
508+ $ intermediateAlias = $ tableAlias .'_intermediate ' ;
509+ $ intermediate = $ intermediateTable .' as ' .$ intermediateAlias ;
510+ } else {
511+ $ intermediateAlias = $ intermediateTable ;
512+ $ intermediate = $ intermediateTable ;
513+ }
514+
515+ $ intermediateFK = $ this ->getHasManyDeepIntermediateForeignKey ($ model );
516+ $ intermediateLocal = $ this ->getHasManyDeepIntermediateLocalKey ($ model );
517+ $ this ->performJoin ($ intermediate , $ intermediateAlias .'. ' .$ intermediateFK , ltrim ($ lastAlias .'. ' .$ intermediateLocal , '. ' ));
518+ $ lastAlias = $ intermediateAlias ;
519+ }
520+
521+ // Now join to the final related table
522+ if ($ this ->enableEagerJoinAliases ) {
523+ $ table = $ related ->getTable ().' as ' .$ tableAlias ;
524+ } else {
525+ $ table = $ tableAlias = $ related ->getTable ();
526+ }
527+
528+ // Get the foreign key on the related table (points to intermediate)
529+ $ foreignKey = $ this ->getHasManyDeepForeignKey ($ model );
530+ $ localKey = $ this ->getHasManyDeepLocalKey ($ model );
531+
532+ $ foreign = $ tableAlias .'. ' .$ foreignKey ;
533+ $ other = ltrim ($ lastAlias .'. ' .$ localKey , '. ' );
534+
535+ $ lastQuery ->addSelect ($ tableAlias .'. ' .$ relationColumn );
536+ break ;
537+
272538 default :
273539 throw new Exception ('Relation ' .$ model ::class.' is not yet supported. ' );
274540 }
0 commit comments