diff --git a/README.md b/README.md index 0b834a1..2f33309 100644 --- a/README.md +++ b/README.md @@ -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+): diff --git a/src/Relations/BelongsToThrough.php b/src/Relations/BelongsToThrough.php index 48f956f..dfe7b81 100644 --- a/src/Relations/BelongsToThrough.php +++ b/src/Relations/BelongsToThrough.php @@ -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. * @@ -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); } @@ -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); }); } @@ -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(); } /** @@ -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); @@ -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); @@ -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(), @@ -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)); } /** @@ -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)); } /** diff --git a/src/Traits/BelongsToThrough.php b/src/Traits/BelongsToThrough.php index 7ce8a1f..c2b7faf 100644 --- a/src/Traits/BelongsToThrough.php +++ b/src/Traits/BelongsToThrough.php @@ -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; @@ -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; } /** @@ -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; @@ -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); } } diff --git a/tests/BelongsToThroughTest.php b/tests/BelongsToThroughTest.php index 709080f..bccf29d 100644 --- a/tests/BelongsToThroughTest.php +++ b/tests/BelongsToThroughTest.php @@ -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 { @@ -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()); + } } diff --git a/tests/Models/CustomerAddress.php b/tests/Models/CustomerAddress.php new file mode 100644 index 0000000..f1f4825 --- /dev/null +++ b/tests/Models/CustomerAddress.php @@ -0,0 +1,18 @@ +belongsToThrough( + VendorCustomer::class, + VendorCustomerAddress::class, + foreignKeyLookup: [VendorCustomerAddress::class => 'id'], + localKeyLookup: [VendorCustomerAddress::class => 'customer_address_id'], + ); + } +} diff --git a/tests/Models/VendorCustomer.php b/tests/Models/VendorCustomer.php new file mode 100644 index 0000000..cb00a11 --- /dev/null +++ b/tests/Models/VendorCustomer.php @@ -0,0 +1,8 @@ +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'); + }); } /** @@ -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(); } }