Skip to content

Commit cefb626

Browse files
committed
feat: add HasManyDeepSupport trait to enhance EloquentDataTable with HasManyDeep relationship handling
1 parent 558f9b1 commit cefb626

File tree

2 files changed

+238
-222
lines changed

2 files changed

+238
-222
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
<?php
2+
3+
namespace Yajra\DataTables\Concerns;
4+
5+
use Illuminate\Database\Eloquent\Relations\Relation;
6+
7+
/**
8+
* Trait to support HasManyDeep relationships in EloquentDataTable.
9+
* This trait encapsulates all HasManyDeep-related methods to keep the main class smaller.
10+
*/
11+
trait HasManyDeepSupport
12+
{
13+
/**
14+
* Check if a relation is a HasManyDeep relationship.
15+
*
16+
* @param \Illuminate\Database\Eloquent\Relations\Relation<\Illuminate\Database\Eloquent\Model, \Illuminate\Database\Eloquent\Model, mixed> $model
17+
*/
18+
protected function isHasManyDeep($model): bool
19+
{
20+
return class_exists(\Staudenmeir\EloquentHasManyDeep\HasManyDeep::class)
21+
&& $model instanceof \Staudenmeir\EloquentHasManyDeep\HasManyDeep;
22+
}
23+
24+
/**
25+
* Get the foreign key name for a HasManyDeep relationship.
26+
* This is the foreign key on the final related table that points to the intermediate table.
27+
*
28+
* @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep<\Illuminate\Database\Eloquent\Model, \Illuminate\Database\Eloquent\Model> $model
29+
*/
30+
protected function getHasManyDeepForeignKey($model): string
31+
{
32+
// Try to get from relationship definition using reflection
33+
$foreignKeys = $this->getForeignKeys($model);
34+
if (! empty($foreignKeys)) {
35+
return $this->extractColumnFromQualified(end($foreignKeys));
36+
}
37+
38+
// Try to get the foreign key using common HasManyDeep methods
39+
if (method_exists($model, 'getForeignKeyName')) {
40+
return $model->getForeignKeyName();
41+
}
42+
43+
// Fallback: try to infer from intermediate model or use related model's key
44+
$intermediateTable = $this->getHasManyDeepIntermediateTable($model);
45+
46+
return $intermediateTable
47+
? \Illuminate\Support\Str::singular($intermediateTable).'_id'
48+
: $model->getRelated()->getKeyName();
49+
}
50+
51+
/**
52+
* Get the local key name for a HasManyDeep relationship.
53+
* This is the local key on the intermediate table (or parent if no intermediate).
54+
*
55+
* @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep<\Illuminate\Database\Eloquent\Model, \Illuminate\Database\Eloquent\Model> $model
56+
*/
57+
protected function getHasManyDeepLocalKey($model): string
58+
{
59+
// Try to get from relationship definition using reflection
60+
$localKeys = $this->getLocalKeys($model);
61+
if (! empty($localKeys)) {
62+
return $this->extractColumnFromQualified(end($localKeys));
63+
}
64+
65+
// Try to get the local key using common HasManyDeep methods
66+
if (method_exists($model, 'getLocalKeyName')) {
67+
return $model->getLocalKeyName();
68+
}
69+
70+
// Fallback: use the intermediate model's key name, or parent if no intermediate
71+
$intermediateTable = $this->getHasManyDeepIntermediateTable($model);
72+
$through = $this->getThroughModels($model);
73+
$fallbackKey = $model->getParent()->getKeyName();
74+
if ($intermediateTable && ! empty($through)) {
75+
$firstThrough = is_string($through[0]) ? $through[0] : get_class($through[0]);
76+
if (class_exists($firstThrough)) {
77+
$fallbackKey = app($firstThrough)->getKeyName();
78+
}
79+
}
80+
81+
return $fallbackKey;
82+
}
83+
84+
/**
85+
* Get the intermediate table name for a HasManyDeep relationship.
86+
*
87+
* @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep<\Illuminate\Database\Eloquent\Model, \Illuminate\Database\Eloquent\Model> $model
88+
*/
89+
protected function getHasManyDeepIntermediateTable($model): ?string
90+
{
91+
// Try to get intermediate models from the relationship
92+
// HasManyDeep stores intermediate models in a protected property
93+
$through = $this->getThroughModels($model);
94+
if (! empty($through)) {
95+
// Get the first intermediate model
96+
$firstThrough = is_string($through[0]) ? $through[0] : get_class($through[0]);
97+
if (class_exists($firstThrough)) {
98+
$throughModel = app($firstThrough);
99+
100+
return $throughModel->getTable();
101+
}
102+
}
103+
104+
return null;
105+
}
106+
107+
/**
108+
* Get the foreign key for joining to the intermediate table.
109+
*
110+
* @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep<\Illuminate\Database\Eloquent\Model, \Illuminate\Database\Eloquent\Model> $model
111+
*/
112+
protected function getHasManyDeepIntermediateForeignKey($model): string
113+
{
114+
// The foreign key on the intermediate table that points to the parent
115+
// For User -> Posts -> Comments, this would be posts.user_id
116+
$parent = $model->getParent();
117+
118+
// Try to get from relationship definition
119+
$foreignKeys = $this->getForeignKeys($model);
120+
if (! empty($foreignKeys)) {
121+
$firstFK = $foreignKeys[0];
122+
123+
return $this->extractColumnFromQualified($firstFK);
124+
}
125+
126+
// Default: assume intermediate table has a foreign key named {parent_table}_id
127+
return \Illuminate\Support\Str::singular($parent->getTable()).'_id';
128+
}
129+
130+
/**
131+
* Get the local key for joining from the parent to the intermediate table.
132+
*
133+
* @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep<\Illuminate\Database\Eloquent\Model, \Illuminate\Database\Eloquent\Model> $model
134+
*/
135+
protected function getHasManyDeepIntermediateLocalKey($model): string
136+
{
137+
// The local key on the parent table
138+
return $model->getParent()->getKeyName();
139+
}
140+
141+
/**
142+
* Extract the array of foreign keys from a HasManyDeep relationship using reflection.
143+
*
144+
* @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep<\Illuminate\Database\Eloquent\Model, \Illuminate\Database\Eloquent\Model> $model
145+
*/
146+
private function getForeignKeys($model): array
147+
{
148+
try {
149+
$reflection = new \ReflectionClass($model);
150+
if ($reflection->hasProperty('foreignKeys')) {
151+
$property = $reflection->getProperty('foreignKeys');
152+
// Safe: Accessing protected property from third-party package (staudenmeir/eloquent-has-many-deep)
153+
// The property exists and is part of the package's internal API
154+
$property->setAccessible(true); // NOSONAR
155+
$foreignKeys = $property->getValue($model); // NOSONAR
156+
if (is_array($foreignKeys) && ! empty($foreignKeys)) {
157+
return $foreignKeys;
158+
}
159+
}
160+
} catch (\Exception) {
161+
// Reflection failed - fall back to empty array
162+
// This is safe because callers handle empty arrays appropriately
163+
}
164+
165+
return [];
166+
}
167+
168+
/**
169+
* Extract the array of local keys from a HasManyDeep relationship using reflection.
170+
*
171+
* @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep<\Illuminate\Database\Eloquent\Model, \Illuminate\Database\Eloquent\Model> $model
172+
*/
173+
private function getLocalKeys($model): array
174+
{
175+
try {
176+
$reflection = new \ReflectionClass($model);
177+
if ($reflection->hasProperty('localKeys')) {
178+
$property = $reflection->getProperty('localKeys');
179+
// Safe: Accessing protected property from third-party package (staudenmeir/eloquent-has-many-deep)
180+
// The property exists and is part of the package's internal API
181+
$property->setAccessible(true); // NOSONAR
182+
$localKeys = $property->getValue($model); // NOSONAR
183+
if (is_array($localKeys) && ! empty($localKeys)) {
184+
return $localKeys;
185+
}
186+
}
187+
} catch (\Exception) {
188+
// Reflection failed - fall back to empty array
189+
// This is safe because callers handle empty arrays appropriately
190+
}
191+
192+
return [];
193+
}
194+
195+
/**
196+
* Extract the array of through models from a HasManyDeep relationship using reflection.
197+
*
198+
* @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep<\Illuminate\Database\Eloquent\Model, \Illuminate\Database\Eloquent\Model> $model
199+
*/
200+
private function getThroughModels($model): array
201+
{
202+
try {
203+
$reflection = new \ReflectionClass($model);
204+
if ($reflection->hasProperty('through')) {
205+
$property = $reflection->getProperty('through');
206+
// Safe: Accessing protected property from third-party package (staudenmeir/eloquent-has-many-deep)
207+
// The property exists and is part of the package's internal API
208+
$property->setAccessible(true); // NOSONAR
209+
$through = $property->getValue($model); // NOSONAR
210+
if (is_array($through) && ! empty($through)) {
211+
return $through;
212+
}
213+
}
214+
} catch (\Exception) {
215+
// Reflection failed - fall back to empty array
216+
// This is safe because callers handle empty arrays appropriately
217+
}
218+
219+
return [];
220+
}
221+
222+
/**
223+
* Extract the column name from a qualified column name (e.g., 'table.column' -> 'column').
224+
*/
225+
private function extractColumnFromQualified(string $qualified): string
226+
{
227+
if (str_contains($qualified, '.')) {
228+
$parts = explode('.', $qualified);
229+
230+
return end($parts);
231+
}
232+
233+
return $qualified;
234+
}
235+
}

0 commit comments

Comments
 (0)