Skip to content

Commit 6713393

Browse files
shaedrichcrynobonetaylorotwell
authored
[12.x] Add NamedScope attribute (#54450)
* Add NamedScope attribute * StyleCI * Make Model::getAttributedNamedScope() protected * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * fix(database): ✏ Fix typo * refactor(database): 🚚 Rename attribute class to comply with PSR-4 * refactor(database): ✅ Simplify test * refactor(database): ♻️ Simplify named local scope attribute check logic * fix(database): 🐛 Make attributed named scope statically callable * style(database): 🚨 StyleCI * style(database): 🚨 StyleCI * formatting * add files * fix tests --------- Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> Co-authored-by: Mior Muhammad Zaki <crynobone@gmail.com> Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent 8f0e00b commit 6713393

File tree

5 files changed

+135
-3
lines changed

5 files changed

+135
-3
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_METHOD)]
8+
class Scope
9+
{
10+
/**
11+
* Create a new attribute instance.
12+
*
13+
* @param array|string $classes
14+
* @return void
15+
*/
16+
public function __construct()
17+
{
18+
}
19+
}

src/Illuminate/Database/Eloquent/Model.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Illuminate\Contracts\Support\CanBeEscapedWhenCastToString;
1212
use Illuminate\Contracts\Support\Jsonable;
1313
use Illuminate\Database\ConnectionResolverInterface as Resolver;
14+
use Illuminate\Database\Eloquent\Attributes\Scope as LocalScope;
1415
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
1516
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
1617
use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot;
@@ -24,6 +25,7 @@
2425
use JsonException;
2526
use JsonSerializable;
2627
use LogicException;
28+
use ReflectionMethod;
2729
use Stringable;
2830

2931
abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToString, HasBroadcastChannel, Jsonable, JsonSerializable, QueueableEntity, Stringable, UrlRoutable
@@ -1639,7 +1641,8 @@ public function newPivot(self $parent, array $attributes, $table, $exists, $usin
16391641
*/
16401642
public function hasNamedScope($scope)
16411643
{
1642-
return method_exists($this, 'scope'.ucfirst($scope));
1644+
return method_exists($this, 'scope'.ucfirst($scope)) ||
1645+
static::isScopeMethodWithAttribute($scope);
16431646
}
16441647

16451648
/**
@@ -1651,9 +1654,26 @@ public function hasNamedScope($scope)
16511654
*/
16521655
public function callNamedScope($scope, array $parameters = [])
16531656
{
1657+
if ($this->isScopeMethodWithAttribute($scope)) {
1658+
return $this->{$scope}(...$parameters);
1659+
}
1660+
16541661
return $this->{'scope'.ucfirst($scope)}(...$parameters);
16551662
}
16561663

1664+
/**
1665+
* Determine if the given method has a scope attribute.
1666+
*
1667+
* @param string $method
1668+
* @return bool
1669+
*/
1670+
protected static function isScopeMethodWithAttribute(string $method)
1671+
{
1672+
return method_exists(static::class, $method) &&
1673+
(new ReflectionMethod(static::class, $method))
1674+
->getAttributes(LocalScope::class) !== [];
1675+
}
1676+
16571677
/**
16581678
* Convert the model instance to an array.
16591679
*
@@ -2381,6 +2401,10 @@ public function __call($method, $parameters)
23812401
*/
23822402
public static function __callStatic($method, $parameters)
23832403
{
2404+
if (static::isScopeMethodWithAttribute($method)) {
2405+
$parameters = [static::query(), ...$parameters];
2406+
}
2407+
23842408
return (new static)->$method(...$parameters);
23852409
}
23862410

tests/Integration/Database/EloquentModelScopeTest.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Illuminate\Tests\Integration\Database;
44

5+
use Illuminate\Contracts\Database\Eloquent\Builder;
6+
use Illuminate\Database\Eloquent\Attributes\Scope as NamedScope;
57
use Illuminate\Database\Eloquent\Model;
68

79
class EloquentModelScopeTest extends DatabaseTestCase
@@ -19,12 +21,25 @@ public function testModelDoesNotHaveScope()
1921

2022
$this->assertFalse($model->hasNamedScope('doesNotExist'));
2123
}
24+
25+
public function testModelHasAttributedScope()
26+
{
27+
$model = new TestScopeModel1;
28+
29+
$this->assertTrue($model->hasNamedScope('existsAsWell'));
30+
}
2231
}
2332

2433
class TestScopeModel1 extends Model
2534
{
26-
public function scopeExists()
35+
public function scopeExists(Builder $builder)
36+
{
37+
return $builder;
38+
}
39+
40+
#[NamedScope]
41+
protected function existsAsWell(Builder $builder)
2742
{
28-
return true;
43+
return $builder;
2944
}
3045
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Integration\Database;
4+
5+
use Orchestra\Testbench\Attributes\WithMigration;
6+
use Orchestra\Testbench\TestCase;
7+
8+
#[WithMigration]
9+
class EloquentNamedScopeAttibuteTest extends TestCase
10+
{
11+
protected $query = 'select * from "named_scope_users" where "email_verified_at" is not null';
12+
13+
protected function setUp(): void
14+
{
15+
parent::setUp();
16+
17+
$this->markTestSkippedUnless(
18+
$this->usesSqliteInMemoryDatabaseConnection(),
19+
'Requires in-memory database connection',
20+
);
21+
}
22+
23+
public function test_it_can_query_named_scoped_from_the_query_builder()
24+
{
25+
$query = Fixtures\NamedScopeUser::query()->verified(true);
26+
27+
$this->assertSame($this->query, $query->toRawSql());
28+
}
29+
30+
public function test_it_can_query_named_scoped_from_static_query()
31+
{
32+
$query = Fixtures\NamedScopeUser::verified(true);
33+
34+
$this->assertSame($this->query, $query->toRawSql());
35+
}
36+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Integration\Database\Fixtures;
4+
5+
use Illuminate\Database\Eloquent\Attributes\Scope as NamedScope;
6+
use Illuminate\Database\Eloquent\Builder;
7+
8+
class NamedScopeUser extends User
9+
{
10+
/** {@inheritdoc} */
11+
#[\Override]
12+
protected function casts(): array
13+
{
14+
return [
15+
'email_verified_at' => 'datetime',
16+
'password' => 'hashed',
17+
];
18+
}
19+
20+
#[NamedScope]
21+
protected function verified(Builder $builder, bool $email = true)
22+
{
23+
return $builder->when(
24+
$email === true,
25+
fn ($query) => $query->whereNotNull('email_verified_at'),
26+
fn ($query) => $query->whereNull('email_verified_at'),
27+
);
28+
}
29+
30+
public function scopeVerifiedUser(Builder $builder, bool $email = true)
31+
{
32+
return $builder->when(
33+
$email === true,
34+
fn ($query) => $query->whereNotNull('email_verified_at'),
35+
fn ($query) => $query->whereNull('email_verified_at'),
36+
);
37+
}
38+
}

0 commit comments

Comments
 (0)