Skip to content

Commit 6f5c0ce

Browse files
Allow customizing through relations local keys (#89)
* Allow local keys for through relations * Added local keys lookup for through relations * Added local keys lookup usage * Test get through relations with customized local keys * Various improvements * Fix backward compatibility * Adjust method signature --------- Co-authored-by: Jonas Staudenmeir <[email protected]>
1 parent 02cb4fc commit 6f5c0ce

File tree

8 files changed

+200
-25
lines changed

8 files changed

+200
-25
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,30 @@ class Comment extends Model
8686
}
8787
```
8888

89+
You can specify custom local keys for the relations:
90+
91+
`VendorCustomerAddress` → belongs to → `VendorCustomer` in `VendorCustomerAddress.vendor_customer_id`
92+
`VendorCustomerAddress` → belongs to → `CustomerAddress` in `VendorCustomerAddress.address_id`
93+
94+
You can access `VendorCustomer` from `CustomerAddress` by the following
95+
96+
```php
97+
class CustomerAddress extends Model
98+
{
99+
use \Znck\Eloquent\Traits\BelongsToThrough;
100+
101+
public function vendorCustomer(): BelongsToThrough
102+
{
103+
return $this->belongsToThrough(
104+
VendorCustomer::class,
105+
VendorCustomerAddress::class,
106+
foreignKeyLookup: [VendorCustomerAddress::class => 'id'],
107+
localKeyLookup: [VendorCustomerAddress::class => 'address_id'],
108+
);
109+
}
110+
}
111+
```
112+
89113
### Table Aliases
90114

91115
If your relationship path contains the same model multiple times, you can specify a table alias (Laravel 6+):

src/Relations/BelongsToThrough.php

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ class BelongsToThrough extends Relation
4242
*/
4343
protected $foreignKeyLookup;
4444

45+
/**
46+
* The custom local keys on the relationship.
47+
*
48+
* @var array
49+
*/
50+
protected $localKeyLookup;
51+
4552
/**
4653
* Create a new belongs to through relationship instance.
4754
*
@@ -51,13 +58,23 @@ class BelongsToThrough extends Relation
5158
* @param string|null $localKey
5259
* @param string $prefix
5360
* @param array $foreignKeyLookup
61+
* @param array $localKeyLookup
62+
*
5463
* @return void
5564
*/
56-
public function __construct(Builder $query, Model $parent, array $throughParents, $localKey = null, $prefix = '', array $foreignKeyLookup = [])
57-
{
65+
public function __construct(
66+
Builder $query,
67+
Model $parent,
68+
array $throughParents,
69+
$localKey = null,
70+
$prefix = '',
71+
array $foreignKeyLookup = [],
72+
array $localKeyLookup = []
73+
) {
5874
$this->throughParents = $throughParents;
5975
$this->prefix = $prefix;
6076
$this->foreignKeyLookup = $foreignKeyLookup;
77+
$this->localKeyLookup = $localKeyLookup;
6178

6279
parent::__construct($query, $parent);
6380
}
@@ -93,14 +110,14 @@ protected function performJoins(Builder $query = null)
93110

94111
$first = $model->qualifyColumn($this->getForeignKeyName($predecessor));
95112

96-
$second = $predecessor->getQualifiedKeyName();
113+
$second = $predecessor->qualifyColumn($this->getLocalKeyName($predecessor));
97114

98115
$query->join($model->getTable(), $first, '=', $second);
99116

100117
if ($this->hasSoftDeletes($model)) {
101-
$column= $model->getQualifiedDeletedAtColumn();
118+
$column = $model->getQualifiedDeletedAtColumn();
102119

103-
$query->withGlobalScope(__CLASS__ . ":$column", function (Builder $query) use ($column) {
120+
$query->withGlobalScope(__CLASS__ . ":{$column}", function (Builder $query) use ($column) {
104121
$query->whereNull($column);
105122
});
106123
}
@@ -121,7 +138,24 @@ public function getForeignKeyName(Model $model = null)
121138
return $this->foreignKeyLookup[$table];
122139
}
123140

124-
return Str::singular($table).'_id';
141+
return Str::singular($table) . '_id';
142+
}
143+
144+
/**
145+
* Get the local key for a model.
146+
*
147+
* @param \Illuminate\Database\Eloquent\Model $model
148+
* @return string
149+
*/
150+
public function getLocalKeyName(Model $model): string
151+
{
152+
$table = explode(' as ', $model->getTable())[0];
153+
154+
if (array_key_exists($table, $this->localKeyLookup)) {
155+
return $this->localKeyLookup[$table];
156+
}
157+
158+
return $model->getKeyName();
125159
}
126160

127161
/**
@@ -225,7 +259,7 @@ public function getResults()
225259
public function first($columns = ['*'])
226260
{
227261
if ($columns === ['*']) {
228-
$columns = [$this->related->getTable().'.*'];
262+
$columns = [$this->related->getTable() . '.*'];
229263
}
230264

231265
return $this->query->first($columns);
@@ -242,10 +276,10 @@ public function get($columns = ['*'])
242276
$columns = $this->query->getQuery()->columns ? [] : $columns;
243277

244278
if ($columns === ['*']) {
245-
$columns = [$this->related->getTable().'.*'];
279+
$columns = [$this->related->getTable() . '.*'];
246280
}
247281

248-
$columns[] = $this->getQualifiedFirstLocalKeyName().' as '.static::THROUGH_KEY;
282+
$columns[] = $this->getQualifiedFirstLocalKeyName() . ' as ' . static::THROUGH_KEY;
249283

250284
$this->query->addSelect($columns);
251285

@@ -264,7 +298,7 @@ public function getRelationExistenceQuery(Builder $query, Builder $parent, $colu
264298
{
265299
$this->performJoins($query);
266300

267-
$foreignKey = $parent->getQuery()->from.'.'.$this->getFirstForeignKeyName();
301+
$foreignKey = $parent->getQuery()->from . '.' . $this->getFirstForeignKeyName();
268302

269303
return $query->select($columns)->whereColumn(
270304
$this->getQualifiedFirstLocalKeyName(),
@@ -315,7 +349,7 @@ public function getThroughParents()
315349
*/
316350
public function getFirstForeignKeyName()
317351
{
318-
return $this->prefix.$this->getForeignKeyName(end($this->throughParents));
352+
return $this->prefix . $this->getForeignKeyName(end($this->throughParents));
319353
}
320354

321355
/**
@@ -325,7 +359,9 @@ public function getFirstForeignKeyName()
325359
*/
326360
public function getQualifiedFirstLocalKeyName()
327361
{
328-
return end($this->throughParents)->getQualifiedKeyName();
362+
$lastThroughParent = end($this->throughParents);
363+
364+
return $lastThroughParent->qualifyColumn($this->getLocalKeyName($lastThroughParent));
329365
}
330366

331367
/**

src/Traits/BelongsToThrough.php

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,20 @@ trait BelongsToThrough
1616
* @param string|null $localKey
1717
* @param string $prefix
1818
* @param array $foreignKeyLookup
19+
* @param array $localKeyLookup
1920
* @return \Znck\Eloquent\Relations\BelongsToThrough
2021
*/
21-
public function belongsToThrough($related, $through, $localKey = null, $prefix = '', $foreignKeyLookup = [])
22-
{
22+
public function belongsToThrough(
23+
$related,
24+
$through,
25+
$localKey = null,
26+
$prefix = '',
27+
$foreignKeyLookup = [],
28+
array $localKeyLookup = []
29+
) {
2330
$relatedInstance = $this->newRelatedInstance($related);
24-
$throughParents = [];
25-
$foreignKeys = [];
31+
$throughParents = [];
32+
$foreignKeys = [];
2633

2734
foreach ((array) $through as $model) {
2835
$foreignKey = null;
@@ -42,15 +49,41 @@ public function belongsToThrough($related, $through, $localKey = null, $prefix =
4249
$throughParents[] = $instance;
4350
}
4451

45-
foreach ($foreignKeyLookup as $model => $foreignKey) {
52+
$foreignKeys = array_merge($foreignKeys, $this->mapKeys($foreignKeyLookup));
53+
54+
$localKeys = $this->mapKeys($localKeyLookup);
55+
56+
return $this->newBelongsToThrough(
57+
$relatedInstance->newQuery(),
58+
$this,
59+
$throughParents,
60+
$localKey,
61+
$prefix,
62+
$foreignKeys,
63+
$localKeys
64+
);
65+
}
66+
67+
/**
68+
* Map keys to an associative array where the key is the table name and the value is the key from the lookup.
69+
*
70+
* @param array $keyLookup
71+
* @return array
72+
*/
73+
protected function mapKeys(array $keyLookup): array
74+
{
75+
$keys = [];
76+
77+
// Iterate over each model and key in the key lookup
78+
foreach ($keyLookup as $model => $key) {
79+
// Create a new instance of the model
4680
$instance = new $model();
4781

48-
if ($foreignKey) {
49-
$foreignKeys[$instance->getTable()] = $foreignKey;
50-
}
82+
// Add the table name and key to the keys array
83+
$keys[$instance->getTable()] = $key;
5184
}
5285

53-
return $this->newBelongsToThrough($relatedInstance->newQuery(), $this, $throughParents, $localKey, $prefix, $foreignKeys);
86+
return $keys;
5487
}
5588

5689
/**
@@ -67,7 +100,7 @@ protected function belongsToThroughParentInstance($model)
67100
$instance = new $segments[0]();
68101

69102
if (isset($segments[1])) {
70-
$instance->setTable($instance->getTable().' as '.$segments[1]);
103+
$instance->setTable($instance->getTable() . ' as ' . $segments[1]);
71104
}
72105

73106
return $instance;
@@ -82,10 +115,18 @@ protected function belongsToThroughParentInstance($model)
82115
* @param string $localKey
83116
* @param string $prefix
84117
* @param array $foreignKeyLookup
118+
* @param array $localKeyLookup
85119
* @return \Znck\Eloquent\Relations\BelongsToThrough
86120
*/
87-
protected function newBelongsToThrough(Builder $query, Model $parent, array $throughParents, $localKey, $prefix, array $foreignKeyLookup)
88-
{
89-
return new Relation($query, $parent, $throughParents, $localKey, $prefix, $foreignKeyLookup);
121+
protected function newBelongsToThrough(
122+
Builder $query,
123+
Model $parent,
124+
array $throughParents,
125+
$localKey,
126+
$prefix,
127+
array $foreignKeyLookup,
128+
array $localKeyLookup
129+
) {
130+
return new Relation($query, $parent, $throughParents, $localKey, $prefix, $foreignKeyLookup, $localKeyLookup);
90131
}
91132
}

tests/BelongsToThroughTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
use Tests\Models\Comment;
66
use Tests\Models\Country;
7+
use Tests\Models\CustomerAddress;
78
use Tests\Models\Post;
89
use Tests\Models\User;
10+
use Tests\Models\VendorCustomer;
911

1012
class BelongsToThroughTest extends TestCase
1113
{
@@ -134,4 +136,14 @@ public function testGetThroughParents()
134136
$this->assertInstanceOf(User::class, $throughParents[0]);
135137
$this->assertInstanceOf(Post::class, $throughParents[1]);
136138
}
139+
140+
public function testGetThroughWithCustomizedLocalKeys()
141+
{
142+
$addresses = CustomerAddress::with('vendorCustomer')->get();
143+
144+
$this->assertEquals(41, $addresses[0]->vendorCustomer->id);
145+
$this->assertEquals(42, $addresses[1]->vendorCustomer->id);
146+
$this->assertInstanceOf(VendorCustomer::class, $addresses[1]->vendorCustomer);
147+
$this->assertFalse($addresses[2]->vendorCustomer()->exists());
148+
}
137149
}

tests/Models/CustomerAddress.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Tests\Models;
4+
5+
use Znck\Eloquent\Relations\BelongsToThrough;
6+
7+
class CustomerAddress extends Model
8+
{
9+
public function vendorCustomer(): BelongsToThrough
10+
{
11+
return $this->belongsToThrough(
12+
VendorCustomer::class,
13+
VendorCustomerAddress::class,
14+
foreignKeyLookup: [VendorCustomerAddress::class => 'id'],
15+
localKeyLookup: [VendorCustomerAddress::class => 'customer_address_id'],
16+
);
17+
}
18+
}

tests/Models/VendorCustomer.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Tests\Models;
4+
5+
class VendorCustomer extends Model
6+
{
7+
//
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Tests\Models;
4+
5+
class VendorCustomerAddress extends Model
6+
{
7+
//
8+
}

tests/TestCase.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99
use PHPUnit\Framework\TestCase as Base;
1010
use Tests\Models\Comment;
1111
use Tests\Models\Country;
12+
use Tests\Models\CustomerAddress;
1213
use Tests\Models\Post;
1314
use Tests\Models\User;
15+
use Tests\Models\VendorCustomer;
16+
use Tests\Models\VendorCustomerAddress;
1417

1518
abstract class TestCase extends Base
1619
{
@@ -61,6 +64,20 @@ protected function migrate()
6164
$table->unsignedInteger('custom_post_id')->nullable();
6265
$table->unsignedInteger('parent_id')->nullable();
6366
});
67+
68+
DB::schema()->create('vendor_customers', function (Blueprint $table) {
69+
$table->increments('id');
70+
});
71+
72+
DB::schema()->create('customer_addresses', function (Blueprint $table) {
73+
$table->increments('id');
74+
});
75+
76+
DB::schema()->create('vendor_customer_addresses', function (Blueprint $table) {
77+
$table->increments('id');
78+
$table->unsignedInteger('vendor_customer_id');
79+
$table->unsignedInteger('customer_address_id');
80+
});
6481
}
6582

6683
/**
@@ -91,6 +108,17 @@ protected function seed()
91108
Comment::create(['id' => 34, 'post_id' => null, 'custom_post_id' => 21, 'parent_id' => 33]);
92109
Comment::create(['id' => 35, 'post_id' => null, 'custom_post_id' => 24, 'parent_id' => 34]);
93110

111+
VendorCustomer::create(['id' => 41]);
112+
VendorCustomer::create(['id' => 42]);
113+
VendorCustomer::create(['id' => 43]);
114+
115+
CustomerAddress::create(['id' => 51]);
116+
CustomerAddress::create(['id' => 52]);
117+
CustomerAddress::create(['id' => 53]);
118+
119+
VendorCustomerAddress::create(['id' => 61, 'vendor_customer_id' => 41, 'customer_address_id' => 51]);
120+
VendorCustomerAddress::create(['id' => 62, 'vendor_customer_id' => 42, 'customer_address_id' => 52]);
121+
94122
Model::reguard();
95123
}
96124
}

0 commit comments

Comments
 (0)