Skip to content

Commit a4731b8

Browse files
authored
Merge pull request #3 from kjsoftware/test/SV-v21
Test/sv v21
2 parents 4c96d8d + 5e1946d commit a4731b8

File tree

8 files changed

+151
-35
lines changed

8 files changed

+151
-35
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@
77

88
This package allows you to filter, sort and include eloquent relations based on a request. The `QueryBuilder` used in this package extends Laravel's default Eloquent builder. This means all your favorite methods and macros are still available. Query parameter names follow the [JSON API specification](http://jsonapi.org/) as closely as possible.
99

10+
## Caching foreign keys
11+
Add this line to your composer.json file to cache foreign keys. This will allow the query builder to automatically detect foreign keys without having to make a database call.
12+
These foreign keys will always be included in select statements, which will prevent a loss of potential relations.
13+
```json
14+
"scripts": {
15+
"post-update-cmd": [
16+
"@php artisan query-builder:cache-foreign-keys"
17+
],
18+
}
19+
```
20+
1021
## Basic usage
1122

1223
### Filter a query based on a request: `/users?filter[name]=John`:

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"illuminate/database": "^9.0|^10.0",
2525
"illuminate/http": "^9.0|^10.0",
2626
"illuminate/support": "^9.0|^10.0",
27-
"spatie/laravel-package-tools": "^1.11"
27+
"spatie/laravel-package-tools": "^1.11",
28+
"doctrine/dbal": "^3.5"
2829
},
2930
"require-dev": {
3031
"ext-json": "*",

src/AllowedField.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,19 @@
22

33
namespace Spatie\QueryBuilder;
44

5+
use Illuminate\Support\Collection;
56
use Spatie\QueryBuilder\Filters\Filter;
67

78
class AllowedField
89
{
9-
/** @var string */
10-
protected $name;
10+
protected string $name;
11+
protected Collection $internalNames;
1112

12-
/** @var string */
13-
protected $internalName;
14-
public function __construct(string $name, ?string $internalName = null)
13+
public function __construct(string $name, string|array $internalName = null)
1514
{
1615
$this->name = $name;
1716

18-
$this->internalName = $internalName ?? $name;
17+
$this->internalNames = collect($internalName);
1918
}
2019

2120

@@ -26,18 +25,19 @@ public static function setFilterArrayValueDelimiter(string $delimiter = null): v
2625
}
2726
}
2827

29-
public static function partial(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self
28+
public static function partial(string $name, $internalNames = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self
3029
{
3130
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
32-
return new static($name, $internalName);
31+
return new static($name, $internalNames);
3332
}
3433

3534
public function getName(): string
3635
{
3736
return $this->name;
3837
}
39-
public function getInternalName(): string
38+
39+
public function getInternalNames(): Collection
4040
{
41-
return $this->internalName;
41+
return $this->internalNames;
4242
}
4343
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace Spatie\QueryBuilder\App\Console\Commands;
4+
5+
use Illuminate\Console\Command;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Support\Facades\Cache;
8+
use Illuminate\Support\Facades\File;
9+
use Illuminate\Support\Facades\Schema;
10+
use Illuminate\Support\Str;
11+
use ReflectionClass;
12+
13+
class CacheForeignKeys extends Command
14+
{
15+
16+
protected $signature = 'query-builder:cache-foreign-keys';
17+
18+
protected $description = 'Cache foreign keys for the QueryBuilder package.';
19+
20+
public function __invoke(): void
21+
{
22+
$modelsDirectory = app_path('Models');
23+
24+
$files = File::allFiles($modelsDirectory);
25+
26+
$foreignKeys = [];
27+
$this->info('Fetching all models in App\Models...' . PHP_EOL);
28+
29+
foreach ($files as $file) {
30+
// Build the full class name
31+
$fullClassName = 'App\\Models' . '\\' . $file->getRelativePath() . '\\' . pathinfo($file->getRelativePathname(), PATHINFO_FILENAME);
32+
$fullClassName = Str::replace('/', '\\', $fullClassName);
33+
34+
// Check if the class exists
35+
if (class_exists($fullClassName) && is_subclass_of($fullClassName, Model::class)) {
36+
// Create a ReflectionClass instance
37+
$reflectionClass = new ReflectionClass($fullClassName);
38+
39+
// Check if the class is instantiable
40+
if ($reflectionClass->isInstantiable()) {
41+
// Instantiate the class
42+
$instance = $reflectionClass->newInstance();
43+
$table = $instance->getTable();
44+
45+
// Get all foreign keys for the table
46+
$tableForeignKeys = Schema::getConnection()->getDoctrineSchemaManager()->listTableForeignKeys($table);
47+
48+
// Add the foreign keys to the array
49+
$foreignKeys[$table] = array_reduce($tableForeignKeys, function ($carry, $foreignKey) {
50+
return array_merge($carry, $foreignKey->getLocalColumns());
51+
}, []);
52+
53+
// Add the primary key to the array
54+
$foreignKeys[$table][] = $instance->getKeyName();
55+
} else {
56+
$this->error("The class $fullClassName is not instantiable.");
57+
}
58+
} else {
59+
$this->warn("The class $fullClassName does not exist or does not extend " . Model::class . '.');
60+
}
61+
}
62+
$this->info(PHP_EOL . 'Cached foreign keys for ' . count($foreignKeys) . ' tables.');
63+
64+
Cache::forever('QUERY_BUILDER_FKS', $foreignKeys);
65+
}
66+
67+
public static function getForTable(string $table): array
68+
{
69+
return Cache::get('QUERY_BUILDER_FKS')[$table] ?? [];
70+
}
71+
72+
}

src/Concerns/AddsFieldsToQuery.php

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
use Illuminate\Support\Collection;
66
use Illuminate\Support\Str;
77
use Spatie\QueryBuilder\AllowedField;
8+
use Spatie\QueryBuilder\App\Console\Commands\CacheForeignKeys;
89
use Spatie\QueryBuilder\Exceptions\AllowedFieldsMustBeCalledBeforeAllowedIncludes;
910
use Spatie\QueryBuilder\Exceptions\InvalidFieldQuery;
1011
use Spatie\QueryBuilder\Exceptions\UnknownIncludedFieldsQuery;
1112

1213
trait AddsFieldsToQuery
1314
{
14-
protected ?Collection $allowedFields = null;
15+
public ?Collection $allowedFields = null;
1516

1617
public function allowedFields($fields): static
1718
{
@@ -36,46 +37,64 @@ public function allowedFields($fields): static
3637
return $this;
3738
}
3839

39-
protected function addRequestedModelFieldsToQuery()
40+
protected function addRequestedModelFieldsToQuery(): void
4041
{
4142
$modelTableName = $this->getModel()->getTable();
4243

43-
$this->allowedFields->map(function (AllowedField $field) {
44-
if ($this->request->fields()->where('name', $field->getName())->count() > 0)
45-
return $field->getInternalName();
46-
})->toArray();
44+
$requestFields = $this->request->fields()->map(function ($field) {
45+
return $field->name;
46+
});
47+
48+
$modelFields = $this->allowedFields->mapWithKeys(function (AllowedField $field) {
49+
return [
50+
$field->getName() => $field->getInternalNames()->toArray()
51+
];
52+
});
53+
54+
if ($requestFields->count() > 0) {
55+
// If fields are requested, only select those
56+
$modelFields = $modelFields->filter(function ($internalName, $name) use ($requestFields) {
57+
return $requestFields->contains($name);
58+
})->toArray();
59+
} else {
60+
// If no fields are requested, select all allowed fields
61+
$modelFields = $modelFields->toArray();
62+
}
4763

4864
if (empty($modelFields)) {
4965
return;
5066
}
5167

68+
// Flatten array
69+
$modelFields = array_unique(array_merge(...array_values($modelFields)));
70+
71+
// Prepend the fields with the table name
5272
$prependedFields = $this->prependFieldsWithTableName($modelFields, $modelTableName);
5373

5474
$this->select($prependedFields);
5575
}
5676

5777
public function getRequestedFieldsForRelatedTable(string $relation): array
5878
{
59-
$table = Str::plural(Str::snake($relation)); // TODO: make this configurable
79+
$table = Str::plural(Str::snake($relation));
6080

6181
$fields = $this->request->fields()->mapWithKeys(function ($fields, $table) {
6282
return [$table => $fields];
6383
})->get($table);
6484

65-
if (! $fields) {
85+
if (!$fields) {
6686
return [];
6787
}
6888

69-
if (! $this->allowedFields instanceof Collection) {
89+
if (!$this->allowedFields instanceof Collection) {
7090
// We have requested fields but no allowed fields (yet?)
71-
7291
throw new UnknownIncludedFieldsQuery($fields);
7392
}
7493

7594
return $fields;
7695
}
7796

78-
protected function ensureAllFieldsExist()
97+
protected function ensureAllFieldsExist(): void
7998
{
8099
// Map fieldnames from object
81100
$allowedFields = $this->allowedFields->map(function (AllowedField $field) {
@@ -100,16 +119,20 @@ protected function prependFieldsWithTableName(array $fields, string $tableName):
100119

101120
protected function prependField(string $field, ?string $table = null): string
102121
{
103-
if (! $table) {
122+
if (!$table) {
104123
$table = $this->getModel()->getTable();
105124
}
106125

107126
if (Str::contains($field, '.')) {
108127
// Already prepended
109-
110128
return $field;
111129
}
112130

113131
return "{$table}.{$field}";
114132
}
133+
134+
public function getAllowedFields(): ?Collection
135+
{
136+
return $this->allowedFields;
137+
}
115138
}

src/Concerns/FiltersQuery.php

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

33
namespace Spatie\QueryBuilder\Concerns;
44

5+
use Illuminate\Support\Collection;
56
use Spatie\QueryBuilder\AllowedFilter;
67
use Spatie\QueryBuilder\Exceptions\InvalidFilterQuery;
78

@@ -78,4 +79,9 @@ protected function ensureAllFiltersExist()
7879
throw InvalidFilterQuery::filtersNotAllowed($diff, $allowedFilterNames);
7980
}
8081
}
82+
83+
public function getAllowedFilters(): ?Collection
84+
{
85+
return $this->allowedFilters;
86+
}
8187
}

src/Concerns/SortsQuery.php

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,18 @@
22

33
namespace Spatie\QueryBuilder\Concerns;
44

5+
use Illuminate\Support\Collection;
56
use Spatie\QueryBuilder\AllowedSort;
67
use Spatie\QueryBuilder\Exceptions\InvalidSortQuery;
8+
use Spatie\QueryBuilder\QueryBuilder;
79

810
trait SortsQuery
911
{
10-
/** @var \Illuminate\Support\Collection */
12+
/** @var Collection */
1113
protected $allowedSorts;
1214

1315
public function allowedSorts($sorts): static
1416
{
15-
if ($this->request->sorts()->isEmpty()) {
16-
// We haven't got any requested sorts. No need to parse allowed sorts.
17-
18-
return $this;
19-
}
20-
2117
$sorts = is_array($sorts) ? $sorts : func_get_args();
2218

2319
$this->allowedSorts = collect($sorts)->map(function ($sort) {
@@ -36,19 +32,19 @@ public function allowedSorts($sorts): static
3632
}
3733

3834
/**
39-
* @param array|string|\Spatie\QueryBuilder\AllowedSort $sorts
35+
* @param array|string|AllowedSort $sorts
4036
*
41-
* @return \Spatie\QueryBuilder\QueryBuilder
37+
* @return QueryBuilder
4238
*/
4339
public function defaultSort($sorts): static
4440
{
4541
return $this->defaultSorts(func_get_args());
4642
}
4743

4844
/**
49-
* @param array|string|\Spatie\QueryBuilder\AllowedSort $sorts
45+
* @param array|string|AllowedSort $sorts
5046
*
51-
* @return \Spatie\QueryBuilder\QueryBuilder
47+
* @return QueryBuilder
5248
*/
5349
public function defaultSorts($sorts): static
5450
{
@@ -113,4 +109,9 @@ protected function ensureAllSortsExist(): void
113109
throw InvalidSortQuery::sortsNotAllowed($unknownSorts, $allowedSortNames);
114110
}
115111
}
112+
113+
public function getAllowedSorts(): ?Collection
114+
{
115+
return $this->allowedSorts;
116+
}
116117
}

src/QueryBuilderServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44

55
use Spatie\LaravelPackageTools\Package;
66
use Spatie\LaravelPackageTools\PackageServiceProvider;
7+
use Spatie\QueryBuilder\App\Console\Commands\CacheForeignKeys;
78

89
class QueryBuilderServiceProvider extends PackageServiceProvider
910
{
1011
public function configurePackage(Package $package): void
1112
{
1213
$package
1314
->name('laravel-query-builder')
15+
->hasConsoleCommands(CacheForeignKeys::class)
1416
->hasConfigFile();
1517
}
1618

0 commit comments

Comments
 (0)