Skip to content

Commit 55aa90b

Browse files
authored
Merge pull request #7 from kjsoftware/v2
V2
2 parents 8b4a330 + 6484f30 commit 55aa90b

File tree

10 files changed

+206
-45
lines changed

10 files changed

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

src/Concerns/AddsFieldsToQuery.php

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44

55
use Illuminate\Support\Collection;
66
use Illuminate\Support\Str;
7+
use Spatie\QueryBuilder\AllowedField;
78
use Spatie\QueryBuilder\Exceptions\AllowedFieldsMustBeCalledBeforeAllowedIncludes;
89
use Spatie\QueryBuilder\Exceptions\InvalidFieldQuery;
910
use Spatie\QueryBuilder\Exceptions\UnknownIncludedFieldsQuery;
1011

1112
trait AddsFieldsToQuery
1213
{
13-
protected ?Collection $allowedFields = null;
14+
public ?Collection $allowedFields = null;
1415

1516
public function allowedFields($fields): static
1617
{
@@ -20,10 +21,13 @@ public function allowedFields($fields): static
2021

2122
$fields = is_array($fields) ? $fields : func_get_args();
2223

23-
$this->allowedFields = collect($fields)
24-
->map(function (string $fieldName) {
25-
return $this->prependField($fieldName);
26-
});
24+
$this->allowedFields = collect($fields)->map(function ($field) {
25+
if ($field instanceof AllowedField) {
26+
return $field;
27+
}
28+
29+
return AllowedField::partial($field);
30+
});
2731

2832
$this->ensureAllFieldsExist();
2933

@@ -36,14 +40,34 @@ protected function addRequestedModelFieldsToQuery(): void
3640
{
3741
$modelTableName = $this->getModel()->getTable();
3842

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

4363
if (empty($modelFields)) {
4464
return;
4565
}
4666

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

4973
$this->select($prependedFields);
@@ -65,7 +89,6 @@ public function getRequestedFieldsForRelatedTable(string $relation): array
6589

6690
if (! $this->allowedFields instanceof Collection) {
6791
// We have requested fields but no allowed fields (yet?)
68-
6992
throw new UnknownIncludedFieldsQuery($fields);
7093
}
7194

@@ -74,21 +97,17 @@ public function getRequestedFieldsForRelatedTable(string $relation): array
7497

7598
protected function ensureAllFieldsExist(): void
7699
{
77-
$modelTable = $this->getModel()->getTable();
100+
// Map fieldnames from object
101+
$allowedFields = $this->allowedFields->map(function (AllowedField $field) {
102+
return $field->getName();
103+
});
78104

79-
$requestedFields = $this->request->fields()
80-
->map(function ($fields, $model) use ($modelTable) {
81-
$tableName = $model;
105+
$requestedFields = $this->request->fields();
82106

83-
return $this->prependFieldsWithTableName($fields, $model === '_' ? $modelTable : $tableName);
84-
})
85-
->flatten()
86-
->unique();
87-
88-
$unknownFields = $requestedFields->diff($this->allowedFields);
107+
$unknownFields = $requestedFields->pluck('name')->diff($allowedFields);
89108

90109
if ($unknownFields->isNotEmpty()) {
91-
throw InvalidFieldQuery::fieldsNotAllowed($unknownFields, $this->allowedFields);
110+
throw InvalidFieldQuery::fieldsNotAllowed($unknownFields, $allowedFields);
92111
}
93112
}
94113

@@ -107,10 +126,14 @@ protected function prependField(string $field, ?string $table = null): string
107126

108127
if (Str::contains($field, '.')) {
109128
// Already prepended
110-
111129
return $field;
112130
}
113131

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

src/Concerns/FiltersQuery.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,9 @@ protected function ensureAllFiltersExist(): void
7676
throw InvalidFilterQuery::filtersNotAllowed($diff, $allowedFilterNames);
7777
}
7878
}
79+
80+
public function getAllowedFilters(): ?Collection
81+
{
82+
return $this->allowedFilters;
83+
}
7984
}

src/Concerns/SortsQuery.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,9 @@ protected function ensureAllSortsExist(): void
9595
throw InvalidSortQuery::sortsNotAllowed($unknownSorts, $allowedSortNames);
9696
}
9797
}
98+
99+
public function getAllowedSorts(): ?Collection
100+
{
101+
return $this->allowedSorts;
102+
}
98103
}

src/Mappings/Column.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Spatie\QueryBuilder\Mappings;
4+
5+
class Column
6+
{
7+
/**
8+
* Create a new currency instance.
9+
*
10+
* @param string $name
11+
* @return void
12+
*/
13+
public function __construct(string $name)
14+
{
15+
$this->name = $name;
16+
}
17+
18+
}

src/QueryBuilderRequest.php

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Illuminate\Http\Request;
66
use Illuminate\Support\Collection;
77
use Illuminate\Support\Str;
8+
use Spatie\QueryBuilder\Mappings\Column;
89

910
class QueryBuilderRequest extends Request
1011
{
@@ -65,32 +66,13 @@ public function fields(): Collection
6566

6667
$fieldsPerTable = collect(is_string($fieldsData) ? explode(static::getFieldsArrayValueDelimiter(), $fieldsData) : $fieldsData);
6768

68-
if ($fieldsPerTable->isEmpty()) {
69+
$data = $this->getRequestData($fieldsParameterName);
70+
71+
if (! $data) {
6972
return collect();
7073
}
7174

72-
$fields = [];
73-
74-
$fieldsPerTable->each(function ($tableFields, $model) use (&$fields) {
75-
if (is_numeric($model)) {
76-
// If the field is in dot notation, we'll grab the table without the field.
77-
// If the field isn't in dot notation we want the base table. We'll use `_` and replace it later.
78-
$model = Str::contains($tableFields, '.') ? Str::beforeLast($tableFields, '.') : '_';
79-
}
80-
81-
if (! isset($fields[$model])) {
82-
$fields[$model] = [];
83-
}
84-
85-
// If the field is in dot notation, we'll grab the field without the tables:
86-
$tableFields = array_map(function (string $field) {
87-
return Str::afterLast($field, '.');
88-
}, explode(static::getFieldsArrayValueDelimiter(), $tableFields));
89-
90-
$fields[$model] = array_merge($fields[$model], $tableFields);
91-
});
92-
93-
return collect($fields);
75+
return collect(explode(static::getFieldsArrayValueDelimiter(), $data))->mapInto(Column::class);
9476
}
9577

9678
public function sorts(): Collection

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)