Skip to content

Commit 99b9d0d

Browse files
GautierDeleStyleCIBotcoderabbitai[bot]
authored
✨ scout queries (#11)
* ✨ scout queries * Apply fixes from StyleCI * 📝 Add docstrings to `feature/scout-queries` (#12) * 📝 Add docstrings to `feature/scout-queries` Docstrings generation was requested by @GautierDele. * #11 (comment) The following files were modified: * `src/AccessServiceProvider.php` * `src/Controls/Control.php` * `src/Perimeters/Perimeter.php` * `tests/Support/Access/Controls/ModelControl.php` * `tests/Support/Models/Model.php` * Apply fixes from StyleCI * ♻️ removed unecessary comments * Apply fixes from StyleCI --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: StyleCI Bot <[email protected]> Co-authored-by: Gautier DELEGLISE <[email protected]> --------- Co-authored-by: StyleCI Bot <[email protected]> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 64303f7 commit 99b9d0d

File tree

7 files changed

+247
-23
lines changed

7 files changed

+247
-23
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
},
1717
"require-dev": {
1818
"guzzlehttp/guzzle": "^6.0|^7.0",
19+
"laravel/scout": "^10",
1920
"orchestra/testbench": "^9|^10",
2021
"phpunit/phpunit": "^11.0"
2122
},

src/AccessServiceProvider.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
namespace Lomkit\Access;
44

55
use Illuminate\Foundation\Events\PublishingStubs;
6+
use Illuminate\Support\Facades\Auth;
67
use Illuminate\Support\Facades\Event;
78
use Illuminate\Support\ServiceProvider;
9+
use Laravel\Scout\Builder;
810
use Lomkit\Access\Console\ControlMakeCommand;
911
use Lomkit\Access\Console\PerimeterMakeCommand;
1012

@@ -32,14 +34,28 @@ public function register()
3234

3335
/**
3436
* Bootstrap any package services.
35-
*
36-
* @return void
3737
*/
3838
public function boot()
3939
{
4040
$this->registerPublishing();
4141

4242
$this->registerStubs();
43+
44+
$this->bootScoutBuilder();
45+
}
46+
47+
/**
48+
* Registers a macro on Laravel Scout's Builder.
49+
*/
50+
protected function bootScoutBuilder(): void
51+
{
52+
if (class_exists(Builder::class)) {
53+
Builder::macro('controlled', function (Builder $builder) {
54+
$control = $builder->model->newControl();
55+
56+
return $control->queried($builder, Auth::user());
57+
});
58+
}
4359
}
4460

4561
/**

src/Controls/Control.php

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
class Control
1414
{
15-
// @TODO: scout queried
15+
// @TODO: change readme image
1616
/**
1717
* The control name resolver.
1818
*
@@ -68,12 +68,12 @@ public function applies(Model $user, string $method, Model $model): bool
6868
}
6969

7070
/**
71-
* Modifies the query builder to enforce access control restrictions for a given user.
71+
* Applies access control restrictions to an Eloquent query builder for the specified user.
7272
*
73-
* @param Builder $query The query builder instance to modify.
74-
* @param Model $user The user model used to determine applicable query control restrictions.
73+
* @param Builder $query The Eloquent query builder to modify.
74+
* @param Model $user The user for whom access control is enforced.
7575
*
76-
* @return Builder The modified query builder with access controls applied.
76+
* @return Builder The query builder with access control restrictions applied.
7777
*/
7878
public function queried(Builder $query, Model $user): Builder
7979
{
@@ -91,12 +91,25 @@ public function queried(Builder $query, Model $user): Builder
9191
}
9292

9393
/**
94-
* Applies query modifications based on access control perimeters for the given user.
94+
* Applies access control restrictions to a Laravel Scout query builder for the specified user.
9595
*
96-
* @param Builder $query The query builder instance to be modified.
97-
* @param Model $user The user model used to evaluate access control conditions.
96+
* @param \Laravel\Scout\Builder $query The Scout query builder to modify.
97+
* @param Model $user The user for whom access control is enforced.
9898
*
99-
* @return Builder The query builder after applying access control modifications.
99+
* @return \Laravel\Scout\Builder The query builder with access controls applied.
100+
*/
101+
public function scoutQueried(\Laravel\Scout\Builder $query, Model $user): \Laravel\Scout\Builder
102+
{
103+
return $this->applyScoutQueryControl($query, $user);
104+
}
105+
106+
/**
107+
* Modifies an Eloquent query builder to enforce access control rules for the specified user.
108+
*
109+
* @param Builder $query The Eloquent query builder to modify.
110+
* @param Model $user The user for whom access control is evaluated.
111+
*
112+
* @return Builder The modified query builder reflecting access control restrictions.
100113
*/
101114
protected function applyQueryControl(Builder $query, Model $user): Builder
102115
{
@@ -120,17 +133,58 @@ protected function applyQueryControl(Builder $query, Model $user): Builder
120133
}
121134

122135
/**
123-
* Modifies the query builder to return no results.
136+
* Applies access control modifications to a Laravel Scout query builder based on defined perimeters.
124137
*
125-
* @param Builder $query The query builder instance to modify.
138+
* @param \Laravel\Scout\Builder $query The Scout query builder to modify.
139+
* @param Model $user The user for whom access control is being enforced.
126140
*
127-
* @return Builder The modified query builder that yields an empty result set.
141+
* @return \Laravel\Scout\Builder The modified Scout query builder reflecting access control restrictions.
142+
*/
143+
protected function applyScoutQueryControl(\Laravel\Scout\Builder $query, Model $user): \Laravel\Scout\Builder
144+
{
145+
$noResultCallback = function (\Laravel\Scout\Builder $query) {
146+
return $this->noResultScoutQuery($query);
147+
};
148+
149+
foreach ($this->perimeters() as $perimeter) {
150+
if ($perimeter->applyAllowedCallback($user)) {
151+
$query = $perimeter->applyScoutQueryCallback($query, $user);
152+
153+
$noResultCallback = function ($query) {return $query; };
154+
155+
if (!$perimeter->overlays()) {
156+
return $query;
157+
}
158+
}
159+
}
160+
161+
return $noResultCallback($query);
162+
}
163+
164+
/**
165+
* Alters the Eloquent query builder to ensure no records are returned.
166+
*
167+
* @param Builder $query The Eloquent query builder to modify.
168+
*
169+
* @return Builder The query builder configured to yield no results.
128170
*/
129171
protected function noResultQuery(Builder $query): Builder
130172
{
131173
return $query->whereRaw('0=1');
132174
}
133175

176+
/**
177+
* Modifies the Scout query builder to ensure no records are returned.
178+
*
179+
* @param \Laravel\Scout\Builder $query The Scout query builder to modify.
180+
*
181+
* @return \Laravel\Scout\Builder The modified query builder that yields no results.
182+
*/
183+
protected function noResultScoutQuery(\Laravel\Scout\Builder $query): \Laravel\Scout\Builder
184+
{
185+
return $query->where('__NOT_A_VALID_FIELD__', 0);
186+
}
187+
134188
/**
135189
* Specify the callback that should be invoked to guess control names.
136190
*

src/Perimeters/Perimeter.php

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,51 @@
88

99
class Perimeter
1010
{
11+
protected Closure $scoutQueryCallback;
12+
1113
protected Closure $queryCallback;
1214
protected Closure $shouldCallback;
1315
protected Closure $allowedCallback;
1416

17+
/**
18+
* Initializes the Perimeter with default callbacks for access control and query customization.
19+
*/
1520
public function __construct()
1621
{
1722
// Default implementations that can be overridden
23+
$this->scoutQueryCallback = function (\Laravel\Scout\Builder $query, Model $user) { return $query; };
1824
$this->queryCallback = function (Builder $query, Model $user) { return $query; };
1925
$this->shouldCallback = function (Model $user, string $method, Model $model) { return true; };
2026
$this->allowedCallback = function (Model $user) { return true; };
2127
}
2228

2329
/**
24-
* Executes the should callback to determine if the access control condition is met.
30+
* Determines if the access control condition should be applied for the given user, method, and model.
2531
*
26-
* @param Model $user The user instance for which the check is performed.
27-
* @param string $method The access control method or action being evaluated.
28-
* @param Model $model The model instance related to the access check.
32+
* @param Model $user The user being evaluated.
33+
* @param string $method The access control action or method.
34+
* @param Model $model The related model instance.
2935
*
30-
* @return bool True if the callback validation passes; otherwise, false.
36+
* @return bool True if the condition applies; false otherwise.
3137
*/
3238
public function applyShouldCallback(Model $user, string $method, Model $model): bool
3339
{
3440
return ($this->shouldCallback)($user, $method, $model);
3541
}
3642

43+
/**
44+
* Applies the configured Scout query callback to modify a Laravel Scout search query for a given user.
45+
*
46+
* @param \Laravel\Scout\Builder $query The Scout query builder to modify.
47+
* @param Model $user The user model for whom the query is being modified.
48+
*
49+
* @return \Laravel\Scout\Builder The modified Scout query builder.
50+
*/
51+
public function applyScoutQueryCallback(\Laravel\Scout\Builder $query, Model $user): \Laravel\Scout\Builder
52+
{
53+
return ($this->scoutQueryCallback)($query, $user);
54+
}
55+
3756
/**
3857
* Applies the registered query callback to modify the query builder based on the user's context.
3958
*
@@ -88,11 +107,11 @@ public function should(Closure $shouldCallback): self
88107
}
89108

90109
/**
91-
* Sets the query modification callback.
110+
* Sets a custom callback to modify Eloquent query builders for access control.
92111
*
93-
* @param Closure $queryCallback A callback that customizes the query logic.
112+
* @param Closure $queryCallback Callback that receives and returns a query builder.
94113
*
95-
* @return self Returns the current instance for method chaining.
114+
* @return self The current Perimeter instance.
96115
*/
97116
public function query(Closure $queryCallback): self
98117
{
@@ -101,6 +120,20 @@ public function query(Closure $queryCallback): self
101120
return $this;
102121
}
103122

123+
/**
124+
* Sets the callback used to modify Laravel Scout search queries for this perimeter.
125+
*
126+
* @param Closure $scoutQueryCallback Callback that receives a Scout query builder and user model, and returns a modified query builder.
127+
*
128+
* @return self
129+
*/
130+
public function scoutQuery(Closure $scoutQueryCallback): self
131+
{
132+
$this->scoutQueryCallback = $scoutQueryCallback;
133+
134+
return $this;
135+
}
136+
104137
/**
105138
* Creates and returns a new instance of the Perimeter class.
106139
*
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
use Illuminate\Support\Facades\Auth;
4+
use Lomkit\Access\Tests\Support\Models\Model;
5+
6+
class ControlsScoutQueryTest extends \Lomkit\Access\Tests\Feature\TestCase
7+
{
8+
public function test_control_scout_query_with_no_perimeter_passing(): void
9+
{
10+
Model::factory()
11+
->count(50)
12+
->create();
13+
14+
$query = Model::search();
15+
$query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->scoutQueried($query, Auth::user());
16+
17+
$this->assertEquals(['__NOT_A_VALID_FIELD__' => 0], $query->wheres);
18+
}
19+
20+
public function test_control_scout_queried_using_client_perimeter(): void
21+
{
22+
Auth::user()->update(['should_client' => true]);
23+
24+
Model::factory()
25+
->count(50)
26+
->create();
27+
Model::factory()
28+
->state(['is_client' => true])
29+
->count(50)
30+
->create();
31+
32+
$query = Model::search();
33+
$query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->scoutQueried($query, Auth::user());
34+
35+
$this->assertEquals(['is_client' => true], $query->wheres);
36+
}
37+
38+
public function test_control_scout_queried_using_shared_overlayed_perimeter(): void
39+
{
40+
Auth::user()->update(['should_shared' => true]);
41+
Auth::user()->update(['should_client' => true]);
42+
43+
Model::factory()
44+
->count(50)
45+
->create();
46+
Model::factory()
47+
->state(['is_shared' => true])
48+
->count(50)
49+
->create();
50+
Model::factory()
51+
->state(['is_client' => true])
52+
->count(50)
53+
->create();
54+
55+
$query = Model::search();
56+
$query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->scoutQueried($query, Auth::user());
57+
58+
$this->assertEquals(['is_shared' => true, 'is_client' => true], $query->wheres);
59+
}
60+
61+
public function test_control_scout_queried_using_shared_overlayed_perimeter_with_distant_perimeter(): void
62+
{
63+
Auth::user()->update(['should_shared' => true]);
64+
Auth::user()->update(['should_own' => true]);
65+
66+
Model::factory()
67+
->state(['is_client' => true])
68+
->count(50)
69+
->create();
70+
Model::factory()
71+
->state(['is_shared' => true])
72+
->count(50)
73+
->create();
74+
Model::factory()
75+
->state(['is_own' => true])
76+
->count(50)
77+
->create();
78+
79+
$query = Model::search();
80+
$query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->scoutQueried($query, Auth::user());
81+
82+
$this->assertEquals(['is_shared' => true, 'is_own' => true], $query->wheres);
83+
}
84+
85+
public function test_control_scout_queried_using_only_shared_overlayed_perimeter(): void
86+
{
87+
Auth::user()->update(['should_shared' => true]);
88+
89+
Model::factory()
90+
->state(['is_client' => true])
91+
->count(50)
92+
->create();
93+
Model::factory()
94+
->state(['is_shared' => true])
95+
->count(50)
96+
->create();
97+
Model::factory()
98+
->state(['is_own' => true])
99+
->count(50)
100+
->create();
101+
102+
$query = Model::search();
103+
$query = (new \Lomkit\Access\Tests\Support\Access\Controls\ModelControl())->scoutQueried($query, Auth::user());
104+
105+
$this->assertEquals(['is_shared' => true], $query->wheres);
106+
}
107+
}

0 commit comments

Comments
 (0)