Skip to content

Commit d6141f9

Browse files
authored
Merge pull request #152 from Lomkit/feature/laravel-12
⬆️ Laravel 12 compatibility
2 parents eb13f44 + 1b265df commit d6141f9

File tree

12 files changed

+247
-42
lines changed

12 files changed

+247
-42
lines changed

.github/workflows/tests.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,9 @@ jobs:
1414
strategy:
1515
fail-fast: false
1616
matrix:
17-
php-version: [ '8.1', '8.2', '8.3' ]
18-
laravel-version: [ '^10.0', '^11.0' ]
17+
php-version: [ '8.2', '8.3', '8.4' ]
18+
laravel-version: [ '^11.0', '^12.0' ]
1919
database: [ 'sqlite', 'mysql', 'pgsql' ]
20-
exclude:
21-
- php-version: '8.1'
22-
laravel-version: '^11.0'
2320

2421
name: Tests on PHP ${{ matrix.php-version }} with Laravel ${{ matrix.laravel-version }} and ${{ matrix.database }}
2522

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Laravel Rest Api is an elegant way to expose your app through an API, it takes f
66

77
## Requirements
88

9-
PHP 8.1+ and Laravel 10.0+
9+
PHP 8.2+ and Laravel 11+
1010

1111
## Documentation, Installation, and Usage Instructions
1212

composer.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
}
1111
],
1212
"require": {
13-
"php": "^8.0",
13+
"php": "^8.2",
1414
"ext-json": "*",
15-
"laravel/framework": "^10.0|^11.0"
15+
"laravel/framework": "^11|^12"
1616
},
1717
"require-dev": {
1818
"guzzlehttp/guzzle": "^6.0|^7.0",
19-
"orchestra/testbench": "^8.5|^9.0",
20-
"phpunit/phpunit": "^8.0|^9.0|^10.0|^11.0",
21-
"laravel/scout": "^10.0|^11.0"
19+
"orchestra/testbench": "^9|^10",
20+
"phpunit/phpunit": "^10|^11",
21+
"laravel/scout": "^10"
2222
},
2323
"autoload": {
2424
"psr-4": {

src/Actions/DispatchAction.php

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Lomkit\Rest\Actions;
44

55
use Illuminate\Contracts\Queue\ShouldQueue;
6+
use Illuminate\Database\Eloquent\Builder;
67
use Illuminate\Support\Collection;
78
use Illuminate\Support\Facades\Bus;
89
use Illuminate\Support\Facades\DB;
@@ -109,32 +110,50 @@ public function dispatch($chunkCount)
109110
}
110111

111112
/**
112-
* Dispatch the given action.
113+
* Processes models in chunks using classic mode and dispatches an action for each set.
113114
*
114-
* @param int $chunkCount
115+
* The method builds a search query for the resource associated with the current request, applying
116+
* search criteria from the request input and removing any default result limits. It then processes
117+
* the query results in chunks (of size $chunkCount) by invoking the forModels method on each chunk.
118+
* Finally, it returns the query limit if one is set; otherwise, it returns the total count of models.
115119
*
116-
* @return int
120+
* @param int $chunkCount The number of models to process per chunk.
121+
*
122+
* @return int The effective result limit if set, or the total count of models.
117123
*/
118124
public function handleClassic(int $chunkCount)
119125
{
126+
/**
127+
* @var Builder $searchQuery
128+
*/
120129
$searchQuery =
121130
app()->make(QueryBuilder::class, ['resource' => $this->request->resource, 'query' => null])
131+
->disableDefaultLimit()
122132
->search($this->request->input('search', []));
123133

134+
$limit = $searchQuery->toBase()->limit;
135+
124136
$searchQuery
125137
->clone()
126138
->chunk(
127139
$chunkCount,
128-
function ($chunk) {
129-
return $this->forModels(
130-
\Illuminate\Database\Eloquent\Collection::make(
131-
$chunk
132-
)
133-
);
140+
function ($chunk, $page) use ($limit, $chunkCount) {
141+
$collection = \Illuminate\Database\Eloquent\Collection::make($chunk);
142+
143+
// This is to remove for Laravel 12, chunking with limit does not work
144+
// in Laravel 11
145+
if (!is_null($limit) && $page * $chunkCount >= $limit) {
146+
$collection = $collection->take($limit - ($page - 1) * $chunkCount);
147+
$this->forModels($collection);
148+
149+
return false;
150+
}
151+
152+
return $this->forModels($collection);
134153
}
135154
);
136155

137-
return $searchQuery->count();
156+
return $limit ?? $searchQuery->count();
138157
}
139158

140159
/**
@@ -202,7 +221,7 @@ protected function dispatchSynchronouslyForCollection(Collection $models)
202221
*
203222
* @return $this
204223
*/
205-
protected function addQueuedActionJob(Collection $models)
224+
protected function addQueuedActionJob(Collection $models): self
206225
{
207226
$job = new CallRestApiAction($this->action, $this->fields, $models);
208227

src/Http/Response.php

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ protected function buildGatesForModel(Model $model, Resource $resource, array $g
4141
);
4242
}
4343

44+
/**
45+
* Convert an Eloquent model into an array representation for the HTTP response.
46+
*
47+
* This method transforms the given model by selecting only the specified attributes and aggregates as defined in the request parameters or resource. If authorization gating is enabled and gate parameters are provided, it appends the corresponding authorization data. Additionally, it recursively processes any loaded relations—returning pivot data when applicable and mapping related models (or collections of models) using the resource’s configuration.
48+
*
49+
* @param Model $model The Eloquent model instance to be converted.
50+
* @param resource $resource The resource defining the fields and structure of the response.
51+
* @param array $requestArray Request parameters that control field selection, aggregates, and authorization gates.
52+
* @param Relation|null $relation Optional relation context for processing nested relationships.
53+
*
54+
* @return array The structured array representation of the model, including attributes and recursively processed relations.
55+
*/
4456
public function modelToResponse(Model $model, Resource $resource, array $requestArray, Relation $relation = null)
4557
{
4658
$currentRequestArray = $relation === null ? $requestArray : collect($requestArray['includes'] ?? [])
@@ -72,7 +84,7 @@ public function modelToResponse(Model $model, Resource $resource, array $request
7284
})
7385
->toArray(),
7486
collect($model->getRelations())
75-
->mapWithKeys(function ($modelRelation, $relationName) use ($requestArray, $currentRequestArray, $relation, $resource) {
87+
->mapWithKeys(function ($modelRelation, $relationName) use ($requestArray, $relation, $resource) {
7688
$key = Str::snake($relationName);
7789

7890
if (is_null($modelRelation)) {
@@ -89,15 +101,8 @@ public function modelToResponse(Model $model, Resource $resource, array $request
89101

90102
$relationConcrete = $resource->relation($relationName);
91103
$relationResource = $relationConcrete->resource();
92-
$requestArrayRelation = collect($currentRequestArray['includes'] ?? [])
93-
->first(function ($include) use ($relationName) {
94-
return preg_match('/(?:\.\b)?'.$relationName.'\b/', $include['relation']);
95-
});
96-
97-
// We reapply the limits in case of BelongsToManyRelation where we can't apply limits easily
98-
if ($modelRelation instanceof Collection) {
99-
$modelRelation = $modelRelation->take($requestArrayRelation['limit'] ?? 50);
100-
} elseif ($modelRelation instanceof Model) {
104+
105+
if ($modelRelation instanceof Model) {
101106
return [
102107
$key => $this->modelToResponse(
103108
$modelRelation,

src/Query/Builder.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ public function __construct(Resource $resource, \Illuminate\Database\Eloquent\Bu
4444
*/
4545
protected bool $disableSecurity = false;
4646

47+
/**
48+
* Determine if default limit should be applied.
49+
*
50+
* @var bool
51+
*/
52+
protected bool $disableDefaultLimit = false;
53+
4754
/**
4855
* The query builder instance.
4956
*
@@ -56,13 +63,39 @@ public function newQueryBuilder($parameters)
5663
return app()->make(QueryBuilder::class, $parameters);
5764
}
5865

66+
/**
67+
* Sets the security flag for the query builder.
68+
*
69+
* Toggling this flag disables or enables security checks during query building.
70+
*
71+
* @param bool $disable Set to true to disable security checks (default), or false to enable them.
72+
*
73+
* @return $this The current instance for method chaining.
74+
*/
5975
public function disableSecurity($disable = true)
6076
{
6177
$this->disableSecurity = $disable;
6278

6379
return $this;
6480
}
6581

82+
/**
83+
* Set whether to disable applying the default query limit.
84+
*
85+
* When disabled, the query will not enforce any default limit on the number of results,
86+
* allowing retrieval of all matching records unless a custom limit is specified.
87+
*
88+
* @param bool $disable True to disable the default limit, false to enable it.
89+
*
90+
* @return self Returns the current instance for chaining.
91+
*/
92+
public function disableDefaultLimit($disable = true)
93+
{
94+
$this->disableDefaultLimit = $disable;
95+
96+
return $this;
97+
}
98+
6699
/**
67100
* Convert the query builder to an Eloquent query builder.
68101
*

src/Query/ScoutBuilder.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,23 @@ class ScoutBuilder implements QueryBuilder
1313
use Conditionable;
1414

1515
/**
16-
* Perform a search operation on the query builder.
16+
* Executes a search query using Laravel Scout with customizable criteria.
1717
*
18-
* @param array $parameters An array of search parameters.
18+
* This method configures the underlying search builder by setting the query text,
19+
* and conditionally applying filters, sort orders, and additional instructions based
20+
* on the provided parameters. It also enforces a limit on the number of results,
21+
* defaulting to 50 if no limit is specified. Any extra parameters, after excluding
22+
* reserved keys (filters, instructions, sorts, text, and limit), are forwarded to the
23+
* underlying search operation.
1924
*
20-
* @return \Laravel\Scout\Builder The modified query builder.
25+
* @param array $parameters An associative array of search criteria, which may include:
26+
* - 'text': The search query string.
27+
* - 'filters': An array of filter conditions.
28+
* - 'sorts': An array of sorting directives.
29+
* - 'instructions': Additional query instructions.
30+
* - 'limit': Maximum number of results to return (defaults to 50 if not provided).
31+
*
32+
* @return \Laravel\Scout\Builder The configured Scout query builder.
2133
*/
2234
public function search(array $parameters = [])
2335
{
@@ -37,6 +49,8 @@ public function search(array $parameters = [])
3749
$this->applyInstructions($parameters['instructions']);
3850
});
3951

52+
$this->queryBuilder->take($parameters['limit'] ?? 50);
53+
4054
$this->queryBuilder
4155
->query(function (Builder $query) use ($parameters) {
4256
app()->make(QueryBuilder::class, ['query' => $query, 'resource' => $this->resource])
@@ -48,6 +62,7 @@ public function search(array $parameters = [])
4862
'instructions',
4963
'sorts',
5064
'text',
65+
'limit',
5166
])
5267
->all()
5368
);

src/Query/Traits/PerformSearch.php

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,25 @@
1010
trait PerformSearch
1111
{
1212
/**
13-
* Perform a search operation on the query builder.
13+
* Executes a modular search on the query builder based on provided parameters.
1414
*
15-
* @param array $parameters An array of search parameters.
15+
* This method first authorizes the view operation on the model and then, unless security is disabled,
16+
* applies a security-aware search query. It conditionally processes various search parameters to:
17+
* - Filter results via a nested subquery to prevent conflicts.
18+
* - Sort results using custom definitions if provided, or default ordering from the resource.
19+
* - Apply named scopes, instructions, includes, and aggregate functions.
20+
* - Limit the result set according to a specified limit or a default value of 50.
1621
*
17-
* @return Builder The modified query builder.
22+
* @param array $parameters An associative array of search parameters that may include:
23+
* - 'filters': Criteria for filtering the results.
24+
* - 'sorts': Definitions for ordering the results.
25+
* - 'scopes': Named scopes to refine the query.
26+
* - 'instructions': Additional instructions for query customization.
27+
* - 'includes': Related resources to include in the query.
28+
* - 'aggregates': Aggregate functions to apply on related data.
29+
* - 'limit': Maximum number of records to return.
30+
*
31+
* @return Builder The modified query builder with the applied search parameters.
1832
*/
1933
public function search(array $parameters = [])
2034
{
@@ -32,7 +46,7 @@ public function search(array $parameters = [])
3246
});
3347
});
3448

35-
if (!isset($parameters['sorts']) || empty($parameters['sorts'])) {
49+
if (empty($parameters['sorts'])) {
3650
foreach ($this->resource->defaultOrderBy(
3751
app()->make(RestRequest::class)
3852
) as $column => $order) {
@@ -59,9 +73,9 @@ public function search(array $parameters = [])
5973
$this->applyAggregates($parameters['aggregates']);
6074
});
6175

62-
// In case we are in a relation we don't apply the limits since we don't know how much records will be related.
63-
if (!$this->queryBuilder instanceof Relation && !$this->disableSecurity) {
64-
$this->queryBuilder->limit($parameters['limit'] ?? 50);
76+
$limit = $this->disableDefaultLimit() && !isset($parameters['limit']) ? null : ($parameters['limit'] ?? 50);
77+
if ($limit !== null) {
78+
$this->queryBuilder->limit($limit);
6579
}
6680

6781
return $this->queryBuilder;

tests/Feature/Controllers/ActionsOperationsTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,43 @@ public function test_operate_action_with_search(): void
295295
);
296296
}
297297

298+
public function test_operate_action_with_search_and_limit(): void
299+
{
300+
ModelFactory::new()
301+
->count(300)
302+
->create([
303+
'string' => 'match',
304+
]);
305+
306+
ModelFactory::new()->count(2)
307+
->create();
308+
309+
Gate::policy(Model::class, GreenPolicy::class);
310+
311+
$response = $this->post(
312+
'/api/models/actions/modify-number',
313+
[
314+
'search' => [
315+
'filters' => [
316+
['field' => 'string', 'value' => 'match'],
317+
],
318+
'limit' => 150,
319+
],
320+
],
321+
['Accept' => 'application/json']
322+
);
323+
324+
$response->assertJson([
325+
'data' => [
326+
'impacted' => 150,
327+
],
328+
]);
329+
$this->assertEquals(
330+
150,
331+
Model::where('number', 100000000)->count()
332+
);
333+
}
334+
298335
public function test_operate_batchable_action(): void
299336
{
300337
ModelFactory::new()->count(150)->create();

0 commit comments

Comments
 (0)