├── src ├── Traits │ ├── HasTableAlias.php │ └── BelongsToThrough.php ├── IdeHelperServiceProvider.php ├── IdeHelper │ └── BelongsToThroughRelationsHook.php └── Relations │ └── BelongsToThrough.php ├── LICENSE ├── composer.json └── README.md /src/Traits/HasTableAlias.php: -------------------------------------------------------------------------------- 1 | getTable(); 18 | 19 | if (str_contains($table, ' as ')) { 20 | $table = explode(' as ', $table)[1]; 21 | } 22 | 23 | return $table.'.'.$column; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015 Rahul Kadyan 4 | Copyright (c) 2019 Jonas Staudenmeir 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "staudenmeir/belongs-to-through", 3 | "description": "Laravel Eloquent BelongsToThrough relationships", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Rahul Kadyan", 8 | "email": "hi@znck.me" 9 | }, 10 | { 11 | "name": "Jonas Staudenmeir", 12 | "email": "mail@jonas-staudenmeir.de" 13 | } 14 | ], 15 | "require": { 16 | "php": "^8.2", 17 | "illuminate/database": "^12.0" 18 | }, 19 | "require-dev": { 20 | "barryvdh/laravel-ide-helper": "^3.0", 21 | "larastan/larastan": "^3.0", 22 | "laravel/framework": "^12.0", 23 | "mockery/mockery": "^1.5.1", 24 | "orchestra/testbench-core": "^10.0", 25 | "phpunit/phpunit": "^11.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Staudenmeir\\BelongsToThrough\\": "src/", 30 | "Znck\\Eloquent\\": "src/" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Tests\\": "tests/" 36 | } 37 | }, 38 | "config": { 39 | "sort-packages": true 40 | }, 41 | "extra": { 42 | "laravel": { 43 | "providers": [ 44 | "Staudenmeir\\BelongsToThrough\\IdeHelperServiceProvider" 45 | ] 46 | } 47 | }, 48 | "minimum-stability": "dev", 49 | "prefer-stable": true 50 | } 51 | -------------------------------------------------------------------------------- /src/IdeHelperServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->alias(ModelsCommand::class, static::ModelsCommandAlias); 24 | } 25 | 26 | public function register(): void 27 | { 28 | /** @var \Illuminate\Config\Repository $config */ 29 | $config = $this->app->get('config'); 30 | 31 | $config->set( 32 | 'ide-helper.model_hooks', 33 | array_merge( 34 | [BelongsToThroughRelationsHook::class], 35 | $config->array('ide-helper.model_hooks', []) 36 | ) 37 | ); 38 | } 39 | 40 | /** 41 | * @return list 42 | */ 43 | public function provides(): array 44 | { 45 | return [ 46 | static::ModelsCommandAlias, 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/IdeHelper/BelongsToThroughRelationsHook.php: -------------------------------------------------------------------------------- 1 | getMethods(ReflectionMethod::IS_PUBLIC); 26 | 27 | foreach ($methods as $method) { 28 | if ($method->isAbstract() || $method->isStatic() || !$method->isPublic() 29 | || $method->getNumberOfParameters() > 0 || $method->getDeclaringClass()->getName() === Model::class) { 30 | continue; 31 | } 32 | 33 | if ($method->getReturnType() instanceof ReflectionNamedType 34 | && $method->getReturnType()->getName() === BelongsToThroughRelation::class) { 35 | /** @var \Illuminate\Database\Eloquent\Relations\Relation<*, *, *> $relationship */ 36 | $relationship = $method->invoke($model); 37 | 38 | $this->addRelationship($command, $method, $relationship); 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * @param \Illuminate\Database\Eloquent\Relations\Relation<*, *, *> $relationship 45 | */ 46 | protected function addRelationship(ModelsCommand $command, ReflectionMethod $method, Relation $relationship): void 47 | { 48 | $type = '\\' . $relationship->getRelated()::class; 49 | 50 | $command->setProperty( 51 | $method->getName(), 52 | $type, 53 | true, 54 | false, 55 | '', 56 | true 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Traits/BelongsToThrough.php: -------------------------------------------------------------------------------- 1 | $related 20 | * @param non-empty-list>|non-empty-list, 1: string}>|class-string<\Illuminate\Database\Eloquent\Model> $through 21 | * @param string|null $localKey 22 | * @param string $prefix 23 | * @param array, string> $foreignKeyLookup 24 | * @param array, string> $localKeyLookup 25 | * @return \Znck\Eloquent\Relations\BelongsToThrough 26 | */ 27 | public function belongsToThrough( 28 | $related, 29 | $through, 30 | $localKey = null, 31 | $prefix = '', 32 | $foreignKeyLookup = [], 33 | array $localKeyLookup = [] 34 | ) { 35 | /** @var TRelatedModel $relatedInstance */ 36 | $relatedInstance = $this->newRelatedInstance($related); 37 | 38 | /** @var list<\Illuminate\Database\Eloquent\Model> $throughParents */ 39 | $throughParents = []; 40 | $foreignKeys = []; 41 | 42 | foreach ((array) $through as $model) { 43 | $foreignKey = null; 44 | 45 | if (is_array($model)) { 46 | /** @var string $foreignKey */ 47 | $foreignKey = $model[1]; 48 | 49 | /** @var class-string<\Illuminate\Database\Eloquent\Model> $model */ 50 | $model = $model[0]; 51 | } 52 | 53 | $instance = $this->belongsToThroughParentInstance($model); 54 | 55 | if ($foreignKey) { 56 | $foreignKeys[$instance->getTable()] = $foreignKey; 57 | } 58 | 59 | $throughParents[] = $instance; 60 | } 61 | 62 | $foreignKeys = array_merge($foreignKeys, $this->mapKeys($foreignKeyLookup)); 63 | 64 | $localKeys = $this->mapKeys($localKeyLookup); 65 | 66 | return $this->newBelongsToThrough( 67 | $relatedInstance->newQuery(), 68 | $this, 69 | $throughParents, 70 | $localKey, 71 | $prefix, 72 | $foreignKeys, 73 | $localKeys 74 | ); 75 | } 76 | 77 | /** 78 | * Map keys to an associative array where the key is the table name and the value is the key from the lookup. 79 | * 80 | * @param array, string> $keyLookup 81 | * @return array 82 | */ 83 | protected function mapKeys(array $keyLookup): array 84 | { 85 | $keys = []; 86 | 87 | // Iterate over each model and key in the key lookup 88 | foreach ($keyLookup as $model => $key) { 89 | // Create a new instance of the model 90 | $instance = new $model(); 91 | 92 | // Add the table name and key to the keys array 93 | $keys[$instance->getTable()] = $key; 94 | } 95 | 96 | return $keys; 97 | } 98 | 99 | /** 100 | * Create a through parent instance for a belongs-to-through relationship. 101 | * 102 | * @template TModel of \Illuminate\Database\Eloquent\Model 103 | * 104 | * @param class-string $model 105 | * @return TModel 106 | */ 107 | protected function belongsToThroughParentInstance($model) 108 | { 109 | /** @var array{0: class-string, 1?: string} $segments */ 110 | $segments = preg_split('/\s+as\s+/i', $model); 111 | 112 | /** @var TModel $instance */ 113 | $instance = new $segments[0](); 114 | 115 | if (isset($segments[1])) { 116 | $instance->setTable($instance->getTable() . ' as ' . $segments[1]); 117 | } 118 | 119 | return $instance; 120 | } 121 | 122 | /** 123 | * Instantiate a new BelongsToThrough relationship. 124 | * 125 | * @template TRelatedModel of \Illuminate\Database\Eloquent\Model 126 | * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model 127 | * 128 | * @param \Illuminate\Database\Eloquent\Builder $query 129 | * @param TDeclaringModel $parent 130 | * @param non-empty-list<\Illuminate\Database\Eloquent\Model> $throughParents 131 | * @param string|null $localKey 132 | * @param string $prefix 133 | * @param array $foreignKeyLookup 134 | * @param array $localKeyLookup 135 | * @return \Znck\Eloquent\Relations\BelongsToThrough 136 | */ 137 | protected function newBelongsToThrough( 138 | Builder $query, 139 | Model $parent, 140 | array $throughParents, 141 | $localKey, 142 | $prefix, 143 | array $foreignKeyLookup, 144 | array $localKeyLookup 145 | ) { 146 | return new Relation($query, $parent, $throughParents, $localKey, $prefix, $foreignKeyLookup, $localKeyLookup); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BelongsToThrough 2 | 3 | [![CI](https://github.com/staudenmeir/belongs-to-through/actions/workflows/ci.yml/badge.svg)](https://github.com/staudenmeir/belongs-to-through/actions/workflows/ci.yml?query=branch%3Amain) 4 | [![Code Coverage](https://codecov.io/gh/staudenmeir/belongs-to-through/graph/badge.svg?token=Z4KscVFWIE)](https://codecov.io/gh/staudenmeir/belongs-to-through) 5 | [![PHPStan](https://img.shields.io/badge/PHPStan-level%2010-brightgreen.svg?style=flat)](https://github.com/staudenmeir/belongs-to-through/actions/workflows/static-analysis.yml?query=branch%3Amain) 6 | [![Latest Stable Version](https://poser.pugx.org/staudenmeir/belongs-to-through/v/stable)](https://packagist.org/packages/staudenmeir/belongs-to-through) 7 | [![Total Downloads](https://poser.pugx.org/staudenmeir/belongs-to-through/downloads)](https://packagist.org/packages/staudenmeir/belongs-to-through/stats) 8 | [![License](https://poser.pugx.org/staudenmeir/belongs-to-through/license)](https://github.com/staudenmeir/belongs-to-through/blob/main/LICENSE) 9 | 10 | This inverse version of `HasManyThrough` allows `BelongsToThrough` relationships with unlimited intermediate models. 11 | 12 | Supports Laravel 5.0+. 13 | 14 | ## Installation 15 | 16 | composer require staudenmeir/belongs-to-through:"^2.5" 17 | 18 | Use this command if you are in PowerShell on Windows (e.g. in VS Code): 19 | 20 | composer require staudenmeir/belongs-to-through:"^^^^2.5" 21 | 22 | ## Versions 23 | 24 | | Laravel | Package | 25 | |:--------|:--------| 26 | | 12.x | 2.17 | 27 | | 11.x | 2.16 | 28 | | 10.x | 2.13 | 29 | | 9.x | 2.12 | 30 | | 8.x | 2.11 | 31 | | 7.x | 2.10 | 32 | | 6.x | 2.6 | 33 | | 5.x | 2.5 | 34 | 35 | ## Usage 36 | 37 | - [Custom Foreign Keys](#custom-foreign-keys) 38 | - [Custom Local Keys](#custom-local-keys) 39 | - [Table Aliases](#table-aliases) 40 | - [Soft Deleting](#soft-deleting) 41 | 42 | Consider this `HasManyThrough` relationship: 43 | `Country` → has many → `User` → has many → `Post` 44 | 45 | ```php 46 | class Country extends Model 47 | { 48 | public function posts() 49 | { 50 | return $this->hasManyThrough(Post::class, User::class); 51 | } 52 | } 53 | ``` 54 | 55 | Use the `BelongsToThrough` trait in your model to define the inverse relationship: 56 | `Post` → belongs to → `User` → belongs to → `Country` 57 | 58 | ```php 59 | class Post extends Model 60 | { 61 | use \Znck\Eloquent\Traits\BelongsToThrough; 62 | 63 | public function country(): \Znck\Eloquent\Relations\BelongsToThrough 64 | { 65 | return $this->belongsToThrough(Country::class, User::class); 66 | } 67 | } 68 | ``` 69 | 70 | You can also define deeper relationships: 71 | `Comment` → belongs to → `Post` → belongs to → `User` → belongs to → `Country` 72 | 73 | Supply an array of intermediate models as the second argument, from the related (`Country`) to the parent model (`Comment`): 74 | 75 | ```php 76 | class Comment extends Model 77 | { 78 | use \Znck\Eloquent\Traits\BelongsToThrough; 79 | 80 | public function country(): \Znck\Eloquent\Relations\BelongsToThrough 81 | { 82 | return $this->belongsToThrough(Country::class, [User::class, Post::class]); 83 | } 84 | } 85 | ``` 86 | 87 | ### Custom Foreign Keys 88 | 89 | You can specify custom foreign keys as the fifth argument: 90 | 91 | ```php 92 | class Comment extends Model 93 | { 94 | use \Znck\Eloquent\Traits\BelongsToThrough; 95 | 96 | public function country(): \Znck\Eloquent\Relations\BelongsToThrough 97 | { 98 | return $this->belongsToThrough( 99 | Country::class, 100 | [User::class, Post::class], 101 | foreignKeyLookup: [User::class => 'custom_user_id'] 102 | ); 103 | } 104 | } 105 | ``` 106 | 107 | ### Custom Local Keys 108 | 109 | You can specify custom local keys for the relations: 110 | 111 | `VendorCustomerAddress` → belongs to → `VendorCustomer` in `VendorCustomerAddress.vendor_customer_id` 112 | `VendorCustomerAddress` → belongs to → `CustomerAddress` in `VendorCustomerAddress.address_id` 113 | 114 | You can access `VendorCustomer` from `CustomerAddress` by the following 115 | 116 | ```php 117 | class CustomerAddress extends Model 118 | { 119 | use \Znck\Eloquent\Traits\BelongsToThrough; 120 | 121 | public function vendorCustomer(): \Znck\Eloquent\Relations\BelongsToThrough 122 | { 123 | return $this->belongsToThrough( 124 | VendorCustomer::class, 125 | VendorCustomerAddress::class, 126 | foreignKeyLookup: [VendorCustomerAddress::class => 'id'], 127 | localKeyLookup: [VendorCustomerAddress::class => 'address_id'], 128 | ); 129 | } 130 | } 131 | ``` 132 | 133 | ### Table Aliases 134 | 135 | If your relationship path contains the same model multiple times, you can specify a table alias (Laravel 6+): 136 | 137 | ```php 138 | class Comment extends Model 139 | { 140 | use \Znck\Eloquent\Traits\BelongsToThrough; 141 | 142 | public function grandparent(): \Znck\Eloquent\Relations\BelongsToThrough 143 | { 144 | return $this->belongsToThrough( 145 | Comment::class, 146 | Comment::class . ' as alias', 147 | foreignKeyLookup: [Comment::class => 'parent_id'] 148 | ); 149 | } 150 | } 151 | ``` 152 | 153 | Use the `HasTableAlias` trait in the models you are aliasing: 154 | 155 | ```php 156 | class Comment extends Model 157 | { 158 | use \Znck\Eloquent\Traits\HasTableAlias; 159 | } 160 | ``` 161 | 162 | ### Soft Deleting 163 | 164 | By default, soft-deleted intermediate models will be excluded from the result. Use `withTrashed()` to include them: 165 | 166 | ```php 167 | class Comment extends Model 168 | { 169 | use \Znck\Eloquent\Traits\BelongsToThrough; 170 | 171 | public function country(): \Znck\Eloquent\Relations\BelongsToThrough 172 | { 173 | return $this->belongsToThrough(Country::class, [User::class, Post::class]) 174 | ->withTrashed('users.deleted_at'); 175 | } 176 | } 177 | 178 | class User extends Model 179 | { 180 | use SoftDeletes; 181 | } 182 | ``` 183 | 184 | ## Contributing 185 | 186 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) and [CODE OF CONDUCT](.github/CODE_OF_CONDUCT.md) for details. 187 | 188 | ## Credits 189 | 190 | - [Rahul Kadyan](https://github.com/znck) 191 | - [Danny Weeks](https://github.com/dannyweeks) 192 | - [All Contributors](../../contributors) 193 | -------------------------------------------------------------------------------- /src/Relations/BelongsToThrough.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class BelongsToThrough extends Relation 21 | { 22 | use SupportsDefaultModels; 23 | 24 | /** 25 | * The column alias for the local key on the first "through" parent model. 26 | * 27 | * @var string 28 | */ 29 | public const THROUGH_KEY = 'laravel_through_key'; 30 | 31 | /** 32 | * The "through" parent model instances. 33 | * 34 | * @var non-empty-list<\Illuminate\Database\Eloquent\Model> 35 | */ 36 | protected $throughParents; 37 | 38 | /** 39 | * The foreign key prefix for the first "through" parent model. 40 | * 41 | * @var string 42 | */ 43 | protected $prefix; 44 | 45 | /** 46 | * The custom foreign keys on the relationship. 47 | * 48 | * @var array 49 | */ 50 | protected $foreignKeyLookup; 51 | 52 | /** 53 | * The custom local keys on the relationship. 54 | * 55 | * @var array 56 | */ 57 | protected $localKeyLookup; 58 | 59 | /** 60 | * Create a new belongs to through relationship instance. 61 | * 62 | * @param \Illuminate\Database\Eloquent\Builder $query 63 | * @param TDeclaringModel $parent 64 | * @param non-empty-list<\Illuminate\Database\Eloquent\Model> $throughParents 65 | * @param string|null $localKey 66 | * @param string $prefix 67 | * @param array $foreignKeyLookup 68 | * @param array $localKeyLookup 69 | * @return void 70 | * 71 | * @phpstan-ignore constructor.unusedParameter($localKey) 72 | */ 73 | public function __construct( 74 | Builder $query, 75 | Model $parent, 76 | array $throughParents, 77 | $localKey = null, 78 | $prefix = '', 79 | array $foreignKeyLookup = [], 80 | array $localKeyLookup = [] 81 | ) { 82 | $this->throughParents = $throughParents; 83 | $this->prefix = $prefix; 84 | $this->foreignKeyLookup = $foreignKeyLookup; 85 | $this->localKeyLookup = $localKeyLookup; 86 | 87 | parent::__construct($query, $parent); 88 | } 89 | 90 | /** @inheritDoc */ 91 | public function addConstraints() 92 | { 93 | $this->performJoins(); 94 | 95 | if (static::$constraints) { 96 | $localValue = $this->parent[$this->getFirstForeignKeyName()]; 97 | 98 | $this->query->where($this->getQualifiedFirstLocalKeyName(), '=', $localValue); 99 | } 100 | } 101 | 102 | /** 103 | * Set the join clauses on the query. 104 | * 105 | * @param \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>|null $query 106 | * @return void 107 | */ 108 | protected function performJoins(?Builder $query = null) 109 | { 110 | $query = $query ?: $this->query; 111 | 112 | foreach ($this->throughParents as $i => $model) { 113 | $predecessor = $i > 0 ? $this->throughParents[$i - 1] : $this->related; 114 | 115 | $first = $model->qualifyColumn($this->getForeignKeyName($predecessor)); 116 | 117 | $second = $predecessor->qualifyColumn($this->getLocalKeyName($predecessor)); 118 | 119 | $query->join($model->getTable(), $first, '=', $second); 120 | 121 | if ($this->hasSoftDeletes($model)) { 122 | /** @var string $column */ 123 | /** @phpstan-ignore method.notFound */ 124 | $column = $model->getQualifiedDeletedAtColumn(); 125 | 126 | $query->withGlobalScope(__CLASS__ . ":{$column}", function (Builder $query) use ($column) { 127 | $query->whereNull($column); 128 | }); 129 | } 130 | } 131 | } 132 | 133 | /** 134 | * Get the foreign key for a model. 135 | * 136 | * @param \Illuminate\Database\Eloquent\Model|null $model 137 | * @return string 138 | */ 139 | public function getForeignKeyName(?Model $model = null) 140 | { 141 | $table = explode(' as ', ($model ?? $this->parent)->getTable())[0]; 142 | 143 | if (array_key_exists($table, $this->foreignKeyLookup)) { 144 | return $this->foreignKeyLookup[$table]; 145 | } 146 | 147 | return Str::singular($table) . '_id'; 148 | } 149 | 150 | /** 151 | * Get the local key for a model. 152 | * 153 | * @param \Illuminate\Database\Eloquent\Model $model 154 | * @return string 155 | */ 156 | public function getLocalKeyName(Model $model): string 157 | { 158 | $table = explode(' as ', $model->getTable())[0]; 159 | 160 | if (array_key_exists($table, $this->localKeyLookup)) { 161 | return $this->localKeyLookup[$table]; 162 | } 163 | 164 | return $model->getKeyName(); 165 | } 166 | 167 | /** 168 | * Determine whether a model uses SoftDeletes. 169 | * 170 | * @param \Illuminate\Database\Eloquent\Model $model 171 | * @return bool 172 | */ 173 | public function hasSoftDeletes(Model $model) 174 | { 175 | return in_array(SoftDeletes::class, class_uses_recursive($model)); 176 | } 177 | 178 | /** @inheritDoc */ 179 | public function addEagerConstraints(array $models) 180 | { 181 | $keys = $this->getKeys($models, $this->getFirstForeignKeyName()); 182 | 183 | $this->query->whereIn($this->getQualifiedFirstLocalKeyName(), $keys); 184 | } 185 | 186 | /** @inheritDoc */ 187 | public function initRelation(array $models, $relation) 188 | { 189 | foreach ($models as $model) { 190 | $model->setRelation($relation, $this->getDefaultFor($model)); 191 | } 192 | 193 | return $models; 194 | } 195 | 196 | /** @inheritDoc */ 197 | public function match(array $models, Collection $results, $relation) 198 | { 199 | $dictionary = $this->buildDictionary($results); 200 | 201 | foreach ($models as $model) { 202 | $key = $model[$this->getFirstForeignKeyName()]; 203 | 204 | if (isset($dictionary[$key])) { 205 | $model->setRelation($relation, $dictionary[$key]); 206 | } 207 | } 208 | 209 | return $models; 210 | } 211 | 212 | /** 213 | * Build model dictionary keyed by the relation's foreign key. 214 | * 215 | * @param \Illuminate\Database\Eloquent\Collection $results 216 | * @return TRelatedModel[] 217 | */ 218 | protected function buildDictionary(Collection $results) 219 | { 220 | $dictionary = []; 221 | 222 | foreach ($results as $result) { 223 | $dictionary[$result[static::THROUGH_KEY]] = $result; 224 | 225 | unset($result[static::THROUGH_KEY]); 226 | } 227 | 228 | return $dictionary; 229 | } 230 | 231 | /** 232 | * Get the results of the relationship. 233 | * 234 | * @return TRelatedModel|object|static|null 235 | */ 236 | public function getResults() 237 | { 238 | return $this->first() ?: $this->getDefaultFor($this->parent); 239 | } 240 | 241 | /** 242 | * Execute the query and get the first result. 243 | * 244 | * @param string[] $columns 245 | * @return TRelatedModel|object|static|null 246 | */ 247 | public function first($columns = ['*']) 248 | { 249 | if ($columns === ['*']) { 250 | $columns = [$this->related->getTable() . '.*']; 251 | } 252 | 253 | return $this->query->first($columns); 254 | } 255 | 256 | /** 257 | * Execute the query as a "select" statement. 258 | * 259 | * @param list $columns 260 | * @return \Illuminate\Database\Eloquent\Collection 261 | */ 262 | public function get($columns = ['*']) 263 | { 264 | $columns = $this->query->getQuery()->columns ? [] : $columns; 265 | 266 | if ($columns === ['*']) { 267 | $columns = [$this->related->getTable() . '.*']; 268 | } 269 | 270 | $columns[] = $this->getQualifiedFirstLocalKeyName() . ' as ' . static::THROUGH_KEY; 271 | 272 | $this->query->addSelect($columns); 273 | 274 | return $this->query->get(); 275 | } 276 | 277 | /** @inheritDoc */ 278 | public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) 279 | { 280 | $this->performJoins($query); 281 | 282 | $from = $parentQuery->getQuery()->from; 283 | 284 | if ($from instanceof Expression) { 285 | $from = $from->getValue( 286 | $parentQuery->getGrammar() 287 | ); 288 | } 289 | 290 | $foreignKey = $from . '.' . $this->getFirstForeignKeyName(); 291 | 292 | /** @var \Illuminate\Database\Eloquent\Builder $query */ 293 | $query = $query->select($columns)->whereColumn( 294 | $this->getQualifiedFirstLocalKeyName(), 295 | '=', 296 | $foreignKey 297 | ); 298 | 299 | return $query; 300 | } 301 | 302 | /** 303 | * Restore soft-deleted models. 304 | * 305 | * @param string[]|string ...$columns 306 | * @return $this 307 | */ 308 | public function withTrashed(...$columns) 309 | { 310 | if (empty($columns)) { 311 | /** @phpstan-ignore method.notFound */ 312 | $this->query->withTrashed(); 313 | 314 | return $this; 315 | } 316 | 317 | if (is_array($columns[0])) { 318 | $columns = $columns[0]; 319 | } 320 | 321 | /** @var string[] $columns */ 322 | foreach ($columns as $column) { 323 | $this->query->withoutGlobalScope(__CLASS__ . ":$column"); 324 | } 325 | 326 | return $this; 327 | } 328 | 329 | /** 330 | * Get the "through" parent model instances. 331 | * 332 | * @return list<\Illuminate\Database\Eloquent\Model> 333 | */ 334 | public function getThroughParents() 335 | { 336 | return $this->throughParents; 337 | } 338 | 339 | /** 340 | * Get the foreign key for the first "through" parent model. 341 | * 342 | * @return string 343 | */ 344 | public function getFirstForeignKeyName() 345 | { 346 | $firstThroughParent = end($this->throughParents); 347 | 348 | return $this->prefix . $this->getForeignKeyName($firstThroughParent); 349 | } 350 | 351 | /** 352 | * Get the qualified local key for the first "through" parent model. 353 | * 354 | * @return string 355 | */ 356 | public function getQualifiedFirstLocalKeyName() 357 | { 358 | $firstThroughParent = end($this->throughParents); 359 | 360 | return $firstThroughParent->qualifyColumn($this->getLocalKeyName($firstThroughParent)); 361 | } 362 | 363 | /** 364 | * Make a new related instance for the given model. 365 | * 366 | * @param \Illuminate\Database\Eloquent\Model $parent 367 | * @return \Illuminate\Database\Eloquent\Model 368 | */ 369 | protected function newRelatedInstanceFor(Model $parent) 370 | { 371 | return $this->related->newInstance(); 372 | } 373 | } 374 | --------------------------------------------------------------------------------