Skip to content

Commit 04e2e19

Browse files
alsterholmdevfreytaylorotwell
authored
[11.x] Add CollectedBy attribute (#53122)
* Add CollectedBy attribute * Fix styling * Fix null checks * Fix styling * Fix type checks * Wrap collection class name resolution in once * Fix docblock return type Co-authored-by: Jeffrey Angenent <[email protected]> * Use static variable instead of once function to memoize * formatting --------- Co-authored-by: Jeffrey Angenent <[email protected]> Co-authored-by: Taylor Otwell <[email protected]>
1 parent ddfbbf0 commit 04e2e19

File tree

5 files changed

+88
-2
lines changed

5 files changed

+88
-2
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Eloquent\Attributes;
4+
5+
use Attribute;
6+
7+
#[Attribute(Attribute::TARGET_CLASS)]
8+
class CollectedBy
9+
{
10+
/**
11+
* Create a new attribute instance.
12+
*
13+
* @param class-string<\Illuminate\Database\Eloquent\Collection<*, *>> $collectionClass
14+
* @return void
15+
*/
16+
public function __construct(public string $collectionClass)
17+
{
18+
}
19+
}

src/Illuminate/Database/Eloquent/HasCollection.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@
22

33
namespace Illuminate\Database\Eloquent;
44

5+
use Illuminate\Database\Eloquent\Attributes\CollectedBy;
6+
use ReflectionClass;
7+
58
/**
69
* @template TCollection of \Illuminate\Database\Eloquent\Collection
710
*/
811
trait HasCollection
912
{
13+
/**
14+
* The resolved collection class names by model.
15+
*
16+
* @var array<class-string<static>, class-string<TCollection>>
17+
*/
18+
protected static array $resolvedCollectionClasses = [];
19+
1020
/**
1121
* Create a new Eloquent Collection instance.
1222
*
@@ -15,6 +25,26 @@ trait HasCollection
1525
*/
1626
public function newCollection(array $models = [])
1727
{
18-
return new static::$collectionClass($models);
28+
static::$resolvedCollectionClasses[static::class] ??= ($this->resolveCollectionFromAttribute() ?? static::$collectionClass);
29+
30+
return new static::$resolvedCollectionClasses[static::class]($models);
31+
}
32+
33+
/**
34+
* Resolve the collection class name from the CollectedBy attribute.
35+
*
36+
* @return class-string<TCollection>|null
37+
*/
38+
public function resolveCollectionFromAttribute()
39+
{
40+
$reflectionClass = new ReflectionClass(static::class);
41+
42+
$attributes = $reflectionClass->getAttributes(CollectedBy::class);
43+
44+
if (! isset($attributes[0]) || ! isset($attributes[0]->getArguments()[0])) {
45+
return;
46+
}
47+
48+
return $attributes[0]->getArguments()[0];
1949
}
2050
}

tests/Database/DatabaseEloquentBelongsToManyWithCastedAttributesTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ protected function getRelation()
6262
$builder = m::mock(Builder::class);
6363
$related = m::mock(Model::class);
6464
$related->shouldReceive('newCollection')->passthru();
65+
$related->shouldReceive('resolveCollectionFromAttribute')->passthru();
6566
$builder->shouldReceive('getModel')->andReturn($related);
6667
$related->shouldReceive('qualifyColumn');
6768
$builder->shouldReceive('join', 'where');

tests/Database/DatabaseEloquentModelTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Illuminate\Database\Connection;
1616
use Illuminate\Database\ConnectionResolverInterface;
1717
use Illuminate\Database\ConnectionResolverInterface as Resolver;
18+
use Illuminate\Database\Eloquent\Attributes\CollectedBy;
1819
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
1920
use Illuminate\Database\Eloquent\Builder;
2021
use Illuminate\Database\Eloquent\Casts\ArrayObject;
@@ -3169,6 +3170,14 @@ public function testGuardedWithMutators()
31693170
$this->assertSame('123 Main Street', $model->address_line_one);
31703171
$this->assertSame('Anytown', $model->address_line_two);
31713172
}
3173+
3174+
public function testCollectedByAttribute()
3175+
{
3176+
$model = new EloquentModelWithCollectedByAttribute;
3177+
$collection = $model->newCollection([$model]);
3178+
3179+
$this->assertInstanceOf(CustomEloquentCollection::class, $collection);
3180+
}
31723181
}
31733182

31743183
class EloquentTestObserverStub
@@ -3928,3 +3937,12 @@ public function setFullAddressAttribute($fullAddress)
39283937
$this->attributes['address_line_two'] = $addressLineTwo;
39293938
}
39303939
}
3940+
3941+
#[CollectedBy(CustomEloquentCollection::class)]
3942+
class EloquentModelWithCollectedByAttribute extends Model
3943+
{
3944+
}
3945+
3946+
class CustomEloquentCollection extends Collection
3947+
{
3948+
}

types/Database/Eloquent/Model.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
namespace Illuminate\Types\Model;
44

5+
use Illuminate\Database\Eloquent\Attributes\CollectedBy;
56
use Illuminate\Database\Eloquent\Collection;
67
use Illuminate\Database\Eloquent\HasCollection;
78
use Illuminate\Database\Eloquent\Model;
89
use User;
910

1011
use function PHPStan\Testing\assertType;
1112

12-
function test(User $user, Post $post, Comment $comment): void
13+
function test(User $user, Post $post, Comment $comment, Article $article): void
1314
{
1415
assertType('UserFactory', User::factory(function ($attributes, $model) {
1516
assertType('array<string, mixed>', $attributes);
@@ -34,6 +35,7 @@ function test(User $user, Post $post, Comment $comment): void
3435

3536
assertType('Illuminate\Database\Eloquent\Collection<(int|string), User>', $user->newCollection([new User()]));
3637
assertType('Illuminate\Types\Model\Posts<(int|string), Illuminate\Types\Model\Post>', $post->newCollection(['foo' => new Post()]));
38+
assertType('Illuminate\Types\Model\Articles<(int|string), Illuminate\Types\Model\Article>', $article->newCollection([new Article()]));
3739
assertType('Illuminate\Types\Model\Comments', $comment->newCollection([new Comment()]));
3840

3941
assertType('bool', $user->restore());
@@ -74,3 +76,19 @@ public function newCollection(array $models = []): Comments
7476
final class Comments extends Collection
7577
{
7678
}
79+
80+
#[CollectedBy(Articles::class)]
81+
class Article extends Model
82+
{
83+
/** @use HasCollection<Articles<array-key, static>> */
84+
use HasCollection;
85+
}
86+
87+
/**
88+
* @template TKey of array-key
89+
* @template TModel of Article
90+
*
91+
* @extends Collection<TKey, TModel> */
92+
class Articles extends Collection
93+
{
94+
}

0 commit comments

Comments
 (0)