Skip to content

Commit

Permalink
Allow customizing through relations local keys (#89)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
muhammedkamel and staudenmeir authored Nov 27, 2023
1 parent 02cb4fc commit 6f5c0ce
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 25 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,30 @@ class Comment extends Model
}
```

You can specify custom local keys for the relations:

`VendorCustomerAddress` → belongs to → `VendorCustomer` in `VendorCustomerAddress.vendor_customer_id`
`VendorCustomerAddress` → belongs to → `CustomerAddress` in `VendorCustomerAddress.address_id`

You can access `VendorCustomer` from `CustomerAddress` by the following

```php
class CustomerAddress extends Model
{
use \Znck\Eloquent\Traits\BelongsToThrough;

public function vendorCustomer(): BelongsToThrough
{
return $this->belongsToThrough(
VendorCustomer::class,
VendorCustomerAddress::class,
foreignKeyLookup: [VendorCustomerAddress::class => 'id'],
localKeyLookup: [VendorCustomerAddress::class => 'address_id'],
);
}
}
```

### Table Aliases

If your relationship path contains the same model multiple times, you can specify a table alias (Laravel 6+):
Expand Down
60 changes: 48 additions & 12 deletions src/Relations/BelongsToThrough.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ class BelongsToThrough extends Relation
*/
protected $foreignKeyLookup;

/**
* The custom local keys on the relationship.
*
* @var array
*/
protected $localKeyLookup;

/**
* Create a new belongs to through relationship instance.
*
Expand All @@ -51,13 +58,23 @@ class BelongsToThrough extends Relation
* @param string|null $localKey
* @param string $prefix
* @param array $foreignKeyLookup
* @param array $localKeyLookup
*
* @return void
*/
public function __construct(Builder $query, Model $parent, array $throughParents, $localKey = null, $prefix = '', array $foreignKeyLookup = [])
{
public function __construct(
Builder $query,
Model $parent,
array $throughParents,
$localKey = null,
$prefix = '',
array $foreignKeyLookup = [],
array $localKeyLookup = []
) {
$this->throughParents = $throughParents;
$this->prefix = $prefix;
$this->foreignKeyLookup = $foreignKeyLookup;
$this->localKeyLookup = $localKeyLookup;

parent::__construct($query, $parent);
}
Expand Down Expand Up @@ -93,14 +110,14 @@ protected function performJoins(Builder $query = null)

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

$second = $predecessor->getQualifiedKeyName();
$second = $predecessor->qualifyColumn($this->getLocalKeyName($predecessor));

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

if ($this->hasSoftDeletes($model)) {
$column= $model->getQualifiedDeletedAtColumn();
$column = $model->getQualifiedDeletedAtColumn();

$query->withGlobalScope(__CLASS__ . ":$column", function (Builder $query) use ($column) {
$query->withGlobalScope(__CLASS__ . ":{$column}", function (Builder $query) use ($column) {
$query->whereNull($column);
});
}
Expand All @@ -121,7 +138,24 @@ public function getForeignKeyName(Model $model = null)
return $this->foreignKeyLookup[$table];
}

return Str::singular($table).'_id';
return Str::singular($table) . '_id';
}

/**
* Get the local key for a model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return string
*/
public function getLocalKeyName(Model $model): string
{
$table = explode(' as ', $model->getTable())[0];

if (array_key_exists($table, $this->localKeyLookup)) {
return $this->localKeyLookup[$table];
}

return $model->getKeyName();
}

/**
Expand Down Expand Up @@ -225,7 +259,7 @@ public function getResults()
public function first($columns = ['*'])
{
if ($columns === ['*']) {
$columns = [$this->related->getTable().'.*'];
$columns = [$this->related->getTable() . '.*'];
}

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

if ($columns === ['*']) {
$columns = [$this->related->getTable().'.*'];
$columns = [$this->related->getTable() . '.*'];
}

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

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

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

$foreignKey = $parent->getQuery()->from.'.'.$this->getFirstForeignKeyName();
$foreignKey = $parent->getQuery()->from . '.' . $this->getFirstForeignKeyName();

return $query->select($columns)->whereColumn(
$this->getQualifiedFirstLocalKeyName(),
Expand Down Expand Up @@ -315,7 +349,7 @@ public function getThroughParents()
*/
public function getFirstForeignKeyName()
{
return $this->prefix.$this->getForeignKeyName(end($this->throughParents));
return $this->prefix . $this->getForeignKeyName(end($this->throughParents));
}

/**
Expand All @@ -325,7 +359,9 @@ public function getFirstForeignKeyName()
*/
public function getQualifiedFirstLocalKeyName()
{
return end($this->throughParents)->getQualifiedKeyName();
$lastThroughParent = end($this->throughParents);

return $lastThroughParent->qualifyColumn($this->getLocalKeyName($lastThroughParent));
}

/**
Expand Down
67 changes: 54 additions & 13 deletions src/Traits/BelongsToThrough.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,20 @@ trait BelongsToThrough
* @param string|null $localKey
* @param string $prefix
* @param array $foreignKeyLookup
* @param array $localKeyLookup
* @return \Znck\Eloquent\Relations\BelongsToThrough
*/
public function belongsToThrough($related, $through, $localKey = null, $prefix = '', $foreignKeyLookup = [])
{
public function belongsToThrough(
$related,
$through,
$localKey = null,
$prefix = '',
$foreignKeyLookup = [],
array $localKeyLookup = []
) {
$relatedInstance = $this->newRelatedInstance($related);
$throughParents = [];
$foreignKeys = [];
$throughParents = [];
$foreignKeys = [];

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

foreach ($foreignKeyLookup as $model => $foreignKey) {
$foreignKeys = array_merge($foreignKeys, $this->mapKeys($foreignKeyLookup));

$localKeys = $this->mapKeys($localKeyLookup);

return $this->newBelongsToThrough(
$relatedInstance->newQuery(),
$this,
$throughParents,
$localKey,
$prefix,
$foreignKeys,
$localKeys
);
}

/**
* Map keys to an associative array where the key is the table name and the value is the key from the lookup.
*
* @param array $keyLookup
* @return array
*/
protected function mapKeys(array $keyLookup): array
{
$keys = [];

// Iterate over each model and key in the key lookup
foreach ($keyLookup as $model => $key) {
// Create a new instance of the model
$instance = new $model();

if ($foreignKey) {
$foreignKeys[$instance->getTable()] = $foreignKey;
}
// Add the table name and key to the keys array
$keys[$instance->getTable()] = $key;
}

return $this->newBelongsToThrough($relatedInstance->newQuery(), $this, $throughParents, $localKey, $prefix, $foreignKeys);
return $keys;
}

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

if (isset($segments[1])) {
$instance->setTable($instance->getTable().' as '.$segments[1]);
$instance->setTable($instance->getTable() . ' as ' . $segments[1]);
}

return $instance;
Expand All @@ -82,10 +115,18 @@ protected function belongsToThroughParentInstance($model)
* @param string $localKey
* @param string $prefix
* @param array $foreignKeyLookup
* @param array $localKeyLookup
* @return \Znck\Eloquent\Relations\BelongsToThrough
*/
protected function newBelongsToThrough(Builder $query, Model $parent, array $throughParents, $localKey, $prefix, array $foreignKeyLookup)
{
return new Relation($query, $parent, $throughParents, $localKey, $prefix, $foreignKeyLookup);
protected function newBelongsToThrough(
Builder $query,
Model $parent,
array $throughParents,
$localKey,
$prefix,
array $foreignKeyLookup,
array $localKeyLookup
) {
return new Relation($query, $parent, $throughParents, $localKey, $prefix, $foreignKeyLookup, $localKeyLookup);
}
}
12 changes: 12 additions & 0 deletions tests/BelongsToThroughTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

use Tests\Models\Comment;
use Tests\Models\Country;
use Tests\Models\CustomerAddress;
use Tests\Models\Post;
use Tests\Models\User;
use Tests\Models\VendorCustomer;

class BelongsToThroughTest extends TestCase
{
Expand Down Expand Up @@ -134,4 +136,14 @@ public function testGetThroughParents()
$this->assertInstanceOf(User::class, $throughParents[0]);
$this->assertInstanceOf(Post::class, $throughParents[1]);
}

public function testGetThroughWithCustomizedLocalKeys()
{
$addresses = CustomerAddress::with('vendorCustomer')->get();

$this->assertEquals(41, $addresses[0]->vendorCustomer->id);
$this->assertEquals(42, $addresses[1]->vendorCustomer->id);
$this->assertInstanceOf(VendorCustomer::class, $addresses[1]->vendorCustomer);
$this->assertFalse($addresses[2]->vendorCustomer()->exists());
}
}
18 changes: 18 additions & 0 deletions tests/Models/CustomerAddress.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Tests\Models;

use Znck\Eloquent\Relations\BelongsToThrough;

class CustomerAddress extends Model
{
public function vendorCustomer(): BelongsToThrough
{
return $this->belongsToThrough(
VendorCustomer::class,
VendorCustomerAddress::class,
foreignKeyLookup: [VendorCustomerAddress::class => 'id'],
localKeyLookup: [VendorCustomerAddress::class => 'customer_address_id'],
);
}
}
8 changes: 8 additions & 0 deletions tests/Models/VendorCustomer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Tests\Models;

class VendorCustomer extends Model
{
//
}
8 changes: 8 additions & 0 deletions tests/Models/VendorCustomerAddress.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Tests\Models;

class VendorCustomerAddress extends Model
{
//
}
28 changes: 28 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
use PHPUnit\Framework\TestCase as Base;
use Tests\Models\Comment;
use Tests\Models\Country;
use Tests\Models\CustomerAddress;
use Tests\Models\Post;
use Tests\Models\User;
use Tests\Models\VendorCustomer;
use Tests\Models\VendorCustomerAddress;

abstract class TestCase extends Base
{
Expand Down Expand Up @@ -61,6 +64,20 @@ protected function migrate()
$table->unsignedInteger('custom_post_id')->nullable();
$table->unsignedInteger('parent_id')->nullable();
});

DB::schema()->create('vendor_customers', function (Blueprint $table) {
$table->increments('id');
});

DB::schema()->create('customer_addresses', function (Blueprint $table) {
$table->increments('id');
});

DB::schema()->create('vendor_customer_addresses', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('vendor_customer_id');
$table->unsignedInteger('customer_address_id');
});
}

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

VendorCustomer::create(['id' => 41]);
VendorCustomer::create(['id' => 42]);
VendorCustomer::create(['id' => 43]);

CustomerAddress::create(['id' => 51]);
CustomerAddress::create(['id' => 52]);
CustomerAddress::create(['id' => 53]);

VendorCustomerAddress::create(['id' => 61, 'vendor_customer_id' => 41, 'customer_address_id' => 51]);
VendorCustomerAddress::create(['id' => 62, 'vendor_customer_id' => 42, 'customer_address_id' => 52]);

Model::reguard();
}
}

0 comments on commit 6f5c0ce

Please sign in to comment.