@@ -163,7 +163,7 @@ protected function isMorphRelation($relation)
163163 /**
164164 * Check if a relation is a HasManyDeep relationship.
165165 *
166- * @param Relation $model
166+ * @param \Illuminate\Database\Eloquent\Relations\ Relation $model
167167 */
168168 protected function isHasManyDeep ($ model ): bool
169169 {
@@ -175,32 +175,17 @@ protected function isHasManyDeep($model): bool
175175 * Get the foreign key name for a HasManyDeep relationship.
176176 * This is the foreign key on the final related table that points to the intermediate table.
177177 *
178- * @param Relation $model
178+ * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $model
179179 */
180180 protected function getHasManyDeepForeignKey ($ model ): string
181181 {
182182 // Try to get from relationship definition using reflection
183- try {
184- $ reflection = new \ReflectionClass ($ model );
185- if ($ reflection ->hasProperty ('foreignKeys ' )) {
186- $ property = $ reflection ->getProperty ('foreignKeys ' );
187- $ property ->setAccessible (true );
188- $ foreignKeys = $ property ->getValue ($ model );
183+ $ foreignKeys = $ this ->getForeignKeys ($ model );
184+ if (! empty ($ foreignKeys )) {
185+ // Get the last foreign key (for the final join)
186+ $ lastFK = end ($ foreignKeys );
189187
190- if (is_array ($ foreignKeys ) && ! empty ($ foreignKeys )) {
191- // Get the last foreign key (for the final join)
192- $ lastFK = end ($ foreignKeys );
193- if (is_string ($ lastFK ) && str_contains ($ lastFK , '. ' )) {
194- $ parts = explode ('. ' , $ lastFK );
195-
196- return end ($ parts );
197- }
198-
199- return $ lastFK ;
200- }
201- }
202- } catch (\Exception $ e ) {
203- // Fallback
188+ return $ this ->extractColumnFromQualified ($ lastFK );
204189 }
205190
206191 // Try to get the foreign key using common HasManyDeep methods
@@ -211,16 +196,15 @@ protected function getHasManyDeepForeignKey($model): string
211196 // HasManyDeep may use getQualifiedForeignKeyName() and extract the column
212197 if (method_exists ($ model , 'getQualifiedForeignKeyName ' )) {
213198 $ qualified = $ model ->getQualifiedForeignKeyName ();
214- $ parts = explode ('. ' , $ qualified );
215199
216- return end ( $ parts );
200+ return $ this -> extractColumnFromQualified ( $ qualified );
217201 }
218202
219203 // Fallback: try to infer from intermediate model
220204 $ intermediateTable = $ this ->getHasManyDeepIntermediateTable ($ model , '' );
221205 if ($ intermediateTable ) {
222206 // Assume the related table has a foreign key named {intermediate_table}_id
223- return $ intermediateTable .'_id ' ;
207+ return \ Illuminate \ Support \Str:: singular ( $ intermediateTable) .'_id ' ;
224208 }
225209
226210 // Final fallback: use the related model's key name
@@ -231,32 +215,29 @@ protected function getHasManyDeepForeignKey($model): string
231215 * Get the local key name for a HasManyDeep relationship.
232216 * This is the local key on the intermediate table (or parent if no intermediate).
233217 *
234- * @param Relation $model
218+ * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $model
235219 */
236220 protected function getHasManyDeepLocalKey ($ model ): string
237221 {
238222 // Try to get from relationship definition using reflection
223+ $ localKeys = [];
239224 try {
240225 $ reflection = new \ReflectionClass ($ model );
241226 if ($ reflection ->hasProperty ('localKeys ' )) {
242227 $ property = $ reflection ->getProperty ('localKeys ' );
243228 $ property ->setAccessible (true );
244229 $ 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-
252- return end ($ parts );
253- }
254-
255- return $ lastLK ;
256- }
257230 }
258231 } catch (\Exception $ e ) {
259- // Fallback
232+ // Reflection failed - proceed to other methods
233+ // This is safe because we have multiple fallback strategies
234+ }
235+
236+ if (is_array ($ localKeys ) && ! empty ($ localKeys )) {
237+ // Get the last local key (for the final join)
238+ $ lastLK = end ($ localKeys );
239+
240+ return $ this ->extractColumnFromQualified ($ lastLK );
260241 }
261242
262243 // Try to get the local key using common HasManyDeep methods
@@ -267,31 +248,21 @@ protected function getHasManyDeepLocalKey($model): string
267248 // HasManyDeep may use getQualifiedLocalKeyName() and extract the column
268249 if (method_exists ($ model , 'getQualifiedLocalKeyName ' )) {
269250 $ qualified = $ model ->getQualifiedLocalKeyName ();
270- $ parts = explode ('. ' , $ qualified );
271251
272- return end ( $ parts );
252+ return $ this -> extractColumnFromQualified ( $ qualified );
273253 }
274254
275255 // Fallback: use the intermediate model's key name, or parent if no intermediate
276256 $ intermediateTable = $ this ->getHasManyDeepIntermediateTable ($ model , '' );
277257 if ($ intermediateTable ) {
278- try {
279- $ reflection = new \ReflectionClass ($ model );
280- if ($ reflection ->hasProperty ('through ' )) {
281- $ property = $ reflection ->getProperty ('through ' );
282- $ property ->setAccessible (true );
283- $ through = $ property ->getValue ($ model );
284- if (is_array ($ through ) && ! empty ($ through )) {
285- $ firstThrough = is_string ($ through [0 ]) ? $ through [0 ] : get_class ($ through [0 ]);
286- if (class_exists ($ firstThrough )) {
287- $ throughModel = new $ firstThrough ;
288-
289- return $ throughModel ->getKeyName ();
290- }
291- }
258+ $ through = $ this ->getThroughModels ($ model );
259+ if (! empty ($ through )) {
260+ $ firstThrough = is_string ($ through [0 ]) ? $ through [0 ] : get_class ($ through [0 ]);
261+ if (class_exists ($ firstThrough )) {
262+ $ throughModel = app ($ firstThrough );
263+
264+ return $ throughModel ->getKeyName ();
292265 }
293- } catch (\Exception $ e ) {
294- // Fallback
295266 }
296267 }
297268
@@ -302,32 +273,22 @@ protected function getHasManyDeepLocalKey($model): string
302273 /**
303274 * Get the intermediate table name for a HasManyDeep relationship.
304275 *
305- * @param Relation $model
276+ * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $model
306277 * @param string $lastAlias
307278 */
308279 protected function getHasManyDeepIntermediateTable ($ model , $ lastAlias ): ?string
309280 {
310281 // Try to get intermediate models from the relationship
311282 // HasManyDeep stores intermediate models in a protected property
312- try {
313- $ reflection = new \ReflectionClass ($ model );
314- if ($ reflection ->hasProperty ('through ' )) {
315- $ property = $ reflection ->getProperty ('through ' );
316- $ property ->setAccessible (true );
317- $ through = $ property ->getValue ($ model );
318-
319- if (is_array ($ through ) && ! empty ($ through )) {
320- // Get the first intermediate model
321- $ firstThrough = is_string ($ through [0 ]) ? $ through [0 ] : get_class ($ through [0 ]);
322- if (class_exists ($ firstThrough )) {
323- $ throughModel = new $ firstThrough ;
324-
325- return $ throughModel ->getTable ();
326- }
327- }
283+ $ through = $ this ->getThroughModels ($ model );
284+ if (! empty ($ through )) {
285+ // Get the first intermediate model
286+ $ firstThrough = is_string ($ through [0 ]) ? $ through [0 ] : get_class ($ through [0 ]);
287+ if (class_exists ($ firstThrough )) {
288+ $ throughModel = app ($ firstThrough );
289+
290+ return $ throughModel ->getTable ();
328291 }
329- } catch (\Exception $ e ) {
330- // Fallback if reflection fails
331292 }
332293
333294 return null ;
@@ -336,46 +297,30 @@ protected function getHasManyDeepIntermediateTable($model, $lastAlias): ?string
336297 /**
337298 * Get the foreign key for joining to the intermediate table.
338299 *
339- * @param Relation $model
300+ * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $model
340301 */
341302 protected function getHasManyDeepIntermediateForeignKey ($ model ): string
342303 {
343304 // The foreign key on the intermediate table that points to the parent
344305 // For User -> Posts -> Comments, this would be posts.user_id
345306 $ parent = $ model ->getParent ();
346- $ parentKey = $ parent ->getKeyName ();
347307
348308 // Try to get from relationship definition
349- try {
350- $ reflection = new \ReflectionClass ($ model );
351- if ($ reflection ->hasProperty ('foreignKeys ' )) {
352- $ property = $ reflection ->getProperty ('foreignKeys ' );
353- $ property ->setAccessible (true );
354- $ foreignKeys = $ property ->getValue ($ model );
355-
356- if (is_array ($ foreignKeys ) && ! empty ($ foreignKeys )) {
357- $ firstFK = $ foreignKeys [0 ];
358- if (is_string ($ firstFK ) && str_contains ($ firstFK , '. ' )) {
359- $ parts = explode ('. ' , $ firstFK );
360-
361- return end ($ parts );
362- }
309+ $ foreignKeys = $ this ->getForeignKeys ($ model );
310+ if (! empty ($ foreignKeys )) {
311+ $ firstFK = $ foreignKeys [0 ];
363312
364- return $ firstFK ;
365- }
366- }
367- } catch (\Exception $ e ) {
368- // Fallback
313+ return $ this ->extractColumnFromQualified ($ firstFK );
369314 }
370315
371316 // Default: assume intermediate table has a foreign key named {parent_table}_id
372- return $ parent ->getTable ().'_id ' ;
317+ return \ Illuminate \ Support \Str:: singular ( $ parent ->getTable () ).'_id ' ;
373318 }
374319
375320 /**
376321 * Get the local key for joining from the parent to the intermediate table.
377322 *
378- * @param Relation $model
323+ * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $model
379324 */
380325 protected function getHasManyDeepIntermediateLocalKey ($ model ): string
381326 {
@@ -582,4 +527,68 @@ protected function performJoin($table, $foreign, $other, $type = 'left'): void
582527 $ this ->getBaseQueryBuilder ()->join ($ table , $ foreign , '= ' , $ other , $ type );
583528 }
584529 }
530+
531+ /**
532+ * Extract the array of foreign keys from a HasManyDeep relationship using reflection.
533+ *
534+ * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $model
535+ */
536+ private function getForeignKeys ($ model ): array
537+ {
538+ try {
539+ $ reflection = new \ReflectionClass ($ model );
540+ if ($ reflection ->hasProperty ('foreignKeys ' )) {
541+ $ property = $ reflection ->getProperty ('foreignKeys ' );
542+ $ property ->setAccessible (true );
543+ $ foreignKeys = $ property ->getValue ($ model );
544+ if (is_array ($ foreignKeys ) && ! empty ($ foreignKeys )) {
545+ return $ foreignKeys ;
546+ }
547+ }
548+ } catch (\Exception $ e ) {
549+ // Reflection failed - fall back to empty array
550+ // This is safe because callers handle empty arrays appropriately
551+ }
552+
553+ return [];
554+ }
555+
556+ /**
557+ * Extract the array of through models from a HasManyDeep relationship using reflection.
558+ *
559+ * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $model
560+ */
561+ private function getThroughModels ($ model ): array
562+ {
563+ try {
564+ $ reflection = new \ReflectionClass ($ model );
565+ if ($ reflection ->hasProperty ('through ' )) {
566+ $ property = $ reflection ->getProperty ('through ' );
567+ $ property ->setAccessible (true );
568+ $ through = $ property ->getValue ($ model );
569+ if (is_array ($ through ) && ! empty ($ through )) {
570+ return $ through ;
571+ }
572+ }
573+ } catch (\Exception $ e ) {
574+ // Reflection failed - fall back to empty array
575+ // This is safe because callers handle empty arrays appropriately
576+ }
577+
578+ return [];
579+ }
580+
581+ /**
582+ * Extract the column name from a qualified column name (e.g., 'table.column' -> 'column').
583+ */
584+ private function extractColumnFromQualified (string $ qualified ): string
585+ {
586+ if (str_contains ($ qualified , '. ' )) {
587+ $ parts = explode ('. ' , $ qualified );
588+
589+ return end ($ parts );
590+ }
591+
592+ return $ qualified ;
593+ }
585594}
0 commit comments