Skip to content

Commit 7745f03

Browse files
authored
[11.x] Fix fluent syntax for HasManyThrough when combining HasMany followed by HasOne (#53335)
* Add testcase for HasMany followed by HasOne to testPendingHasThroughRelationship * Fix PendingHasThroughRelationship for HasMany followed by HasOne * Fix conditional types on 'through' relationships by introducing additional template type
1 parent 091d4d4 commit 7745f03

File tree

4 files changed

+89
-8
lines changed

4 files changed

+89
-8
lines changed

src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,11 @@ protected function guessBelongsToRelation()
394394
* @return (
395395
* $relationship is string
396396
* ? \Illuminate\Database\Eloquent\PendingHasThroughRelationship<\Illuminate\Database\Eloquent\Model, $this>
397-
* : \Illuminate\Database\Eloquent\PendingHasThroughRelationship<TIntermediateModel, $this>
397+
* : (
398+
* $relationship is \Illuminate\Database\Eloquent\Relations\HasMany<TIntermediateModel, $this>
399+
* ? \Illuminate\Database\Eloquent\PendingHasThroughRelationship<TIntermediateModel, $this, \Illuminate\Database\Eloquent\Relations\HasMany<TIntermediateModel, $this>>
400+
* : \Illuminate\Database\Eloquent\PendingHasThroughRelationship<TIntermediateModel, $this, \Illuminate\Database\Eloquent\Relations\HasOne<TIntermediateModel, $this>>
401+
* )
398402
* )
399403
*/
400404
public function through($relationship)

src/Illuminate/Database/Eloquent/PendingHasThroughRelationship.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
/**
1111
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
1212
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
13+
* @template TLocalRelationship of \Illuminate\Database\Eloquent\Relations\HasOneOrMany<TIntermediateModel, TDeclaringModel>
1314
*/
1415
class PendingHasThroughRelationship
1516
{
@@ -23,15 +24,15 @@ class PendingHasThroughRelationship
2324
/**
2425
* The local relationship.
2526
*
26-
* @var \Illuminate\Database\Eloquent\Relations\HasMany<TIntermediateModel, TDeclaringModel>|\Illuminate\Database\Eloquent\Relations\HasOne<TIntermediateModel, TDeclaringModel>
27+
* @var TLocalRelationship
2728
*/
2829
protected $localRelationship;
2930

3031
/**
3132
* Create a pending has-many-through or has-one-through relationship.
3233
*
3334
* @param TDeclaringModel $rootModel
34-
* @param \Illuminate\Database\Eloquent\Relations\HasMany<TIntermediateModel, TDeclaringModel>|\Illuminate\Database\Eloquent\Relations\HasOne<TIntermediateModel, TDeclaringModel> $localRelationship
35+
* @param TLocalRelationship $localRelationship
3536
*/
3637
public function __construct($rootModel, $localRelationship)
3738
{
@@ -50,9 +51,13 @@ public function __construct($rootModel, $localRelationship)
5051
* $callback is string
5152
* ? \Illuminate\Database\Eloquent\Relations\HasManyThrough<\Illuminate\Database\Eloquent\Model, TIntermediateModel, TDeclaringModel>|\Illuminate\Database\Eloquent\Relations\HasOneThrough<\Illuminate\Database\Eloquent\Model, TIntermediateModel, TDeclaringModel>
5253
* : (
53-
* $callback is callable(TIntermediateModel): \Illuminate\Database\Eloquent\Relations\HasOne<TRelatedModel, TIntermediateModel>
54-
* ? \Illuminate\Database\Eloquent\Relations\HasOneThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
55-
* : \Illuminate\Database\Eloquent\Relations\HasManyThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
54+
* TLocalRelationship is \Illuminate\Database\Eloquent\Relations\HasMany<TIntermediateModel, TDeclaringModel>
55+
* ? \Illuminate\Database\Eloquent\Relations\HasManyThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
56+
* : (
57+
* $callback is callable(TIntermediateModel): \Illuminate\Database\Eloquent\Relations\HasMany<TRelatedModel, TIntermediateModel>
58+
* ? \Illuminate\Database\Eloquent\Relations\HasManyThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
59+
* : \Illuminate\Database\Eloquent\Relations\HasOneThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
60+
* )
5661
* )
5762
* )
5863
*/
@@ -64,7 +69,7 @@ public function has($callback)
6469

6570
$distantRelation = $callback($this->localRelationship->getRelated());
6671

67-
if ($distantRelation instanceof HasMany) {
72+
if ($distantRelation instanceof HasMany || $this->localRelationship instanceof HasMany) {
6873
$returnedRelation = $this->rootModel->hasManyThrough(
6974
$distantRelation->getRelated()::class,
7075
$this->localRelationship->getRelated()::class,

tests/Database/DatabaseEloquentRelationshipsTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,24 @@ public function testPendingHasThroughRelationship()
117117
$this->assertSame('fluent_projects.p_id', $fluent->getQualifiedLocalKeyName());
118118
$this->assertSame('environments.pro_id', $fluent->getQualifiedFirstKeyName());
119119
$this->assertSame('environments.pro_id', $classic->getQualifiedFirstKeyName());
120+
121+
$fluent = (new FluentProject())->environmentData();
122+
$classic = (new ClassicProject())->environmentData();
123+
124+
$this->assertInstanceOf(HasManyThrough::class, $classic);
125+
$this->assertInstanceOf(HasManyThrough::class, $fluent);
126+
$this->assertSame('p_id', $classic->getLocalKeyName());
127+
$this->assertSame('p_id', $fluent->getLocalKeyName());
128+
$this->assertSame('e_id', $classic->getSecondLocalKeyName());
129+
$this->assertSame('e_id', $fluent->getSecondLocalKeyName());
130+
$this->assertSame('pro_id', $classic->getFirstKeyName());
131+
$this->assertSame('pro_id', $fluent->getFirstKeyName());
132+
$this->assertSame('env_id', $classic->getForeignKeyName());
133+
$this->assertSame('env_id', $fluent->getForeignKeyName());
134+
$this->assertSame('classic_projects.p_id', $classic->getQualifiedLocalKeyName());
135+
$this->assertSame('fluent_projects.p_id', $fluent->getQualifiedLocalKeyName());
136+
$this->assertSame('environments.pro_id', $fluent->getQualifiedFirstKeyName());
137+
$this->assertSame('environments.pro_id', $classic->getQualifiedFirstKeyName());
120138
}
121139

122140
public function testStringyHasThroughApi()
@@ -473,6 +491,18 @@ public function deployments()
473491
'e_id',
474492
);
475493
}
494+
495+
public function environmentData()
496+
{
497+
return $this->hasManyThrough(
498+
Metadata::class,
499+
Environment::class,
500+
'pro_id',
501+
'env_id',
502+
'p_id',
503+
'e_id',
504+
);
505+
}
476506
}
477507

478508
class FluentProject extends MockedConnectionModel
@@ -482,6 +512,11 @@ public function deployments()
482512
return $this->through($this->environments())->has(fn (Environment $env) => $env->deployments());
483513
}
484514

515+
public function environmentData()
516+
{
517+
return $this->through($this->environments())->has(fn (Environment $env) => $env->metadata());
518+
}
519+
485520
public function environments()
486521
{
487522
return $this->hasMany(Environment::class, 'pro_id', 'p_id');
@@ -494,6 +529,16 @@ public function deployments()
494529
{
495530
return $this->hasMany(Deployment::class, 'env_id', 'e_id');
496531
}
532+
533+
public function metadata()
534+
{
535+
return $this->hasOne(MetaData::class, 'env_id', 'e_id');
536+
}
537+
}
538+
539+
class MetaData extends MockedConnectionModel
540+
{
541+
//
497542
}
498543

499544
class Deployment extends MockedConnectionModel

types/Database/Eloquent/Relations.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,12 @@ public function mechanic(): HasOne
169169
return $this->hasOne(Mechanic::class);
170170
}
171171

172+
/** @return HasMany<Mechanic, $this> */
173+
public function mechanics(): HasMany
174+
{
175+
return $this->hasMany(Mechanic::class);
176+
}
177+
172178
/** @return HasOneThrough<Car, Mechanic, $this> */
173179
public function car(): HasOneThrough
174180
{
@@ -187,7 +193,7 @@ public function car(): HasOneThrough
187193

188194
$through = $this->through($this->mechanic());
189195
assertType(
190-
'Illuminate\Database\Eloquent\PendingHasThroughRelationship<Illuminate\Types\Relations\Mechanic, $this(Illuminate\Types\Relations\User)>',
196+
'Illuminate\Database\Eloquent\PendingHasThroughRelationship<Illuminate\Types\Relations\Mechanic, $this(Illuminate\Types\Relations\User), Illuminate\Database\Eloquent\Relations\HasOne<Illuminate\Types\Relations\Mechanic, $this(Illuminate\Types\Relations\User)>>',
191197
$through,
192198
);
193199
assertType(
@@ -202,6 +208,27 @@ public function car(): HasOneThrough
202208
return $hasOneThrough;
203209
}
204210

211+
/** @return HasManyThrough<Car, Mechanic, $this> */
212+
public function cars(): HasManyThrough
213+
{
214+
$through = $this->through($this->mechanics());
215+
assertType(
216+
'Illuminate\Database\Eloquent\PendingHasThroughRelationship<Illuminate\Types\Relations\Mechanic, $this(Illuminate\Types\Relations\User), Illuminate\Database\Eloquent\Relations\HasMany<Illuminate\Types\Relations\Mechanic, $this(Illuminate\Types\Relations\User)>>',
217+
$through,
218+
);
219+
$hasManyThrough = $through->has(function ($mechanic) {
220+
assertType('Illuminate\Types\Relations\Mechanic', $mechanic);
221+
222+
return $mechanic->car();
223+
});
224+
assertType(
225+
'Illuminate\Database\Eloquent\Relations\HasManyThrough<Illuminate\Types\Relations\Car, Illuminate\Types\Relations\Mechanic, $this(Illuminate\Types\Relations\User)>',
226+
$hasManyThrough,
227+
);
228+
229+
return $hasManyThrough;
230+
}
231+
205232
/** @return HasManyThrough<Part, Mechanic, $this> */
206233
public function parts(): HasManyThrough
207234
{

0 commit comments

Comments
 (0)