Skip to content

Commit 939c13c

Browse files
authored
Merge pull request #4 from kjsoftware/develop
V1
2 parents 4d5d51c + a4731b8 commit 939c13c

File tree

10 files changed

+222
-40
lines changed

10 files changed

+222
-40
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: 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+
use Spatie\QueryBuilder\Filters\Filter;
7+
8+
class AllowedField
9+
{
10+
protected string $name;
11+
protected Collection $internalNames;
12+
13+
public function __construct(string $name, string|array $internalName = null)
14+
{
15+
$this->name = $name;
16+
17+
$this->internalNames = collect($internalName);
18+
}
19+
20+
21+
public static function setFilterArrayValueDelimiter(string $delimiter = null): void
22+
{
23+
if (isset($delimiter)) {
24+
QueryBuilderRequest::setFilterArrayValueDelimiter($delimiter);
25+
}
26+
}
27+
28+
public static function partial(string $name, $internalNames = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self
29+
{
30+
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
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: 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: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44

55
use Illuminate\Support\Collection;
66
use Illuminate\Support\Str;
7+
use Spatie\QueryBuilder\AllowedField;
8+
use Spatie\QueryBuilder\App\Console\Commands\CacheForeignKeys;
79
use Spatie\QueryBuilder\Exceptions\AllowedFieldsMustBeCalledBeforeAllowedIncludes;
810
use Spatie\QueryBuilder\Exceptions\InvalidFieldQuery;
911
use Spatie\QueryBuilder\Exceptions\UnknownIncludedFieldsQuery;
1012

1113
trait AddsFieldsToQuery
1214
{
13-
protected ?Collection $allowedFields = null;
15+
public ?Collection $allowedFields = null;
1416

1517
public function allowedFields($fields): static
1618
{
@@ -20,10 +22,13 @@ public function allowedFields($fields): static
2022

2123
$fields = is_array($fields) ? $fields : func_get_args();
2224

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

2833
$this->ensureAllFieldsExist();
2934

@@ -32,57 +37,76 @@ public function allowedFields($fields): static
3237
return $this;
3338
}
3439

35-
protected function addRequestedModelFieldsToQuery()
40+
protected function addRequestedModelFieldsToQuery(): void
3641
{
3742
$modelTableName = $this->getModel()->getTable();
3843

39-
$modelFields = $this->request->fields()->get($modelTableName);
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+
}
4063

4164
if (empty($modelFields)) {
4265
return;
4366
}
4467

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

4774
$this->select($prependedFields);
4875
}
4976

5077
public function getRequestedFieldsForRelatedTable(string $relation): array
5178
{
52-
$table = Str::plural(Str::snake($relation)); // TODO: make this configurable
79+
$table = Str::plural(Str::snake($relation));
5380

5481
$fields = $this->request->fields()->mapWithKeys(function ($fields, $table) {
5582
return [$table => $fields];
5683
})->get($table);
5784

58-
if (! $fields) {
85+
if (!$fields) {
5986
return [];
6087
}
6188

62-
if (! $this->allowedFields instanceof Collection) {
89+
if (!$this->allowedFields instanceof Collection) {
6390
// We have requested fields but no allowed fields (yet?)
64-
6591
throw new UnknownIncludedFieldsQuery($fields);
6692
}
6793

6894
return $fields;
6995
}
7096

71-
protected function ensureAllFieldsExist()
97+
protected function ensureAllFieldsExist(): void
7298
{
73-
$requestedFields = $this->request->fields()
74-
->map(function ($fields, $model) {
75-
$tableName = $model;
99+
// Map fieldnames from object
100+
$allowedFields = $this->allowedFields->map(function (AllowedField $field) {
101+
return $field->getName();
102+
});
76103

77-
return $this->prependFieldsWithTableName($fields, $tableName);
78-
})
79-
->flatten()
80-
->unique();
104+
$requestedFields = $this->request->fields();
81105

82-
$unknownFields = $requestedFields->diff($this->allowedFields);
106+
$unknownFields = $requestedFields->pluck('name')->diff($allowedFields);
83107

84108
if ($unknownFields->isNotEmpty()) {
85-
throw InvalidFieldQuery::fieldsNotAllowed($unknownFields, $this->allowedFields);
109+
throw InvalidFieldQuery::fieldsNotAllowed($unknownFields, $allowedFields);
86110
}
87111
}
88112

@@ -95,16 +119,20 @@ protected function prependFieldsWithTableName(array $fields, string $tableName):
95119

96120
protected function prependField(string $field, ?string $table = null): string
97121
{
98-
if (! $table) {
122+
if (!$table) {
99123
$table = $this->getModel()->getTable();
100124
}
101125

102126
if (Str::contains($field, '.')) {
103127
// Already prepended
104-
105128
return $field;
106129
}
107130

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

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/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+
function __construct(string $name)
14+
{
15+
$this->name = $name;
16+
}
17+
18+
}

0 commit comments

Comments
 (0)