Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.x] Support scopes as query methods #5927

Merged
merged 40 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
12cb726
Fix error when the scope a filter
aerni Apr 27, 2022
b42cfe9
Automatically apply the scopes to the builders
aerni Apr 27, 2022
396112b
Apply same magic to the tag scopes
aerni Apr 27, 2022
46ca391
Add method to manually register a scope
aerni Apr 27, 2022
73b257e
Move methods to the base builder
aerni Apr 27, 2022
c4696ca
Move logic to a trait
aerni Apr 27, 2022
9ff2ba9
Also apply the scopes when using a DB
aerni Apr 27, 2022
4e2f6f0
Use `get_class` instead of `::class`
aerni Apr 27, 2022
57aab9b
Also use `get_class` here
aerni Apr 27, 2022
5f0256e
Revert "Add method to manually register a scope"
aerni Apr 27, 2022
e9be34c
Fix typo
aerni Apr 27, 2022
f144d4d
Fix tests by filtering empty values
aerni Apr 27, 2022
f35527e
Add tests
aerni Apr 28, 2022
ae2167a
No need to set to an empty array
aerni Apr 28, 2022
b2d9387
Add tests
aerni Apr 28, 2022
014d06e
Fix styling
aerni May 13, 2022
d5b5570
Merge branch '3.3' of https://github.com/aerni/cms into feature/query…
aerni Jun 15, 2022
ce7bc84
Merge branch '4.x' into feature/query-scopes
jasonvarga Jul 6, 2023
002e765
Merge branch '4.x' into pr/5927
duncanmcclean Dec 5, 2023
c0a7ddf
Eloquent queries: check if scope can be applied
duncanmcclean Dec 5, 2023
04e330b
Merge branch '4.x' into pr/5927
duncanmcclean Dec 5, 2023
ae6a277
Eloquent: move response to under scope check
duncanmcclean Dec 5, 2023
a380ed9
Pass arguments to query scope
duncanmcclean Dec 5, 2023
06b94c6
add array typehint
duncanmcclean Dec 5, 2023
718271f
Add Parameters to applyScope typehint
duncanmcclean Dec 5, 2023
d2c1c17
update tests to ensure arguments are passed along
duncanmcclean Dec 5, 2023
5d3a1ef
Merge branch '4.x' into pr/5927
duncanmcclean Feb 13, 2024
5fba217
Pint
duncanmcclean Feb 13, 2024
9852d59
Merge branch '5.x' into pr/5927
duncanmcclean May 13, 2024
5bb88e5
🍺
duncanmcclean May 13, 2024
92baa6e
Replace `snake_case` helper
duncanmcclean May 14, 2024
3142d95
Replace another usage of `snake_case`
duncanmcclean May 14, 2024
4e35c27
Merge branch '5.x' into feature/query-scopes
jasonvarga Sep 30, 2024
9cefa55
rework things ...
jasonvarga Oct 2, 2024
8df7a29
revert unnecessary changes
jasonvarga Oct 2, 2024
c86430b
handled in other tests
jasonvarga Oct 2, 2024
c53e9f9
prevent scope being applied via statamic and also on underlying eloqu…
jasonvarga Oct 2, 2024
33748ab
statamic scopes require arrays as context
jasonvarga Oct 3, 2024
762a1ab
unused
jasonvarga Oct 3, 2024
6cc4c56
revert. this was fixed on main branch already.
jasonvarga Oct 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Assets/AssetRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
use Statamic\Facades\Site;
use Statamic\Facades\Stache;
use Statamic\Facades\URL;
use Statamic\Query\Scopes\AllowsScopes;
use Statamic\Support\Str;

class AssetRepository implements Contract
{
use AllowsScopes;

public function all()
{
return AssetCollection::make(AssetContainer::all()->flatMap(function ($container) {
Expand Down
3 changes: 2 additions & 1 deletion src/Auth/UserRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
use Statamic\Facades\Blink;
use Statamic\Facades\Blueprint;
use Statamic\OAuth\Provider;
use Statamic\Query\Scopes\AllowsScopes;
use Statamic\Statamic;

abstract class UserRepository implements RepositoryContract
{
use StoresComputedFieldCallbacks;
use AllowsScopes, StoresComputedFieldCallbacks;

public function create()
{
Expand Down
2 changes: 2 additions & 0 deletions src/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ public function register()
app()->bind('statamic.queries.'.$alias, $binding);
});

$this->app->instance('statamic.query-scopes', collect());

$this->app->bind('statamic.imaging.guzzle', function () {
return new \GuzzleHttp\Client;
});
Expand Down
17 changes: 16 additions & 1 deletion src/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Statamic\Query;

use BadMethodCallException;
use Closure;
use DateTimeInterface;
use Illuminate\Pagination\Paginator;
Expand All @@ -13,10 +14,11 @@
use Statamic\Extensions\Pagination\LengthAwarePaginator;
use Statamic\Facades\Pattern;
use Statamic\Query\Concerns\FakesQueries;
use Statamic\Query\Scopes\AppliesScopes;

abstract class Builder implements Contract
{
use FakesQueries;
use AppliesScopes, FakesQueries;

protected $columns;
protected $limit;
Expand All @@ -38,6 +40,19 @@ abstract class Builder implements Contract
'<=' => 'LessThanOrEqualTo',
];

public function __call($method, $args)
{
if ($this->canApplyScope($method)) {
$this->applyScope($method, $args[0] ?? []);

return $this;
}

throw new BadMethodCallException(sprintf(
'Call to undefined method %s::%s()', static::class, $method
));
}

public function select($columns = ['*'])
{
$this->columns = $columns;
Expand Down
9 changes: 9 additions & 0 deletions src/Query/EloquentQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
use Statamic\Contracts\Query\Builder;
use Statamic\Extensions\Pagination\LengthAwarePaginator;
use Statamic\Facades\Blink;
use Statamic\Query\Scopes\AppliesScopes;
use Statamic\Support\Arr;

abstract class EloquentQueryBuilder implements Builder
{
use AppliesScopes;

protected $builder;
protected $columns;

Expand All @@ -39,6 +42,12 @@ public function __construct(EloquentBuilder $builder)

public function __call($method, $args)
{
if ($this->canApplyScope($method)) {
$this->applyScope($method, $args[0] ?? []);

return $this;
}

$response = $this->builder->$method(...$args);

return $response instanceof EloquentBuilder ? $this : $response;
Expand Down
21 changes: 21 additions & 0 deletions src/Query/Scopes/AllowsScopes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Statamic\Query\Scopes;

use Statamic\Support\Str;

trait AllowsScopes
{
public function allowQueryScope(string $scope, ?string $method = null)
{
/** @var \Illuminate\Support\Collection $scopes */
$scopes = app('statamic.query-scopes');

$method ??= Str::camel($scope::handle());

$scopes->put(
$class = get_class($this->query()),
$scopes->get($class, collect())->put($method, $scope)
);
}
}
30 changes: 30 additions & 0 deletions src/Query/Scopes/AppliesScopes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Statamic\Query\Scopes;

use Statamic\Facades\Scope;
use Statamic\Tags\Parameters;

trait AppliesScopes
{
public function applyScope($scope, Parameters|array $context = [])
{
if (! $class = $this->getScopeClassFor($scope)) {
throw new \Exception("The [$scope] scope does not exist.");
}

Scope::find($class::handle())->apply($this, $context);
}

public function canApplyScope($scope): bool
{
return (bool) $this->getScopeClassFor($scope);
}

private function getScopeClassFor(string $method): ?string
{
return app('statamic.query-scopes')
->get(get_class($this), collect())
->get($method);
}
}
3 changes: 3 additions & 0 deletions src/Stache/Repositories/EntryRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@
use Statamic\Exceptions\EntryNotFoundException;
use Statamic\Facades\Blink;
use Statamic\Facades\Collection;
use Statamic\Query\Scopes\AllowsScopes;
use Statamic\Rules\Slug;
use Statamic\Stache\Query\EntryQueryBuilder;
use Statamic\Stache\Stache;
use Statamic\Support\Arr;

class EntryRepository implements RepositoryContract
{
use AllowsScopes;

protected $stache;
protected $store;
protected $substitutionsById = [];
Expand Down
3 changes: 3 additions & 0 deletions src/Stache/Repositories/TermRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Statamic\Facades\Collection;
use Statamic\Facades\Entry;
use Statamic\Facades\Taxonomy;
use Statamic\Query\Scopes\AllowsScopes;
use Statamic\Stache\Query\TermQueryBuilder;
use Statamic\Stache\Stache;
use Statamic\Support\Str;
Expand All @@ -17,6 +18,8 @@

class TermRepository implements RepositoryContract
{
use AllowsScopes;

protected $stache;
protected $store;
protected $substitutionsById = [];
Expand Down
110 changes: 110 additions & 0 deletions tests/Auth/Eloquent/EloquentUserQueryBuilderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

namespace Tests\Auth\Eloquent;

use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Carbon;
use PHPUnit\Framework\Attributes\Test;
use Statamic\Facades\User as Users;
use Statamic\Query\Scopes\Scope;
use Tests\TestCase;

class EloquentUserQueryBuilderTest extends TestCase
{
public static $migrationsGenerated = false;

public function setUp(): void
{
parent::setUp();

Carbon::setTestNow(Carbon::create(2019, 11, 21, 23, 39, 29));

config([
'statamic.users.repository' => 'eloquent',
'auth.providers.users' => [
'driver' => 'eloquent',
'model' => User::class,
],
]);

$this->loadMigrationsFrom(static::migrationsDir());

$tmpDir = static::migrationsDir().'/tmp';

if (! self::$migrationsGenerated) {
$this->artisan('statamic:auth:migration', ['--path' => $tmpDir]);

self::$migrationsGenerated = true;
}

$this->loadMigrationsFrom($tmpDir);
}

private static function migrationsDir()
{
return __DIR__.'/__migrations__';
}

public function tearDown(): void
{
Users::all()->each->delete();

parent::tearDown();
}

public static function tearDownAfterClass(): void
{
// Clean up the orphaned migration file.
(new Filesystem)->deleteDirectory(static::migrationsDir().'/tmp');

parent::tearDownAfterClass();
}

#[Test]
public function it_queries_by_scope()
{
NameScope::register();
Users::allowQueryScope(NameScope::class);
Users::allowQueryScope(NameScope::class, 'byName');

User::create(['name' => 'Jack', 'email' => '[email protected]']);
User::create(['name' => 'Jason', 'email' => '[email protected]']);
User::create(['name' => 'Bob Down', 'email' => '[email protected]']);
User::create(['name' => 'Bob Vance', 'email' => '[email protected]']);

// Scope defined in model ...
// Eloquent model scopes can pass arguments.
$this->assertEquals([
'Jack', 'Jason',
], Users::query()->domain('statamic.com')->get()->map->name->all());

// Statamic style scope ...
// Statamic scopes require arrays.
$this->assertEquals([
'Bob Down', 'Bob Vance',
], Users::query()->name(['name' => 'bob'])->get()->map->name->all());

// Statamic style scope with method ...
$this->assertEquals([
'Bob Down', 'Bob Vance',
], Users::query()->byName(['name' => 'bob'])->get()->map->name->all());

// Otherwise calling a non-existent method should throw appropriate exception ...
try {
Users::query()->something()->get();
$this->fail('Undefined method exception was not thrown.');
} catch (\BadMethodCallException $e) {
$this->assertEquals('Call to undefined method Illuminate\\Database\\Eloquent\\Builder::something()', $e->getMessage());
}
}
}

class NameScope extends Scope
{
protected static $handle = 'name';

public function apply($query, $values)
{
$query->where('name', 'like', '%'.$values['name'].'%');
}
}
5 changes: 5 additions & 0 deletions tests/Auth/Eloquent/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,9 @@ class User extends Authenticatable
protected $hidden = [
'password', 'remember_token',
];

public function scopeDomain($query, $domain)
{
return $query->where('email', 'like', '%@'.$domain);
}
}
20 changes: 20 additions & 0 deletions tests/Data/Assets/AssetQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Statamic\Facades\Asset;
use Statamic\Facades\AssetContainer;
use Statamic\Facades\Blueprint;
use Statamic\Query\Scopes\Scope;
use Tests\PreventSavingStacheItemsToDisk;
use Tests\TestCase;

Expand Down Expand Up @@ -525,6 +526,17 @@ public function it_can_get_assets_using_tap()
$this->assertEquals(['a'], $assets->map->filename()->all());
}

#[Test]
public function assets_are_found_using_scopes()
{
CustomScope::register();
Asset::allowQueryScope(CustomScope::class);
Asset::allowQueryScope(CustomScope::class, 'whereCustom');

$this->assertCount(1, $this->container->queryAssets()->customScope(['path' => 'a.jpg'])->get());
$this->assertCount(1, $this->container->queryAssets()->whereCustom(['path' => 'a.jpg'])->get());
}

#[Test]
public function assets_are_found_using_offset()
{
Expand Down Expand Up @@ -632,3 +644,11 @@ public function values_can_be_plucked()
], $this->container->queryAssets()->where('extension', 'jpg')->pluck('path')->all());
}
}

class CustomScope extends Scope
{
public function apply($query, $params)
{
$query->where('path', $params['path']);
}
}
23 changes: 23 additions & 0 deletions tests/Data/Entries/EntryQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Statamic\Facades\Blueprint;
use Statamic\Facades\Collection;
use Statamic\Facades\Entry;
use Statamic\Query\Scopes\Scope;
use Tests\PreventSavingStacheItemsToDisk;
use Tests\TestCase;

Expand Down Expand Up @@ -666,6 +667,20 @@ public function it_substitutes_entries_by_uri_and_site()
$this->assertSame($found, $substituteFr);
}

#[Test]
public function entries_are_found_using_scopes()
{
CustomScope::register();
Entry::allowQueryScope(CustomScope::class);
Entry::allowQueryScope(CustomScope::class, 'whereCustom');

EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1'])->create();
EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2'])->create();

$this->assertCount(1, Entry::query()->customScope(['title' => 'Post 1'])->get());
$this->assertCount(1, Entry::query()->whereCustom(['title' => 'Post 1'])->get());
}

#[Test]
public function entries_are_found_using_offset()
{
Expand Down Expand Up @@ -898,3 +913,11 @@ public function values_can_be_plucked()
], Entry::query()->where('type', 'b')->pluck('slug')->all());
}
}

class CustomScope extends Scope
{
public function apply($query, $params)
{
$query->where('title', $params['title']);
}
}
Loading
Loading