vendor and env first commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
custom: https://spatie.be/open-source/support-us
|
||||
@@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Bug Report
|
||||
url: https://github.com/spatie/laravel-query-builder/issues/new
|
||||
about: Report a bug
|
||||
- name: Feature Request
|
||||
url: https://github.com/spatie/laravel-query-builder/discussions/new?category_id=3330702
|
||||
about: Share ideas for new features
|
||||
- name: Ask a Question
|
||||
url: https://github.com/spatie/laravel-query-builder/discussions/new?category_id=3330701
|
||||
about: Ask the community for help
|
||||
@@ -0,0 +1,8 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
name: dependabot-auto-merge
|
||||
on: pull_request_target
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
steps:
|
||||
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v2.1.0
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
compat-lookup: true
|
||||
|
||||
- name: Auto-merge Dependabot PRs for semver-minor updates
|
||||
if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}}
|
||||
run: gh pr merge --auto --merge "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
|
||||
- name: Auto-merge Dependabot PRs for semver-patch updates
|
||||
if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
|
||||
run: gh pr merge --auto --merge "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
|
||||
- name: Auto-merge Dependabot PRs for Action major versions when compatibility is higher than 90%
|
||||
if: ${{steps.metadata.outputs.package-ecosystem == 'github_actions' && steps.metadata.outputs.update-type == 'version-update:semver-major' && steps.metadata.outputs.compatibility-score >= 90}}
|
||||
run: gh pr merge --auto --merge "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Check & fix styling
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
php-cs-fixer:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Run PHP CS Fixer
|
||||
uses: docker://oskarstark/php-cs-fixer-ga
|
||||
with:
|
||||
args: --config=.php_cs.dist.php --allow-risky=yes
|
||||
|
||||
- name: Commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: Fix styling
|
||||
@@ -0,0 +1,84 @@
|
||||
name: run-tests
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.php'
|
||||
- '.github/workflows/run-tests.yml'
|
||||
- 'phpunit.xml.dist'
|
||||
- 'composer.json'
|
||||
- 'composer.lock'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 5
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
php: [8.3, 8.2]
|
||||
laravel: [10.*, 11.*]
|
||||
stability: [prefer-stable]
|
||||
include:
|
||||
- laravel: 11.*
|
||||
testbench: 9.*
|
||||
carbon: ^2.63
|
||||
- laravel: 10.*
|
||||
testbench: 8.*
|
||||
|
||||
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }}
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
env:
|
||||
MYSQL_USER: user
|
||||
MYSQL_PASSWORD: secret
|
||||
MYSQL_DATABASE: laravel_query_builder
|
||||
MYSQL_ROOT_PASSWORD: secretroot
|
||||
ports:
|
||||
- 3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
|
||||
coverage: none
|
||||
|
||||
- name: Setup problem matchers
|
||||
run: |
|
||||
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
|
||||
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
|
||||
composer update --${{ matrix.stability }} --prefer-dist --no-interaction
|
||||
|
||||
- name: Execute tests
|
||||
run: vendor/bin/pest
|
||||
env:
|
||||
DB_USERNAME: user
|
||||
DB_PASSWORD: secret
|
||||
DB_PORT: ${{ job.services.mysql.ports[3306] }}
|
||||
REDIS_PORT: 6379
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: "Update Changelog"
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Update Changelog
|
||||
uses: stefanzweifel/changelog-updater-action@v1
|
||||
with:
|
||||
latest-version: ${{ github.event.release.name }}
|
||||
release-notes: ${{ github.event.release.body }}
|
||||
|
||||
- name: Commit updated CHANGELOG
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
branch: main
|
||||
commit_message: Update CHANGELOG
|
||||
file_pattern: CHANGELOG.md
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->in([
|
||||
__DIR__ . '/src',
|
||||
__DIR__ . '/tests',
|
||||
])
|
||||
->name('*.php')
|
||||
->notName('*.blade.php')
|
||||
->ignoreDotFiles(true)
|
||||
->ignoreVCS(true);
|
||||
|
||||
return (new PhpCsFixer\Config())
|
||||
->setRules([
|
||||
'@PSR12' => true,
|
||||
'array_syntax' => ['syntax' => 'short'],
|
||||
'ordered_imports' => ['sort_algorithm' => 'alpha'],
|
||||
'no_unused_imports' => true,
|
||||
'not_operator_with_successor_space' => true,
|
||||
'trailing_comma_in_multiline' => true,
|
||||
'phpdoc_scalar' => true,
|
||||
'unary_operator_spaces' => true,
|
||||
'binary_operator_spaces' => true,
|
||||
'blank_line_before_statement' => [
|
||||
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
|
||||
],
|
||||
'phpdoc_single_line_var_spacing' => true,
|
||||
'phpdoc_var_without_name' => true,
|
||||
'method_argument_space' => [
|
||||
'on_multiline' => 'ensure_fully_multiline',
|
||||
'keep_multiple_spaces_after_comma' => true,
|
||||
],
|
||||
'single_trait_insert_per_statement' => true,
|
||||
])
|
||||
->setFinder($finder);
|
||||
File diff suppressed because one or more lines are too long
+713
@@ -0,0 +1,713 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to `laravel-query-builder` will be documented in this file
|
||||
|
||||
## 5.8.0 - 2024-02-06
|
||||
|
||||
### What's Changed
|
||||
|
||||
* [Docs] Update config file content by @shdehnavi in https://github.com/spatie/laravel-query-builder/pull/918
|
||||
* Bump: Deprecating Laravel 9 and PHP 8.1, adding Laravel 11 support by @JustSteveKing in https://github.com/spatie/laravel-query-builder/pull/922
|
||||
|
||||
### New Contributors
|
||||
|
||||
* @shdehnavi made their first contribution in https://github.com/spatie/laravel-query-builder/pull/918
|
||||
* @JustSteveKing made their first contribution in https://github.com/spatie/laravel-query-builder/pull/922
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/5.7.0...5.8.0
|
||||
|
||||
## 5.7.0 - 2024-01-08
|
||||
|
||||
### What's Changed
|
||||
|
||||
* Start testing against PHP 8.3 by @sergiy-petrov in https://github.com/spatie/laravel-query-builder/pull/899
|
||||
* Add the possibility to use the literal relation names in the `allowedFields`. by @carvemerson in https://github.com/spatie/laravel-query-builder/pull/917
|
||||
* Add `unsetDefault` as a replacement for `default(null)` which was removed in 5.6.0 by @patrickrobrecht in https://github.com/spatie/laravel-query-builder/pull/902
|
||||
* Allow passing an array to the `defaultSort` function as documented by @MajidMohammadian in https://github.com/spatie/laravel-query-builder/pull/904
|
||||
* Add `disable_invalid_includes_query_exception` config option by @dimzeta in https://github.com/spatie/laravel-query-builder/pull/906
|
||||
* Update `AllowedFilter.php` to include `getFilterClass` function by @justasSendrauskas in https://github.com/spatie/laravel-query-builder/pull/909
|
||||
|
||||
### New Contributors
|
||||
|
||||
* @sergiy-petrov made their first contribution in https://github.com/spatie/laravel-query-builder/pull/899
|
||||
* @carvemerson made their first contribution in https://github.com/spatie/laravel-query-builder/pull/917
|
||||
* @patrickrobrecht made their first contribution in https://github.com/spatie/laravel-query-builder/pull/902
|
||||
* @MajidMohammadian made their first contribution in https://github.com/spatie/laravel-query-builder/pull/904
|
||||
* @dimzeta made their first contribution in https://github.com/spatie/laravel-query-builder/pull/906
|
||||
* @justasSendrauskas made their first contribution in https://github.com/spatie/laravel-query-builder/pull/909
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/5.6.0...5.7.0
|
||||
|
||||
## 5.6.0 - 2023-10-05
|
||||
|
||||
### What's Changed
|
||||
|
||||
- Add support for defining includes by callback by @enricodelazzari in https://github.com/spatie/laravel-query-builder/pull/894
|
||||
- Add nullable filters by @enricodelazzari in https://github.com/spatie/laravel-query-builder/pull/895
|
||||
- Fix escaping control characters in partial filters by @GrahamCampbell in https://github.com/spatie/laravel-query-builder/pull/898
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/5.5.0...5.6.0
|
||||
|
||||
## 5.5.0 - 2023-09-12
|
||||
|
||||
### What's Changed
|
||||
|
||||
- Add support for [`withExists`](https://laravel.com/docs/master/eloquent-relationships#other-aggregate-functions) via `IncludedExists` by @enricodelazzari in https://github.com/spatie/laravel-query-builder/pull/891
|
||||
- Use default values for all config keys (avoids issues when `QueryBuilder` is used as a dependency in a package)
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/5.4.0...5.5.0
|
||||
|
||||
## 5.4.0 - 2023-09-08
|
||||
|
||||
### What's Changed
|
||||
|
||||
- Deprecate `request_data_source` config. The `QueryBuilder` will always look at both the query string and the request body when available now
|
||||
- Fix having `null` as the query parameter name for filters (see #889)
|
||||
- Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/spatie/laravel-query-builder/pull/890
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/5.3.0...5.4.0
|
||||
|
||||
## 5.3.0 - 2023-08-21
|
||||
|
||||
### What's Changed
|
||||
|
||||
- Accepts string value for the `fields` query parameter by @ezra-obiwale in https://github.com/spatie/laravel-query-builder/pull/872
|
||||
- Add `FiltersEndsWithStrict` filter by @utsavsomaiya in https://github.com/spatie/laravel-query-builder/pull/885
|
||||
- Make sure the `allowedSorts` are always set (even when none are requested) @luilliarcec in https://github.com/spatie/laravel-query-builder/pull/865
|
||||
|
||||
### New Contributors
|
||||
|
||||
- @ezra-obiwale made their first contribution in https://github.com/spatie/laravel-query-builder/pull/872
|
||||
- @utsavsomaiya made their first contribution in https://github.com/spatie/laravel-query-builder/pull/885
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/5.2.0...5.3.0
|
||||
|
||||
## 5.2.0 - 2023-02-24
|
||||
|
||||
### What's Changed
|
||||
|
||||
- Bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 by @dependabot in https://github.com/spatie/laravel-query-builder/pull/843
|
||||
- Update sorting.md by @shukriYusof in https://github.com/spatie/laravel-query-builder/pull/846
|
||||
- Update custom sorts link in documentation by @turpoint in https://github.com/spatie/laravel-query-builder/pull/844
|
||||
- Add config to disable InvalidSortQuery exception by @bohemima in https://github.com/spatie/laravel-query-builder/pull/830
|
||||
|
||||
### New Contributors
|
||||
|
||||
- @shukriYusof made their first contribution in https://github.com/spatie/laravel-query-builder/pull/846
|
||||
- @turpoint made their first contribution in https://github.com/spatie/laravel-query-builder/pull/844
|
||||
- @bohemima made their first contribution in https://github.com/spatie/laravel-query-builder/pull/830
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/5.1.2...5.2.0
|
||||
|
||||
## 5.1.2 - 2023-01-24
|
||||
|
||||
### What's Changed
|
||||
|
||||
- Update including-relationships.md by @designvoid in https://github.com/spatie/laravel-query-builder/pull/837
|
||||
- Fix workflow badges in README by @nelson6e65 in https://github.com/spatie/laravel-query-builder/pull/841
|
||||
- Laravel 10.x Compatibility by @laravel-shift in https://github.com/spatie/laravel-query-builder/pull/842
|
||||
|
||||
### New Contributors
|
||||
|
||||
- @designvoid made their first contribution in https://github.com/spatie/laravel-query-builder/pull/837
|
||||
- @nelson6e65 made their first contribution in https://github.com/spatie/laravel-query-builder/pull/841
|
||||
- @laravel-shift made their first contribution in https://github.com/spatie/laravel-query-builder/pull/842
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/5.1.1...5.1.2
|
||||
|
||||
## 5.1.1 - 2022-12-02
|
||||
|
||||
### What's Changed
|
||||
|
||||
- Fix `array_diff_assoc` BC break in v5.1.0 by @stevebauman in https://github.com/spatie/laravel-query-builder/pull/827
|
||||
|
||||
### New Contributors
|
||||
|
||||
- @stevebauman made their first contribution in https://github.com/spatie/laravel-query-builder/pull/827
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/5.1.0...5.1.1
|
||||
|
||||
## 4.0.4 - 2022-11-28
|
||||
|
||||
### What's Changed
|
||||
|
||||
- bugfix: appending to `pluck`ed values (that are not a `Model`) is not possible
|
||||
- Add version number to installation command in V4 by @jamesbhatta in https://github.com/spatie/laravel-query-builder/pull/786
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/4.0.3...4.0.4
|
||||
|
||||
## 5.1.0 - 2022-11-28
|
||||
|
||||
### What's Changed
|
||||
|
||||
- Feature: Update Generics in IncludeInterface by @sidigi in https://github.com/spatie/laravel-query-builder/pull/810
|
||||
- Feature: Add PHP 8.2 Support by @patinthehat in https://github.com/spatie/laravel-query-builder/pull/825
|
||||
- Feature: Add `beginsWithStrict` filter by @danilopinotti in https://github.com/spatie/laravel-query-builder/pull/821
|
||||
- Bugfix: ignore allowed filters by @davidjr82 in https://github.com/spatie/laravel-query-builder/pull/818
|
||||
- Bugfix: Change self to static when creating query builder by @olliescase in https://github.com/spatie/laravel-query-builder/pull/819
|
||||
- Docs: Update filtering.md by @Dion213 in https://github.com/spatie/laravel-query-builder/pull/801
|
||||
- Misc: Add Dependabot Automation by @patinthehat in https://github.com/spatie/laravel-query-builder/pull/823
|
||||
- Misc: Bump actions/checkout from 2 to 3 by @dependabot in https://github.com/spatie/laravel-query-builder/pull/824
|
||||
|
||||
### New Contributors
|
||||
|
||||
- @Dion213 made their first contribution in https://github.com/spatie/laravel-query-builder/pull/801
|
||||
- @sidigi made their first contribution in https://github.com/spatie/laravel-query-builder/pull/810
|
||||
- @patinthehat made their first contribution in https://github.com/spatie/laravel-query-builder/pull/823
|
||||
- @dependabot made their first contribution in https://github.com/spatie/laravel-query-builder/pull/824
|
||||
- @davidjr82 made their first contribution in https://github.com/spatie/laravel-query-builder/pull/818
|
||||
- @olliescase made their first contribution in https://github.com/spatie/laravel-query-builder/pull/819
|
||||
- @danilopinotti made their first contribution in https://github.com/spatie/laravel-query-builder/pull/821
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/5.0.3...5.1.0
|
||||
|
||||
## 5.0.3 - 2022-07-29
|
||||
|
||||
### What's Changed
|
||||
|
||||
- Fixed: Some links in the documentation for v5 pointing to v2 pages by @hameedraha in https://github.com/spatie/laravel-query-builder/pull/757
|
||||
- static return type when returning $this by @lorenzolosa in https://github.com/spatie/laravel-query-builder/pull/775
|
||||
- [PHP 8.2] Fix `${var}` string interpolation deprecation by @Ayesh in https://github.com/spatie/laravel-query-builder/pull/779
|
||||
- Fix grammar by @clouder in https://github.com/spatie/laravel-query-builder/pull/784
|
||||
- Add Inertia.js Tables for Laravel Query Builder by @fabianpnke in https://github.com/spatie/laravel-query-builder/pull/790
|
||||
- Fix Laravel 9 PHPStan generic check for `__invoke()` method of Filter by @kayw-geek in https://github.com/spatie/laravel-query-builder/pull/781
|
||||
- Fix for Warning by @shaunluedeke in https://github.com/spatie/laravel-query-builder/pull/791
|
||||
|
||||
### New Contributors
|
||||
|
||||
- @hameedraha made their first contribution in https://github.com/spatie/laravel-query-builder/pull/757
|
||||
- @lorenzolosa made their first contribution in https://github.com/spatie/laravel-query-builder/pull/775
|
||||
- @Ayesh made their first contribution in https://github.com/spatie/laravel-query-builder/pull/779
|
||||
- @clouder made their first contribution in https://github.com/spatie/laravel-query-builder/pull/784
|
||||
- @fabianpnke made their first contribution in https://github.com/spatie/laravel-query-builder/pull/790
|
||||
- @shaunluedeke made their first contribution in https://github.com/spatie/laravel-query-builder/pull/791
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/5.0.2...5.0.3
|
||||
|
||||
## 5.0.2 - 2022-04-25
|
||||
|
||||
## What's Changed
|
||||
|
||||
- Fix Laravel 9 PHPStan generic check by @kayw-geek in https://github.com/spatie/laravel-query-builder/pull/749
|
||||
|
||||
## New Contributors
|
||||
|
||||
- @kayw-geek made their first contribution in https://github.com/spatie/laravel-query-builder/pull/749
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/5.0.1...5.0.2
|
||||
|
||||
## 4.0.3 - 2022-03-23
|
||||
|
||||
## What's Changed
|
||||
|
||||
- V4 - Add support for laravel > 7.30.4 by @luilliarcec in https://github.com/spatie/laravel-query-builder/pull/744
|
||||
|
||||
## New Contributors
|
||||
|
||||
- @luilliarcec made their first contribution in https://github.com/spatie/laravel-query-builder/pull/744
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/4.0.2...4.0.3
|
||||
|
||||
## 5.0.1 - 2022-03-18
|
||||
|
||||
## What's Changed
|
||||
|
||||
- Update README.md by @wayz9 in https://github.com/spatie/laravel-query-builder/pull/713
|
||||
- Fix release order in CHANGELOG by @medvinator in https://github.com/spatie/laravel-query-builder/pull/717
|
||||
- Fix include casing docs by @canvural in https://github.com/spatie/laravel-query-builder/pull/733
|
||||
- Adapt documentation for publishing package config by @dominikb in https://github.com/spatie/laravel-query-builder/pull/734
|
||||
- Fix warning from passing null to explode for includeParts by @steven-fox in https://github.com/spatie/laravel-query-builder/pull/742
|
||||
|
||||
## New Contributors
|
||||
|
||||
- @wayz9 made their first contribution in https://github.com/spatie/laravel-query-builder/pull/713
|
||||
- @medvinator made their first contribution in https://github.com/spatie/laravel-query-builder/pull/717
|
||||
- @canvural made their first contribution in https://github.com/spatie/laravel-query-builder/pull/733
|
||||
- @steven-fox made their first contribution in https://github.com/spatie/laravel-query-builder/pull/742
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/5.0.0...5.0.1
|
||||
|
||||
## 5.0.0 - 2022-01-13
|
||||
|
||||
- add support for Laravel 9
|
||||
- drop support for older versions
|
||||
|
||||
## 4.0.2 - 2021-12-26
|
||||
|
||||
## What's Changed
|
||||
|
||||
- DOC: New sample with multiple default sorts by @williamxsp in https://github.com/spatie/laravel-query-builder/pull/694
|
||||
- PHP 8.1 Support by @Medalink in https://github.com/spatie/laravel-query-builder/pull/702
|
||||
|
||||
## New Contributors
|
||||
|
||||
- @williamxsp made their first contribution in https://github.com/spatie/laravel-query-builder/pull/694
|
||||
- @Medalink made their first contribution in https://github.com/spatie/laravel-query-builder/pull/702
|
||||
|
||||
**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/4.0.1...4.0.2
|
||||
|
||||
## 4.0.1 - 2021-10-27
|
||||
|
||||
- revert deferred service provider (#677)
|
||||
|
||||
## 4.0.0 - 2021-10-20
|
||||
|
||||
- nested filters will no longer be automatically camel-cased to match a relationship name
|
||||
- includes will no longer be automatically camel-cased to match a relationship name
|
||||
- fields will no longer be automatically snake-cased to match table or column names
|
||||
- switch to deferred service provider
|
||||
|
||||
Take a look at the [upgrade guide](./UPGRADING.md) for a more detailed explanation.
|
||||
|
||||
## 3.6.0 - 2021-09-06
|
||||
|
||||
- add callback sorts (#654)
|
||||
|
||||
## 3.5.0 - 2021-07-05
|
||||
|
||||
- add support for cursor pagination
|
||||
|
||||
## 3.4.3 - 2021-07-05
|
||||
|
||||
- fix unexpected lowercase appends (#637)
|
||||
|
||||
## 3.4.2 - 2021-07-05
|
||||
|
||||
- no changes
|
||||
|
||||
## 3.4.1 - 2021-05-24
|
||||
|
||||
- fix simple paginator append not working (#633)
|
||||
|
||||
## 3.4.0 - 2021-05-20
|
||||
|
||||
- add support for custom includes (#623)
|
||||
- add support for getting request data from the request body (#589)
|
||||
- fix issues when cloning `QueryBuilder` (#621)
|
||||
|
||||
## 3.3.4 - 2020-11-26
|
||||
|
||||
- prepend table name to `WHERE` clause for ambiguous partial filters (#567)
|
||||
- add PHP 8 support
|
||||
|
||||
## 3.3.3 - 2020-10-27
|
||||
|
||||
- prepend table name to `WHERE` clause for ambiguous exact filters (#467)
|
||||
|
||||
## 3.3.2 - 2020-10-27
|
||||
|
||||
- fix config key to disable `InvalidFilterQuery` exception
|
||||
|
||||
## 3.3.1 - 2020-10-11
|
||||
|
||||
- make nested scope compatible with older Laravel (#542)
|
||||
|
||||
## 3.3.0 - 2020-10-05
|
||||
|
||||
- add ability to filter by nested relationship scopes (#519)
|
||||
- add config key to disable `InvalidFilterQuery` exception (#525)
|
||||
|
||||
## 3.2.4 - 2020-10-01
|
||||
|
||||
- update what defines an ignored filter value (#533)
|
||||
|
||||
## 3.2.3 - 2020-09-30
|
||||
|
||||
- add LengthAwarePaginator to QueryBuilder (#532)
|
||||
|
||||
## 3.2.2 - 2020-09-09
|
||||
|
||||
- Revert changes from v3.2.1 to `AllowedFilter::filter()`
|
||||
|
||||
## 3.2.1 - 2020-09-09
|
||||
|
||||
- Fix filtering associative arrays (#488)
|
||||
- AllowedFilter::filter() takes a `Illuminate\Database\Eloquent\Builder` instead of a QueryBuilder instance
|
||||
|
||||
## 3.2.0 - 2020-09-08
|
||||
|
||||
- add support for Laravel 8
|
||||
|
||||
## 3.1.0 - 2020-08-18
|
||||
|
||||
- add individual array delimiters for includes, filters, appends and sorts
|
||||
- ensure relations queried using the exact filter are actual relations on the model
|
||||
|
||||
## 3.0.0 - 2020-08-18
|
||||
|
||||
New major version. Please read the [UPGRADING](UPGRADING.md) guide *before* upgrading.
|
||||
|
||||
- `Spatie\QueryBuilder\QueryBuilder` class no longer extends Laravel's `Illuminate\Database\Eloquent\Builder`
|
||||
|
||||
## 2.8.2 - 2020-05-25
|
||||
|
||||
- fix scope filters that are added via macros (e.g. `onlyTrashed`) (#469)
|
||||
|
||||
## 2.8.1 - 2020-03-20
|
||||
|
||||
- make service provider deferrable (#381)
|
||||
|
||||
## 2.8.0 - 2020-03-02
|
||||
|
||||
- add support for Laravel 7
|
||||
|
||||
## 2.7.2 - 2020-02-26
|
||||
|
||||
- small fix for lumen (#436)
|
||||
|
||||
## 2.7.1 - 2020-02-26
|
||||
|
||||
- small fix for lumen in service provider
|
||||
|
||||
## 2.7.0 - 2020-02-12
|
||||
|
||||
- add support for model binding in scope filter parameters (#415)
|
||||
|
||||
## 2.6.1 - 2020-02-11
|
||||
|
||||
- fix alias for multiple allowed includes (#414)
|
||||
|
||||
## 2.6.0 - 2020-02-10
|
||||
|
||||
- add `FiltersTrashed` for filtering soft-deleted models
|
||||
- add `FiltersCallback` for filtering using a callback
|
||||
|
||||
## 2.5.1 - 2020-01-22
|
||||
|
||||
- fix dealing with empty or `null` includes (#395)
|
||||
- fix passing an associative array of scope filter values (#387)
|
||||
|
||||
## 2.5.0 - 2020-01-09
|
||||
|
||||
- add `defaultDirection`
|
||||
|
||||
## 2.4.0 - 2020-01-04
|
||||
|
||||
- add support for a custom filter delimiter (#369)
|
||||
|
||||
## 2.3.0 - 2019-10-08
|
||||
|
||||
- resolve `QueryBuilderRequest` from service container
|
||||
|
||||
## 2.2.1 - 2019-10-03
|
||||
|
||||
- fix issue when passing camel-cased includes (#336)
|
||||
|
||||
## 2.2.0 - 2019-09-24
|
||||
|
||||
- add option to disable parsing relationship constraints when filtering related model properties in the exact and partial filters (#262)
|
||||
- fix selecting fields from included relationships that are multiple levels deep (#317)
|
||||
|
||||
## 2.1.0 - 2019-09-03
|
||||
|
||||
- add support for Laravel 6
|
||||
|
||||
## 2.0.1 - 2019-08-12
|
||||
|
||||
- update doc block for `QueryBuilder::for()`
|
||||
- add missing typehint in `SortsField`
|
||||
|
||||
## 2.0.0 - 2019-08-12
|
||||
|
||||
- removed request macros
|
||||
- sorts and field selects are not allowed by default and need to be explicitly allowed
|
||||
- requesting an include suffixed with `Count` will add the related models' count using `$query->withCount()`
|
||||
- custom sorts and filters now need to be passed as instances
|
||||
- renamed `Spatie\QueryBuilder\Sort` to `Spatie\QueryBuilder\AllowedSort`
|
||||
- renamed `Spatie\QueryBuilder\Included` to `Spatie\QueryBuilder\AllowedInclude`
|
||||
- renamed `Spatie\QueryBuilder\Filter` to `Spatie\QueryBuilder\AllowedFilter`
|
||||
- `Filter`, `Include` and `Sort` interfaces no longer need to return the `Builder` instance
|
||||
- `allowedFields` should be called before `allowedIncludes`
|
||||
- filters can now have default values
|
||||
- includes will be converted to camelcase before being parsed
|
||||
|
||||
## 1.17.5 - 2019-07-08
|
||||
|
||||
- bugfix: correctly parse sorts in `chunk`ed query (#299)
|
||||
- bugfix: don't parse empty values in arrays for partial filters (#285)
|
||||
|
||||
## 1.17.4 - 2019-06-03
|
||||
|
||||
- bugfix: `orderByRaw` is no longer being rejected as a sorting option (#258)
|
||||
- bugfix: `addSelect` is no longer being replaced by the `?fields` parameter (#260)
|
||||
- bugfix: take leading dash into account when remembering generated sort columns (#272)
|
||||
- bugfix: `allowedIncludes` no longer adds duplicate includes for nested includes (#251)
|
||||
|
||||
## 1.17.3 - 2019-04-16
|
||||
|
||||
- bugfix: remove duplicate parsing of (default) sort clauses
|
||||
|
||||
## 1.17.2 - 2019-04-12
|
||||
|
||||
- bugfix: replace missing `sort()` method on `QueryBuilderRequest`
|
||||
- bugfix: don't escape `allowedSort`s and their aliases
|
||||
- bugfix: don't escape `allowedField`s
|
||||
|
||||
## 1.17.1 - 2019-04-09
|
||||
|
||||
- security fixes
|
||||
|
||||
## 1.16.1 - 2019-04-09
|
||||
|
||||
- security fixes
|
||||
|
||||
## 1.17.0 - 2019-03-11
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- moved features to traits
|
||||
- started using `QueryBuilderRequest` to read data from the current request
|
||||
- deprecated request macros (`Request::filters()`, `Request::includes()`, etc...)
|
||||
- raised minimum supported Laravel version to 5.6.34
|
||||
|
||||
## 1.16.0 - 2019-03-05
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- add support for multiple default sorts (#214)
|
||||
|
||||
## 1.15.2 - 2019-02-28
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- add support for Laravel 5.5 and up (again)
|
||||
- add support for PHP 7.1 and up (again)
|
||||
|
||||
## 1.15.1 - 2019-02-28
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- fix default sort not parsing correctly (#178)
|
||||
|
||||
## 1.15.0 - 2019-02-27
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- drop support for Laravel 5.7 and lower
|
||||
- drop support for PHP 7.1 and lower
|
||||
|
||||
## 1.14.0 - 2019-02-27
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- add aliased sorts (#164)
|
||||
|
||||
## 1.13.2 - 2019-02-27
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- add support for Laravel 5.8
|
||||
- use Str:: and Arr:: instead of helper methods
|
||||
|
||||
## 1.13.1 - 2019-01-18
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- fix detection of false-positives for ignored values (#154)
|
||||
- fix broken morphTo includes (#130)
|
||||
|
||||
## 1.13.0 - 2019-01-12
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- allow ignoring specific filter values using `$filter->ignore()`
|
||||
- allow filtering related model attributes `allowedFilters('related-model.name')`
|
||||
- fix for filtering by relation model properties
|
||||
- add custom sort classes
|
||||
|
||||
## 1.12.0 - 2018-11-27
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- allow differently named columns
|
||||
|
||||
## 1.11.2 - 2018-10-30
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- fix exception when using filters with nested arrays (#117)
|
||||
- fix overwritten fields when using `allowedIncludes` with many-to-many relationships (#118)
|
||||
|
||||
## 1.11.1 - 2018-10-09
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- fix exception when using `allowedFields()` but selecting none
|
||||
|
||||
## 1.11.0 - 2018-10-03
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- add `allowedFields` method
|
||||
- fix & cleanup `Request::fields()` macro
|
||||
- fix fields option (`SELECT * FROM table` instead of `SELECT table.* FROM table`)
|
||||
|
||||
## 1.10.4 - 2018-10-02
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- fix parsing empty filters from url
|
||||
|
||||
## 1.10.3 - 2018-09-17
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- improve compatibility with Lumen
|
||||
|
||||
## 1.10.2 - 2018-08-28
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- add support for Laravel 5.7
|
||||
- add framework/laravel as a dependency
|
||||
|
||||
## 1.10.1 - 2018-08-21
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- improve compatibility with Lumen by only publishing the config file in console mode
|
||||
|
||||
## 1.10.0 - 2018-06-12
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- add support for instantiated custom filter classes
|
||||
|
||||
## 1.9.6 - 2018-06-11
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- fix for using reserved SQL words as attributes in Postgres
|
||||
|
||||
## 1.9.5 - 2018-06-09
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- make sure filtering on string with special characters just works
|
||||
|
||||
## 1.9.4 - 2018-06-06
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- fix for using reserved SQL words as attributes
|
||||
|
||||
## 1.9.3 - 2018-06-05
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- resolved #14
|
||||
|
||||
## 1.9.2 - 2018-05-21
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- prevent double sorting statments
|
||||
|
||||
## 1.9.1 - 2018-05-15
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- improvements around field selection
|
||||
|
||||
## 1.9.0 - 2018-05-02
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- add `Filter::scope()` for querying scopes
|
||||
- explicitly defining parent includes in nested queries is no longer required
|
||||
|
||||
## 1.8.0 - 2018-03-28
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- add `allowedAppends()`
|
||||
|
||||
## 1.7.0 - 2018-03-23
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- add ability to customize query parameter names
|
||||
|
||||
## 1.6.0 - 2018-03-05
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- add support for selecting specific columns using `?fields[table]=field_name`
|
||||
|
||||
## 1.5.3 - 2018-02-09
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- allow arrays in filters
|
||||
|
||||
## 1.5.2 - 2018-02-08
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- add support for Laravel 5.6
|
||||
|
||||
## 1.5.1 - 2018-02-07
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- fix: initializing scopes, macro's, the onDelete callback and eager loads from base query on QueryBuilder
|
||||
|
||||
## 1.5.0 - 2018-02-06
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- use specific exceptions for every invalid query
|
||||
|
||||
## 1.4.0 - 2018-02-05
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- allow multiple sorts
|
||||
|
||||
## 1.3.0 - 2018-02-05
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- allow `allowedIncludes`, `allowedFilters` and `allowedSorts` to accept arrays
|
||||
|
||||
## 1.2.1 - 2018-02-03
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- remove auto registering facade from composer.json
|
||||
|
||||
## 1.2.0 - 2018-01-29
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- add support for global scopes and soft deletes
|
||||
|
||||
## 1.1.2 - 2018-01-23
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- bugfix: revert #11 (escaping `_` and `%` in LIKE queries)
|
||||
|
||||
## 1.1.1 - 2018-01-22
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- escape `_` and `%` in LIKE queries
|
||||
|
||||
## 1.1.0 - 2018-01-20
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- add ability to set a default sort attribute
|
||||
|
||||
## 1.0.1 - 2018-01-19
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- bugfix: using `allowedSorts` together with an empty sort query parameter no longer throws an exception
|
||||
|
||||
## 1.0.0 - 2018-01-17
|
||||
|
||||
**DO NOT USE: THIS VERSION ALLOWS SQL INJECTION ATTACKS**
|
||||
|
||||
- initial release! 🎉
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Spatie bvba <info@spatie.be>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
# Build Eloquent queries from API requests
|
||||
|
||||
[](https://packagist.org/packages/spatie/laravel-query-builder)
|
||||

|
||||

|
||||
[](https://packagist.org/packages/spatie/laravel-query-builder)
|
||||
|
||||
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.
|
||||
|
||||
## Basic usage
|
||||
|
||||
### Filter a query based on a request: `/users?filter[name]=John`:
|
||||
|
||||
```php
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedFilters('name')
|
||||
->get();
|
||||
|
||||
// all `User`s that contain the string "John" in their name
|
||||
```
|
||||
|
||||
[Read more about filtering features like: partial filters, exact filters, scope filters, custom filters, ignored values, default filter values, ...](https://spatie.be/docs/laravel-query-builder/v5/features/filtering/)
|
||||
|
||||
### Including relations based on a request: `/users?include=posts`:
|
||||
|
||||
```php
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedIncludes('posts')
|
||||
->get();
|
||||
|
||||
// all `User`s with their `posts` loaded
|
||||
```
|
||||
|
||||
[Read more about include features like: including nested relationships, including relationship count, custom includes, ...](https://spatie.be/docs/laravel-query-builder/v5/features/including-relationships/)
|
||||
|
||||
### Sorting a query based on a request: `/users?sort=id`:
|
||||
|
||||
```php
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedSorts('id')
|
||||
->get();
|
||||
|
||||
// all `User`s sorted by ascending id
|
||||
```
|
||||
|
||||
[Read more about sorting features like: custom sorts, sort direction, ...](https://spatie.be/docs/laravel-query-builder/v5/features/sorting/)
|
||||
|
||||
### Works together nicely with existing queries:
|
||||
|
||||
```php
|
||||
$query = User::where('active', true);
|
||||
|
||||
$userQuery = QueryBuilder::for($query) // start from an existing Builder instance
|
||||
->withTrashed() // use your existing scopes
|
||||
->allowedIncludes('posts', 'permissions')
|
||||
->where('score', '>', 42); // chain on any of Laravel's query builder methods
|
||||
```
|
||||
|
||||
### Selecting fields for a query: `/users?fields[users]=id,email`
|
||||
|
||||
```php
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedFields(['id', 'email'])
|
||||
->get();
|
||||
|
||||
// the fetched `User`s will only have their id & email set
|
||||
```
|
||||
|
||||
[Read more about selecting fields.](https://spatie.be/docs/laravel-query-builder/v5/features/selecting-fields/)
|
||||
|
||||
## Support us
|
||||
|
||||
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/laravel-query-builder.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/laravel-query-builder)
|
||||
|
||||
We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
|
||||
|
||||
We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).
|
||||
|
||||
## Installation
|
||||
|
||||
You can install the package via composer:
|
||||
|
||||
```bash
|
||||
composer require spatie/laravel-query-builder
|
||||
```
|
||||
|
||||
Read the installation notes on the docs site: [https://spatie.be/docs/laravel-query-builder/v5/installation-setup](https://spatie.be/docs/laravel-query-builder/v5/installation-setup/).
|
||||
|
||||
## Documentation
|
||||
|
||||
You can find the documentation on [https://spatie.be/docs/laravel-query-builder/v5](https://spatie.be/docs/laravel-query-builder/v5).
|
||||
|
||||
Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving the media library? Feel free to [create an issue on GitHub](https://github.com/spatie/laravel-query-builder/issues), we'll try to address it as soon as possible.
|
||||
|
||||
If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker.
|
||||
|
||||
### Upgrading
|
||||
|
||||
Please see [UPGRADING.md](UPGRADING.md) for details.
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
composer test
|
||||
```
|
||||
|
||||
### Changelog
|
||||
|
||||
Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
|
||||
|
||||
### Security
|
||||
|
||||
If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker.
|
||||
|
||||
## Credits
|
||||
|
||||
- [Alex Vanderbist](https://github.com/AlexVanderbist)
|
||||
- [All Contributors](../../contributors)
|
||||
|
||||
## License
|
||||
|
||||
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Upgrading
|
||||
|
||||
## Notice when upgrading to 5.6.0
|
||||
|
||||
The changes to the `default()` method break backwards compatibility when setting the default value to `null` (`default(null)`). This is pretty much an edge case, but if you're trying to unset the default value, you can use the `unsetDefault()` method instead.
|
||||
|
||||
## From v4 to v5
|
||||
|
||||
This version adds support for Laravel 9 and drops support for all older version.
|
||||
|
||||
Appending attributes to a query was removed to make package maintenance easier. The rest of the public API was not changed, so you'll be able to upgrade without making any changes.
|
||||
|
||||
## From v3 to v4
|
||||
|
||||
The biggest change in v4 is the way requested filters, includes and fields are processed. In previous versions we would automatically camel-case relationship names for includes and nested filters. Requested (nested) fields would also be transformed to their plural snake-case form, regardless of what was actually requested.
|
||||
|
||||
In v4 we've removed this behaviour and will instead always pass the requested filter, include or field from the request URL to the query.
|
||||
|
||||
When following Laravel's convention of camelcase relationship names, a request will look like this:
|
||||
|
||||
```
|
||||
GET /api/users
|
||||
?include=latestPosts,friendRequests
|
||||
&filter[homeAddress.city]=Antwerp
|
||||
&fields[related_models.test_models]=id,name
|
||||
```
|
||||
|
||||
A minimal `QueryBuilder` for the above request looks like this:
|
||||
|
||||
```php
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
|
||||
QueryBuilder::for(User::class)
|
||||
->allowedIncludes(['latestPosts', 'friendRequests'])
|
||||
->allowedFilters(['homeAddress.city'])
|
||||
->allowedFields(['related_models.test_models.id', 'related_models.test_models.name']);
|
||||
```
|
||||
|
||||
There is no automated upgrade path available at this time.
|
||||
|
||||
## From v2 to v3
|
||||
|
||||
Possible changes in this version due to internal changes.
|
||||
|
||||
The package's `Spatie\QueryBuilder\QueryBuilder` class no longer extends Laravel's `Illuminate\Database\Eloquent\Builder`. This means you can no longer pass a `QueryBuilder` instance where a `Illuminate\Database\Eloquent\Builder` instance is expected. However, all Eloquent method calls get forwarded to the internal `Illuminate\Database\Eloquent\Builder`.
|
||||
|
||||
Using `$queryBuilder->getEloquentBuilder()` you can access the internal `Illuminate\Database\Eloquent\Builder`.
|
||||
|
||||
## From v1 to v2
|
||||
|
||||
There are a lot of renamed methods and classes in this release. An advanced IDE like PhpStorm is recommended to rename these methods and classes in your code base. Use the refactor -> rename functionality instead of find & replace.
|
||||
|
||||
- rename `Spatie\QueryBuilder\Sort` to `Spatie\QueryBuilder\AllowedSort`
|
||||
- rename `Spatie\QueryBuilder\Included` to `Spatie\QueryBuilder\AllowedInclude`
|
||||
- rename `Spatie\QueryBuilder\Filter` to `Spatie\QueryBuilder\AllowedFilter`
|
||||
- replace request macro's like `request()->filters()`, `request()->includes()`, etc... with their related methods on the `QueryBuilderRequest` class. This class needs to be instantiated with a request object, (more info here: https://github.com/spatie/laravel-query-builder/issues/328):
|
||||
* `request()->includes()` -> `QueryBuilderRequest::fromRequest($request)->includes()`
|
||||
* `request()->filters()` -> `QueryBuilderRequest::fromRequest($request)->filters()`
|
||||
* `request()->sorts()` -> `QueryBuilderRequest::fromRequest($request)->sorts()`
|
||||
* `request()->fields()` -> `QueryBuilderRequest::fromRequest($request)->fields()`
|
||||
* `request()->appends()` -> `QueryBuilderRequest::fromRequest($request)->appends()`
|
||||
- please note that the above methods on `QueryBuilderRequest` do not take any arguments. You can use the `contains` to check for a certain filter/include/sort/...
|
||||
- make sure the second argument for `AllowedSort::custom()` is an instance of a sort class, not a classname
|
||||
* `AllowedSort::custom('name', MySort::class)` -> `AllowedSort::custom('name', new MySort())`
|
||||
- make sure the second argument for `AllowedFilter::custom()` is an instance of a filter class, not a classname
|
||||
* `AllowedFilter::custom('name', MyFilter::class)` -> `AllowedFilter::custom('name', new MyFilter())`
|
||||
- make sure all required sorts are allowed using `allowedSorts()`
|
||||
- make sure all required field selects are allowed using `allowedFields()`
|
||||
- make sure `allowedFields()` is always called before `allowedIncludes()`
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "spatie/laravel-query-builder",
|
||||
"description": "Easily build Eloquent queries from API requests",
|
||||
"keywords": [
|
||||
"spatie",
|
||||
"laravel-query-builder"
|
||||
],
|
||||
"homepage": "https://github.com/spatie/laravel-query-builder",
|
||||
"license": "MIT",
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/laravel-query-builder/issues",
|
||||
"source": "https://github.com/spatie/laravel-query-builder"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Alex Vanderbist",
|
||||
"email": "alex@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"illuminate/database": "^10.0|^11.0",
|
||||
"illuminate/http": "^10.0|^11.0",
|
||||
"illuminate/support": "^10.0|^11.0",
|
||||
"spatie/laravel-package-tools": "^1.11"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"mockery/mockery": "^1.4",
|
||||
"orchestra/testbench": "^7.0|^8.0",
|
||||
"pestphp/pest": "^2.0",
|
||||
"spatie/invade": "^2.0",
|
||||
"spatie/laravel-ray": "^1.28"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\QueryBuilder\\": "src",
|
||||
"Spatie\\QueryBuilder\\Database\\Factories\\": "database/factories"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Spatie\\QueryBuilder\\Tests\\": "tests"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vendor/bin/pest",
|
||||
"test-coverage": "phpunit --coverage-html coverage"
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Spatie\\QueryBuilder\\QueryBuilderServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
* By default the package will use the `include`, `filter`, `sort`
|
||||
* and `fields` query parameters as described in the readme.
|
||||
*
|
||||
* You can customize these query string parameters here.
|
||||
*/
|
||||
'parameters' => [
|
||||
'include' => 'include',
|
||||
|
||||
'filter' => 'filter',
|
||||
|
||||
'sort' => 'sort',
|
||||
|
||||
'fields' => 'fields',
|
||||
|
||||
'append' => 'append',
|
||||
],
|
||||
|
||||
/*
|
||||
* Related model counts are included using the relationship name suffixed with this string.
|
||||
* For example: GET /users?include=postsCount
|
||||
*/
|
||||
'count_suffix' => 'Count',
|
||||
|
||||
/*
|
||||
* Related model exists are included using the relationship name suffixed with this string.
|
||||
* For example: GET /users?include=postsExists
|
||||
*/
|
||||
'exists_suffix' => 'Exists',
|
||||
|
||||
/*
|
||||
* By default the package will throw an `InvalidFilterQuery` exception when a filter in the
|
||||
* URL is not allowed in the `allowedFilters()` method.
|
||||
*/
|
||||
'disable_invalid_filter_query_exception' => false,
|
||||
|
||||
/*
|
||||
* By default the package will throw an `InvalidSortQuery` exception when a sort in the
|
||||
* URL is not allowed in the `allowedSorts()` method.
|
||||
*/
|
||||
'disable_invalid_sort_query_exception' => false,
|
||||
|
||||
/*
|
||||
* By default the package will throw an `InvalidIncludeQuery` exception when an include in the
|
||||
* URL is not allowed in the `allowedIncludes()` method.
|
||||
*/
|
||||
'disable_invalid_includes_query_exception' => false,
|
||||
|
||||
/*
|
||||
* By default, the package expects relationship names to be snake case plural when using fields[relationship].
|
||||
* For example, fetching the id and name for a userOwner relation would look like this:
|
||||
* GET /users?fields[user_owner]=id,name
|
||||
*
|
||||
* Set this to `false` if you don't want that and keep the requested relationship names as-is and allows you to
|
||||
* request the fields using a camelCase relationship name:
|
||||
* GET /users?fields[userOwner]=id,name
|
||||
*/
|
||||
'convert_relation_names_to_snake_case_plural' => true,
|
||||
];
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Database\Factories;
|
||||
|
||||
use Spatie\QueryBuilder\Tests\TestClasses\Models\AppendModel;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class AppendModelFactory extends Factory
|
||||
{
|
||||
protected $model = AppendModel::class;
|
||||
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'firstname' => $this->faker->firstName,
|
||||
'lastname' => $this->faker->lastName,
|
||||
];
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Spatie\QueryBuilder\Tests\TestClasses\Models\SoftDeleteModel;
|
||||
|
||||
class SoftDeleteModelFactory extends Factory
|
||||
{
|
||||
protected $model = SoftDeleteModel::class;
|
||||
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->name,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Spatie\QueryBuilder\Tests\TestClasses\Models\TestModel;
|
||||
|
||||
class TestModelFactory extends Factory
|
||||
{
|
||||
protected $model = TestModel::class;
|
||||
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->name,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: v5
|
||||
slogan: Easily build Eloquent queries from API requests.
|
||||
githubUrl: https://github.com/spatie/laravel-query-builder
|
||||
branch: main
|
||||
---
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: About us
|
||||
weight: 4
|
||||
---
|
||||
|
||||
[Spatie](https://spatie.be) is a webdesign agency based in Antwerp, Belgium.
|
||||
|
||||
Open source software is used in all projects we deliver. Laravel, Nginx, Ubuntu are just a few
|
||||
of the free pieces of software we use every single day. For this, we are very grateful.
|
||||
When we feel we have solved a problem in a way that can help other developers,
|
||||
we release our code as open source software [on GitHub](https://spatie.be/opensource).
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: Advanced usage
|
||||
weight: 3
|
||||
---
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: Extending query builder
|
||||
weight: 1
|
||||
---
|
||||
|
||||
As the `QueryBuilder` extends Laravel's default Eloquent query builder you can use any method or macro you like. You can also specify a base query instead of the model FQCN:
|
||||
|
||||
```php
|
||||
QueryBuilder::for(User::where('id', 42)) // base query instead of model
|
||||
->allowedIncludes(['posts'])
|
||||
->where('activated', true) // chain on any of Laravel's query methods
|
||||
->first(); // we only need one specific user
|
||||
```
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: Front-end implementation
|
||||
weight: 3
|
||||
---
|
||||
|
||||
If you're interested in building query urls on the front-end to match this package, you could use one of the below:
|
||||
|
||||
- Standalone: [elodo package](https://www.npmjs.com/package/elodo) by [Maxim Vanhove](https://github.com/MaximVanhove).
|
||||
- Vue: [vue-api-query package](https://github.com/robsontenorio/vue-api-query) by [Robson Tenório](https://github.com/robsontenorio).
|
||||
- Vue + Inertia.js: [inertiajs-tables-laravel-query-builder](https://github.com/protonemedia/inertiajs-tables-laravel-query-builder) by [
|
||||
Pascal Baljet](https://github.com/pascalbaljet).
|
||||
- React: [cogent-js package](https://www.npmjs.com/package/cogent-js) by [Joel Male](https://github.com/joelwmale).
|
||||
- Typescript: [query-builder-ts package](https://www.npmjs.com/package/@vortechron/query-builder-ts) by [Amirul Adli](https://www.npmjs.com/~vortechron)
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: Multi value delimiter
|
||||
weight: 4
|
||||
---
|
||||
|
||||
Sometimes values to filter for could include commas. This is why you can specify the delimiter symbol using the `QueryBuilderRequest` to overwrite the default behaviour.
|
||||
|
||||
```php
|
||||
// GET /api/endpoint?filter=12,4V|4,7V|2,1V
|
||||
|
||||
QueryBuilderRequest::setArrayValueDelimiter('|');
|
||||
|
||||
QueryBuilder::for(Model::class)
|
||||
->allowedFilters(AllowedFilter::exact('voltage'))
|
||||
->get();
|
||||
|
||||
// filters: [ 'voltage' => [ '12,4V', '4,7V', '2,1V' ]]
|
||||
```
|
||||
|
||||
__Note that this applies to ALL values for filters, includes and sorts__
|
||||
|
||||
## Usage
|
||||
|
||||
There are multiple opportunities where the delimiter can be set.
|
||||
|
||||
You can define it in a `ServiceProvider` to apply it globally, or define a middleware that can be applied only on certain `Controllers`.
|
||||
```php
|
||||
// YourServiceProvider.php
|
||||
public function boot() {
|
||||
QueryBuilderRequest::setArrayValueDelimiter(';');
|
||||
}
|
||||
|
||||
// ApplySemicolonDelimiterMiddleware.php
|
||||
public function handle($request, $next) {
|
||||
QueryBuilderRequest::setArrayValueDelimiter(';');
|
||||
return $next($request);
|
||||
}
|
||||
```
|
||||
|
||||
You can also set the delimiter for each feature individually:
|
||||
```php
|
||||
QueryBuilderRequest::setIncludesArrayValueDelimiter(';'); // Includes
|
||||
QueryBuilderRequest::setAppendsArrayValueDelimiter(';'); // Appends
|
||||
QueryBuilderRequest::setFieldsArrayValueDelimiter(';'); // Fields
|
||||
QueryBuilderRequest::setSortsArrayValueDelimiter(';'); // Sorts
|
||||
QueryBuilderRequest::setFilterArrayValueDelimiter(';'); // Filter
|
||||
```
|
||||
|
||||
You can override the default delimiter for single filters:
|
||||
```php
|
||||
// GET /api/endpoint?filter[id]=h4S4MG3(+>azv4z/I<o>,>XZII/Q1On
|
||||
AllowedFilter::exact('id', 'ref_id', true, ';');
|
||||
```
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: Pagination
|
||||
weight: 2
|
||||
---
|
||||
|
||||
This package doesn't provide any methods to help you paginate responses. However as documented above you can use Laravel's default [`paginate()` method](https://laravel.com/docs/5.5/pagination).
|
||||
|
||||
If you want to completely adhere to the JSON API specification you can also use our own [spatie/json-api-paginate](https://github.com/spatie/laravel-json-api-paginate)!
|
||||
|
||||
## Adding Parameters to Pagination
|
||||
|
||||
By default the query parameters wont be added to the pagination json. You can append the request query to the pagination json by using the `appends` method available on the [LengthAwarePaginator](https://laravel.com/api/6.x/Illuminate/Contracts/Pagination/LengthAwarePaginator.html#method_appends).
|
||||
|
||||
```php
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedFilters(['name', 'email'])
|
||||
->paginate()
|
||||
->appends(request()->query());
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Changelog
|
||||
weight: 6
|
||||
---
|
||||
|
||||
All notable changes to laravel-query-builder are documented [on GitHub](https://github.com/spatie/laravel-query-builder/blob/master/CHANGELOG.md)
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: Features
|
||||
weight: 2
|
||||
---
|
||||
@@ -0,0 +1,322 @@
|
||||
---
|
||||
title: Filtering
|
||||
weight: 1
|
||||
---
|
||||
|
||||
The `filter` query parameters can be used to add `where` clauses to your Eloquent query. Out of the box we support filtering results by partial attribute value, exact attribute value or even if an attribute value exists in a given array of values. For anything more advanced, custom filters can be used.
|
||||
|
||||
By default, all filters have to be explicitly allowed using `allowedFilters()`. This method takes an array of strings or `AllowedFilter` instances. An allowed filter can be partial, beginsWithStrict, endsWithStrict, exact, scope or custom. By default, any string values passed to `allowedFilters()` will automatically be converted to `AllowedFilter::partial()` filters.
|
||||
|
||||
## Basic usage
|
||||
|
||||
```php
|
||||
// GET /users?filter[name]=john&filter[email]=gmail
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedFilters(['name', 'email'])
|
||||
->get();
|
||||
|
||||
// $users will contain all users with "john" in their name AND "gmail" in their email address
|
||||
```
|
||||
|
||||
You can specify multiple matching filter values by passing a comma separated list of values:
|
||||
|
||||
```php
|
||||
// GET /users?filter[name]=seb,freek
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedFilters(['name'])
|
||||
->get();
|
||||
|
||||
// $users will contain all users that contain "seb" OR "freek" in their name
|
||||
```
|
||||
|
||||
By passing column name strings to `allowedFilters`, **partial** filters are automatically applied.
|
||||
|
||||
## Disallowed filters
|
||||
|
||||
Finally, when trying to filter on properties that have not been allowed using `allowedFilters()` an `InvalidFilterQuery` exception will be thrown along with a list of allowed filters.
|
||||
|
||||
|
||||
## Disable InvalidFilterQuery exception
|
||||
|
||||
You can set in configuration file to not throw an InvalidFilterQuery exception when a filter is not set in allowedFilter method. This does **not** allow using any filter, it just disables the exception.
|
||||
|
||||
```php
|
||||
'disable_invalid_filter_query_exception' => true
|
||||
```
|
||||
|
||||
By default the option is set false.
|
||||
|
||||
## Partial, beginsWithStrict and endsWithStrict filters
|
||||
|
||||
By default, all values passed to `allowedFilters` are converted to partial filters. The underlying query will be modified to use a `LIKE LOWER(%value%)` statement. Because this can cause missed indexes, it's often worth considering a `beginsWithStrict` filter for the beginning of the value, or an `endsWithStrict` filter for the end of the value. These filters will use a `LIKE value%` statement and a `LIKE %value` statement respectively, instead of the default partial filter. This can help optimize query performance and index utilization.
|
||||
|
||||
## Exact filters
|
||||
|
||||
When filtering IDs, boolean values or a literal string, you'll want to use exact filters. This way `/users?filter[id]=1` won't match all users having the digit `1` in their ID.
|
||||
|
||||
Exact filters can be added using `Spatie\QueryBuilder\AllowedFilter::exact('property_name')` in the `allowedFilters()` method.
|
||||
|
||||
```php
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
|
||||
// GET /users?filter[name]=John%20Doe
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedFilters([AllowedFilter::exact('name')])
|
||||
->get();
|
||||
|
||||
// only users with the exact name "John Doe"
|
||||
```
|
||||
|
||||
The query builder will automatically map `1`, `0`, `'true'`, and `'false'` as boolean values and a comma separated list of values as an array:
|
||||
|
||||
```php
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
|
||||
// GET /users?filter[id]=1,2,3,4,5&filter[admin]=true
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedFilters([
|
||||
AllowedFilter::exact('id'),
|
||||
AllowedFilter::exact('admin'),
|
||||
])
|
||||
->get();
|
||||
|
||||
// $users will contain all admin users with id 1, 2, 3, 4 or 5
|
||||
```
|
||||
|
||||
## Exact or partial filters for related properties
|
||||
|
||||
You can also add filters for a relationship property using the dot-notation: `AllowedFilter::exact('posts.title')`. This works for exact and partial filters. Under the hood we'll add a `whereHas` statement for the `posts` that filters for the given `title` property as well.
|
||||
|
||||
In some cases you'll want to disable this behaviour and just pass the raw filter-property value to the query. For example, when using a joined table's value for filtering. By passing `false` as the third parameter to `AllowedFilter::exact()` or `AllowedFilter::partial()` this behaviour can be disabled:
|
||||
|
||||
```php
|
||||
$addRelationConstraint = false;
|
||||
|
||||
QueryBuilder::for(User::class)
|
||||
->join('posts', 'posts.user_id', 'users.id')
|
||||
->allowedFilters(AllowedFilter::exact('posts.title', null, $addRelationConstraint));
|
||||
```
|
||||
|
||||
## Scope filters
|
||||
|
||||
Sometimes more advanced filtering options are necessary. This is where scope filters, callback filters and custom filters come in handy.
|
||||
|
||||
Scope filters allow you to add [local scopes](https://laravel.com/docs/master/eloquent#local-scopes) to your query by adding filters to the URL. This works for scopes on the queried model or its relationships using dot-notation.
|
||||
|
||||
Consider the following scope on your model:
|
||||
|
||||
```php
|
||||
public function scopeStartsBefore(Builder $query, $date): Builder
|
||||
{
|
||||
return $query->where('starts_at', '<=', Carbon::parse($date));
|
||||
}
|
||||
```
|
||||
|
||||
To filter based on the `startsBefore` scope, add it to the `allowedFilters` array on the query builder:
|
||||
|
||||
```php
|
||||
QueryBuilder::for(Event::class)
|
||||
->allowedFilters([
|
||||
AllowedFilter::scope('starts_before'),
|
||||
])
|
||||
->get();
|
||||
```
|
||||
|
||||
The following filter will now add the `startsBefore` scope to the underlying query:
|
||||
|
||||
```
|
||||
GET /events?filter[starts_before]=2018-01-01
|
||||
```
|
||||
|
||||
You can even pass multiple parameters to the scope by passing a comma separated list to the filter and use dot-notation for querying scopes on a relationship:
|
||||
|
||||
```
|
||||
GET /events?filter[schedule.starts_between]=2018-01-01,2018-12-31
|
||||
```
|
||||
|
||||
When using scopes that require model instances in the parameters, we'll automatically try to inject the model instances into your scope. This works the same way as route model binding does for injecting Eloquent models into controllers. For example:
|
||||
|
||||
```php
|
||||
public function scopeEvent(Builder $query, \App\Models\Event $event): Builder
|
||||
{
|
||||
return $query->where('id', $event->id);
|
||||
}
|
||||
|
||||
// GET /events?filter[event]=1 - the event with ID 1 will automatically be resolved and passed to the scoped filter
|
||||
```
|
||||
|
||||
Scopes are usually not named with query filters in mind. Use [filter aliases](#filter-aliases) to alias them to something more appropriate:
|
||||
|
||||
```php
|
||||
QueryBuilder::for(User::class)
|
||||
->allowedFilters([
|
||||
AllowedFilter::scope('unconfirmed', 'whereHasUnconfirmedEmail'),
|
||||
// `?filter[unconfirmed]=1` will now add the `scopeWhereHasUnconfirmedEmail` to your query
|
||||
]);
|
||||
```
|
||||
|
||||
## Trashed filters
|
||||
|
||||
When using Laravel's [soft delete feature](https://laravel.com/docs/master/eloquent#querying-soft-deleted-models) you can use the `AllowedFilter::trashed()` filter to query these models.
|
||||
|
||||
The `FiltersTrashed` filter responds to particular values:
|
||||
|
||||
- `with`: include soft-deleted records to the result set
|
||||
- `only`: return only 'trashed' records at the result set
|
||||
- any other value: return only records without that are not soft-deleted in the result set
|
||||
|
||||
For example:
|
||||
|
||||
```php
|
||||
QueryBuilder::for(Booking::class)
|
||||
->allowedFilters([
|
||||
AllowedFilter::trashed(),
|
||||
]);
|
||||
|
||||
// GET /bookings?filter[trashed]=only will only return soft deleted models
|
||||
```
|
||||
|
||||
## Callback filters
|
||||
|
||||
If you want to define a tiny custom filter, you can use a callback filter. Using `AllowedFilter::callback(string $name, callable $filter)` you can specify a callable that will be executed when the filter is requested.
|
||||
|
||||
The filter callback will receive the following parameters: `Builder $query, mixed $value, string $name`. You can modify the `Builder` object to add your own query constraints.
|
||||
|
||||
For example:
|
||||
|
||||
```php
|
||||
QueryBuilder::for(User::class)
|
||||
->allowedFilters([
|
||||
AllowedFilter::callback('has_posts', function (Builder $query, $value) {
|
||||
$query->whereHas('posts');
|
||||
}),
|
||||
]);
|
||||
```
|
||||
|
||||
Using PHP 7.4 this example becomes a lot shorter:
|
||||
|
||||
```php
|
||||
QueryBuilder::for(User::class)
|
||||
->allowedFilters([
|
||||
AllowedFilter::callback('has_posts', fn (Builder $query) => $query->whereHas('posts')),
|
||||
]);
|
||||
```
|
||||
|
||||
## Custom filters
|
||||
|
||||
You can specify custom filters using the `AllowedFilter::custom()` method. Custom filters are instances of invokable classes that implement the `\Spatie\QueryBuilder\Filters\Filter` interface. The `__invoke` method will receive the current query builder instance and the filter name/value. This way you can build any query your heart desires.
|
||||
|
||||
For example:
|
||||
|
||||
```php
|
||||
use Spatie\QueryBuilder\Filters\Filter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class FiltersUserPermission implements Filter
|
||||
{
|
||||
public function __invoke(Builder $query, $value, string $property)
|
||||
{
|
||||
$query->whereHas('permissions', function (Builder $query) use ($value) {
|
||||
$query->where('name', $value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// In your controller for the following request:
|
||||
// GET /users?filter[permission]=createPosts
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedFilters([
|
||||
AllowedFilter::custom('permission', new FiltersUserPermission),
|
||||
])
|
||||
->get();
|
||||
|
||||
// $users will contain all users that have the `createPosts` permission
|
||||
```
|
||||
|
||||
## Filter aliases
|
||||
|
||||
It can be useful to specify an alias for a filter to avoid exposing database column names. For example, your users table might have a `user_passport_full_name` column, which is a horrible name for a filter. Using aliases you can specify a new, shorter name for this filter:
|
||||
|
||||
```php
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
|
||||
// GET /users?filter[name]=John
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedFilters(AllowedFilter::exact('name', 'user_passport_full_name')) // will filter by the `user_passport_full_name` column
|
||||
->get();
|
||||
```
|
||||
|
||||
## Ignored filters values
|
||||
|
||||
You can specify a set of ignored values for every filter. This allows you to not apply a filter when these values are submitted.
|
||||
|
||||
```php
|
||||
QueryBuilder::for(User::class)
|
||||
->allowedFilters([
|
||||
AllowedFilter::exact('name')->ignore(null),
|
||||
])
|
||||
->get();
|
||||
```
|
||||
|
||||
The `ignore()` method takes one or more values, where each may be an array of ignored values. Each of the following calls are valid:
|
||||
|
||||
* `ignore('should_be_ignored')`
|
||||
* `ignore(null, '-1')`
|
||||
* `ignore([null, 'ignore_me', 'also_ignored'])`
|
||||
|
||||
Given an array of values to filter for, only the subset of non-ignored values get passed to the filter. If all values are ignored, the filter does not get applied.
|
||||
|
||||
```php
|
||||
// GET /user?filter[name]=forbidden,John%20Doe
|
||||
|
||||
QueryBuilder::for(User::class)
|
||||
->allowedFilters([
|
||||
AllowedFilter::exact('name')->ignore('forbidden'),
|
||||
])
|
||||
->get();
|
||||
// Returns only users where name matches 'John Doe'
|
||||
|
||||
// GET /user?filter[name]=ignored,ignored_too
|
||||
|
||||
QueryBuilder::for(User::class)
|
||||
->allowedFilters([
|
||||
AllowedFilter::exact('name')->ignore(['ignored', 'ignored_too']),
|
||||
])
|
||||
->get();
|
||||
// Filter does not get applied because all requested values are ignored.
|
||||
```
|
||||
|
||||
## Default Filter Values
|
||||
|
||||
You can specify a default value for a filter if a value for the filter was not present on the request. This is especially useful for boolean filters.
|
||||
|
||||
```php
|
||||
QueryBuilder::for(User::class)
|
||||
->allowedFilters([
|
||||
AllowedFilter::exact('name')->default('Joe'),
|
||||
AllowedFilter::scope('deleted')->default(false),
|
||||
AllowedFilter::scope('permission')->default(null),
|
||||
])
|
||||
->get();
|
||||
```
|
||||
|
||||
## Nullable Filter
|
||||
|
||||
You can mark a filter nullable if you want to retrieve entries whose filtered value is null. This way you can apply the filter with an empty value, as shown in the example.
|
||||
|
||||
```php
|
||||
// GET /user?filter[name]=&filter[permission]=
|
||||
|
||||
QueryBuilder::for(User::class)
|
||||
->allowedFilters([
|
||||
AllowedFilter::exact('name')->nullable(),
|
||||
AllowedFilter::scope('permission')->nullable(),
|
||||
])
|
||||
->get();
|
||||
```
|
||||
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
---
|
||||
title: Including relationships
|
||||
weight: 3
|
||||
---
|
||||
|
||||
The `include` query parameter will load any Eloquent relation or relation count on the resulting models.
|
||||
All includes must be explicitly allowed using `allowedIncludes()`. This method takes an array of relationship names or `AllowedInclude` instances.
|
||||
|
||||
## Basic usage
|
||||
|
||||
```php
|
||||
// GET /users?include=posts
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedIncludes(['posts'])
|
||||
->get();
|
||||
|
||||
// $users will have all their their `posts()` related models loaded
|
||||
```
|
||||
|
||||
You can load multiple relationships by separating them with a comma:
|
||||
|
||||
```php
|
||||
// GET /users?include=posts,permissions
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedIncludes(['posts', 'permissions'])
|
||||
->get();
|
||||
|
||||
// $users will contain all users with their posts and permissions loaded
|
||||
```
|
||||
|
||||
## Default includes
|
||||
|
||||
There is no way to include relationships by default in this package. Default relationships are built-in to Laravel itself using the `with()` method on a query:
|
||||
|
||||
```php
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedIncludes(['friends'])
|
||||
->with('posts') // posts will always by included, friends can be requested
|
||||
->withCount('posts')
|
||||
->withExists('posts')
|
||||
->get();
|
||||
```
|
||||
|
||||
## Disallowed includes
|
||||
|
||||
When trying to include relationships that have not been allowed using `allowedIncludes()` an `InvalidIncludeQuery` exception will be thrown. Its exception message contains the allowed includes for reference.
|
||||
|
||||
## Nested relationships
|
||||
|
||||
You can load nested relationships using the dot `.` notation:
|
||||
|
||||
```php
|
||||
// GET /users?include=posts.comments,permissions
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedIncludes(['posts.comments', 'permissions'])
|
||||
->get();
|
||||
|
||||
// $users will contain all users with their posts, comments on their posts and permissions loaded
|
||||
```
|
||||
|
||||
## Including related model count
|
||||
|
||||
Every allowed include will automatically allow requesting its related model count using a `Count` suffix. On top of that it's also possible to specifically allow requesting and querying the related model count (and not include the entire relationship).
|
||||
|
||||
Under the hood this uses Laravel's `withCount method`. [Read more about the `withCount` method here](https://laravel.com/docs/master/eloquent-relationships#counting-related-models).
|
||||
|
||||
```php
|
||||
// GET /users?include=postsCount,friendsCount
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedIncludes([
|
||||
'posts', // allows including `posts` or `postsCount` or `postsExists`
|
||||
AllowedInclude::count('friendsCount'), // only allows include the number of `friends()` related models
|
||||
]);
|
||||
// every user in $users will contain a `posts_count` and `friends_count` property
|
||||
```
|
||||
|
||||
## Including related model exists
|
||||
|
||||
Every allowed include will automatically allow requesting its related model exists using a `Exists` suffix. On top of that it's also possible to specifically allow requesting and querying the related model exists (and not include the entire relationship).
|
||||
|
||||
Under the hood this uses Laravel's `withExists method`. [Read more about the `withExists` method here](https://laravel.com/docs/master/eloquent-relationships#other-aggregate-functions).
|
||||
|
||||
```php
|
||||
// GET /users?include=postsExists,friendsExists
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedIncludes([
|
||||
'posts', // allows including `posts` or `postsCount` or `postsExists`
|
||||
AllowedInclude::exists('friendsExists'), // only allows include the existence of `friends()` related models
|
||||
]);
|
||||
// every user in $users will contain a `posts_exists` and `friends_exists` property
|
||||
```
|
||||
|
||||
## Include aliases
|
||||
|
||||
It can be useful to specify an alias for an include to enable friendly relationship names. For example, your users table might have a `userProfile` relationship, which might be neater just specified as `profile`. Using aliases you can specify a new, shorter name for this include:
|
||||
|
||||
```php
|
||||
use Spatie\QueryBuilder\AllowedInclude;
|
||||
|
||||
// GET /users?include=profile
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedIncludes(AllowedInclude::relationship('profile', 'userProfile')) // will include the `userProfile` relationship
|
||||
->get();
|
||||
```
|
||||
|
||||
## Custom includes
|
||||
|
||||
You can specify custom includes using the `AllowedInclude::custom()` method. Custom includes are instances of invokable classes that implement the `\Spatie\QueryBuilder\Includes\IncludeInterface` interface. The `__invoke` method will receive the current query builder instance and the include name. This way you can build any query your heart desires.
|
||||
|
||||
For example:
|
||||
|
||||
```php
|
||||
use Spatie\QueryBuilder\Includes\IncludeInterface;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Models\Post;
|
||||
|
||||
class AggregateInclude implements IncludeInterface
|
||||
{
|
||||
protected string $column;
|
||||
|
||||
protected string $function;
|
||||
|
||||
public function __construct(string $column, string $function)
|
||||
{
|
||||
$this->column = $column;
|
||||
|
||||
$this->function = $function;
|
||||
}
|
||||
|
||||
public function __invoke(Builder $query, string $relations)
|
||||
{
|
||||
$query->withAggregate($relations, $this->column, $this->function);
|
||||
}
|
||||
}
|
||||
|
||||
// In your controller for the following request:
|
||||
// GET /posts?include=comments_sum_votes
|
||||
|
||||
$posts = QueryBuilder::for(Post::class)
|
||||
->allowedIncludes([
|
||||
AllowedInclude::custom('comments_sum_votes', new AggregateInclude('votes', 'sum'), 'comments'),
|
||||
])
|
||||
->get();
|
||||
|
||||
// every post in $posts will contain a `comments_sum_votes` property
|
||||
```
|
||||
|
||||
## Callback includes
|
||||
|
||||
If you want to define a tiny custom include, you can use a callback include. Using `AllowedInclude::callback(string $name, Closure $callback, ?string $internalName = null)` you can specify a Closure that will be executed when the includes is requested.
|
||||
|
||||
You can modify the `Builder` object to add your own query constraints.
|
||||
|
||||
For example:
|
||||
|
||||
```php
|
||||
QueryBuilder::for(User::class)
|
||||
->allowedIncludes([
|
||||
AllowedInclude::callback('latest_post', function (Builder $query) {
|
||||
$query->latestOfMany();
|
||||
}),
|
||||
]);
|
||||
```
|
||||
|
||||
## Selecting included fields
|
||||
|
||||
You can select only some fields to be included using the [`allowedFields` method on the query builder](https://spatie.be/docs/laravel-query-builder/v5/features/selecting-fields/).
|
||||
|
||||
⚠️ `allowedFields` must be called before `allowedIncludes`. Otherwise the query builder wont know what fields to include for the requested includes and an exception will be thrown.
|
||||
|
||||
## Include casing
|
||||
|
||||
Relation/include names will be passed from request URL to the query directly. This means `/users?include=blog-posts` will try to load `blog-posts` relationship and `/users?include=blogPosts` will try to load the `blogPosts()` relationship.
|
||||
|
||||
## Eloquent API resources
|
||||
|
||||
Once the relationships are included, we'd recommend including them in your response by using [Eloquent API resources and conditional relationships](https://laravel.com/docs/master/eloquent-resources#conditional-relationships).
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Selecting fields
|
||||
weight: 4
|
||||
---
|
||||
|
||||
Sometimes you'll want to fetch only a couple fields to reduce the overall size of your SQL query. This can be done by specifying some fields using the `allowedFields` method and using the `fields` request query parameter.
|
||||
|
||||
## Basic usage
|
||||
|
||||
The following example fetches only the users' `id` and `name`:
|
||||
|
||||
```
|
||||
GET /users?fields[users]=id,name
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedFields(['id', 'name'])
|
||||
->toSql();
|
||||
```
|
||||
|
||||
The SQL query will look like this:
|
||||
|
||||
```sql
|
||||
SELECT "id", "name" FROM "users"
|
||||
```
|
||||
|
||||
When not allowing any fields explicitly, Eloquent's default behaviour of selecting all fields will be used.
|
||||
|
||||
## Disallowed fields/selects
|
||||
|
||||
When trying to select a column that's not specified in `allowedFields()` an `InvalidFieldQuery` exception will be thrown:
|
||||
|
||||
```php
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedFields('name')
|
||||
->get();
|
||||
|
||||
// GET /users?fields[users]=email will throw an `InvalidFieldQuery` exception as `email` is not an allowed field.
|
||||
```
|
||||
|
||||
## Selecting fields for included relations
|
||||
|
||||
Selecting fields for included models works the same way. This is especially useful when you only need a couple of columns from an included relationship. Consider the following example:
|
||||
|
||||
```php
|
||||
GET /posts?include=author&fields[author]=id,name
|
||||
|
||||
QueryBuilder::for(Post::class)
|
||||
->allowedFields('author.id', 'author.name')
|
||||
->allowedIncludes('author');
|
||||
|
||||
// All posts will be fetched including _only_ the name of the author.
|
||||
```
|
||||
|
||||
⚠️ Keep in mind that the fields query will completely override the `SELECT` part of the query. This means that you'll need to manually specify any columns required for Eloquent relationships to work, in the above example `author.id`. See issue #175 as well.
|
||||
|
||||
⚠️ `allowedFields` must be called before `allowedIncludes`. Otherwise the query builder won't know what fields to include for the requested includes and an exception will be thrown.
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
---
|
||||
title: Sorting
|
||||
weight: 2
|
||||
---
|
||||
|
||||
The `sort` query parameter is used to determine by which property the results collection will be ordered. Sorting is ascending by default and can be reversed by adding a hyphen (`-`) to the start of the property name.
|
||||
|
||||
All sorts have to be explicitly allowed by passing an array to the `allowedSorts()` method. The `allowedSorts` method takes an array of column names as strings or instances of `AllowedSorts`.
|
||||
|
||||
For more advanced use cases, [custom sorts](#content-custom-sorts) can be used.
|
||||
|
||||
## Basic usage
|
||||
|
||||
```php
|
||||
// GET /users?sort=-name
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedSorts('name')
|
||||
->get();
|
||||
|
||||
// $users will be sorted by name and descending (Z -> A)
|
||||
```
|
||||
|
||||
To define a default sort parameter that should be applied without explicitly adding it to the request, you can use the `defaultSort` method.
|
||||
|
||||
```php
|
||||
// GET /users
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->defaultSort('name')
|
||||
->allowedSorts('name', 'street')
|
||||
->get();
|
||||
|
||||
// Will retrieve the users sorted by name
|
||||
```
|
||||
|
||||
You can use `-` if you want to have the default order sorted descendingly.
|
||||
|
||||
```php
|
||||
// GET /users
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->defaultSort('-name')
|
||||
->allowedSorts('name', 'street')
|
||||
->get();
|
||||
|
||||
// Will retrieve the users sorted descendingly by name
|
||||
```
|
||||
|
||||
You can define multiple default sorts
|
||||
|
||||
```php
|
||||
// GET /users
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->defaultSort('-street', 'name')
|
||||
->allowedSorts('name', 'street')
|
||||
->get();
|
||||
|
||||
// Will retrieve the users sorted descendingly by street than in ascending order by name
|
||||
```
|
||||
|
||||
You can sort by multiple properties by separating them with a comma:
|
||||
|
||||
```php
|
||||
// GET /users?sort=name,-street
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedSorts(['name', 'street'])
|
||||
->get();
|
||||
|
||||
// $users will be sorted by name in ascending order with a secondary sort on street in descending order.
|
||||
```
|
||||
|
||||
## Disallowed sorts
|
||||
|
||||
When trying to sort by a property that's not specified in `allowedSorts()` an `InvalidSortQuery` exception will be thrown.
|
||||
|
||||
```php
|
||||
// GET /users?sort=password
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedSorts(['name'])
|
||||
->get();
|
||||
|
||||
// Will throw an `InvalidSortQuery` exception as `password` is not an allowed sorting property
|
||||
```
|
||||
|
||||
## Custom sorts
|
||||
|
||||
You can specify custom sorting methods using the `AllowedSort::custom()` method. Custom sorts are instances of invokable classes that implement the `\Spatie\QueryBuilder\Sorts\Sort` interface. The `__invoke` method will receive the current query builder instance, the direction to sort in and the sort's name. This way you can build any sorting query your heart desires.
|
||||
|
||||
For example sorting by string column length:
|
||||
|
||||
```php
|
||||
class StringLengthSort implements \Spatie\QueryBuilder\Sorts\Sort
|
||||
{
|
||||
public function __invoke(Builder $query, bool $descending, string $property)
|
||||
{
|
||||
$direction = $descending ? 'DESC' : 'ASC';
|
||||
|
||||
$query->orderByRaw("LENGTH(`{$property}`) {$direction}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The custom `StringLengthSort` sort class can then be used like this to sort by the length of the `users.name` column:
|
||||
|
||||
```php
|
||||
// GET /users?sort=name-length
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedSorts([
|
||||
AllowedSort::custom('name-length', new StringLengthSort(), 'name'),
|
||||
])
|
||||
->get();
|
||||
|
||||
// The requested `name-length` sort alias will invoke `StringLengthSort` with the `name` column name.
|
||||
```
|
||||
|
||||
To change the default direction of the a sort you can use `defaultDirection` :
|
||||
|
||||
```php
|
||||
$customSort = AllowedSort::custom('custom-sort', new SentSort())->defaultDirection(SortDirection::DESCENDING);
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedSorts($customSort)
|
||||
->defaultSort($customSort)
|
||||
->get();
|
||||
```
|
||||
|
||||
## Using an alias for sorting
|
||||
|
||||
There may be occasions where it is not appropriate to expose the column name to the user.
|
||||
|
||||
Similar to using an alias when filtering, you can do this for sorts as well.
|
||||
|
||||
The column name can be passed as optional parameter and defaults to the property string.
|
||||
|
||||
```php
|
||||
// GET /users?sort=-street
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedSorts([
|
||||
AllowedSort::field('street', 'actual_column_street'),
|
||||
])
|
||||
->get();
|
||||
```
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: Installation & setup
|
||||
weight: 4
|
||||
---
|
||||
|
||||
You can install the package via composer:
|
||||
|
||||
```bash
|
||||
composer require spatie/laravel-query-builder
|
||||
```
|
||||
|
||||
The package will automatically register its service provider.
|
||||
|
||||
You can optionally publish the config file with:
|
||||
```bash
|
||||
php artisan vendor:publish --provider="Spatie\QueryBuilder\QueryBuilderServiceProvider" --tag="query-builder-config"
|
||||
```
|
||||
|
||||
These are the contents of the default config file that will be published:
|
||||
|
||||
```php
|
||||
return [
|
||||
|
||||
/*
|
||||
* By default the package will use the `include`, `filter`, `sort`
|
||||
* and `fields` query parameters as described in the readme.
|
||||
*
|
||||
* You can customize these query string parameters here.
|
||||
*/
|
||||
'parameters' => [
|
||||
'include' => 'include',
|
||||
|
||||
'filter' => 'filter',
|
||||
|
||||
'sort' => 'sort',
|
||||
|
||||
'fields' => 'fields',
|
||||
|
||||
'append' => 'append',
|
||||
],
|
||||
|
||||
/*
|
||||
* Related model counts are included using the relationship name suffixed with this string.
|
||||
* For example: GET /users?include=postsCount
|
||||
*/
|
||||
'count_suffix' => 'Count',
|
||||
|
||||
/*
|
||||
* Related model exists are included using the relationship name suffixed with this string.
|
||||
* For example: GET /users?include=postsExists
|
||||
*/
|
||||
'exists_suffix' => 'Exists',
|
||||
|
||||
/*
|
||||
* By default the package will throw an `InvalidFilterQuery` exception when a filter in the
|
||||
* URL is not allowed in the `allowedFilters()` method.
|
||||
*/
|
||||
'disable_invalid_filter_query_exception' => false,
|
||||
|
||||
/*
|
||||
* By default the package will throw an `InvalidSortQuery` exception when a sort in the
|
||||
* URL is not allowed in the `allowedSorts()` method.
|
||||
*/
|
||||
'disable_invalid_sort_query_exception' => false,
|
||||
|
||||
/*
|
||||
* By default the package will throw an `InvalidIncludeQuery` exception when an include in the
|
||||
* URL is not allowed in the `allowedIncludes()` method.
|
||||
*/
|
||||
'disable_invalid_includes_query_exception' => false,
|
||||
|
||||
/*
|
||||
* By default, the package expects relationship names to be snake case plural when using fields[relationship].
|
||||
* For example, fetching the id and name for a userOwner relation would look like this:
|
||||
* GET /users?fields[user_owner]=id,name
|
||||
*
|
||||
* Set this to `false` if you don't want that and keep the requested relationship names as-is and allows you to
|
||||
* request the fields using a camelCase relationship name:
|
||||
* GET /users?fields[userOwner]=id,name
|
||||
*/
|
||||
'convert_relation_names_to_snake_case_plural' => true,
|
||||
];
|
||||
```
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: Introduction
|
||||
weight: 1
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
Here's how we use the package ourselves in [Mailcoach](https://mailcoach.app).
|
||||
|
||||
<iframe src="https://player.vimeo.com/video/380520777" width="640" height="360" frameborder="0" allow="autoplay; fullscreen" allowfullscreen></iframe>
|
||||
|
||||
## Basic usage
|
||||
|
||||
### Filter a query based on a request: `/users?filter[name]=John`:
|
||||
|
||||
```php
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedFilters('name')
|
||||
->get();
|
||||
|
||||
// all `User`s that contain the string "John" in their name
|
||||
```
|
||||
|
||||
[Read more about filtering features like: partial filters, exact filters, scope filters, custom filters, ignored values, default filter values, ...](https://spatie.be/docs/laravel-query-builder/v5/features/filtering/)
|
||||
|
||||
### Including relations based on a request: `/users?include=posts`:
|
||||
|
||||
```php
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedIncludes('posts')
|
||||
->get();
|
||||
|
||||
// all `User`s with their `posts` loaded
|
||||
```
|
||||
|
||||
[Read more about include features like: including nested relationships, including relationship count, ...](https://spatie.be/docs/laravel-query-builder/v5/features/including-relationships/)
|
||||
|
||||
### Sorting a query based on a request: `/users?sort=id`:
|
||||
|
||||
```php
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedSorts('id')
|
||||
->get();
|
||||
|
||||
// all `User`s sorted by ascending id
|
||||
```
|
||||
|
||||
[Read more about sorting features like: custom sorts, sort direction, ...](https://spatie.be/docs/laravel-query-builder/v5/features/sorting/)
|
||||
|
||||
### Works together nicely with existing queries:
|
||||
|
||||
```php
|
||||
$query = User::where('active', true);
|
||||
|
||||
$userQuery = QueryBuilder::for($query) // start from an existing Builder instance
|
||||
->withTrashed() // use your existing scopes
|
||||
->allowedIncludes('posts', 'permissions')
|
||||
->where('score', '>', 42); // chain on any of Laravel's query builder methods
|
||||
```
|
||||
|
||||
### Selecting fields for a query: `/users?fields=id,email`
|
||||
|
||||
```php
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedFields(['id', 'email'])
|
||||
->get();
|
||||
|
||||
// the fetched `User`s will only have their id & email set
|
||||
```
|
||||
|
||||
[Read more about selecting fields.](https://spatie.be/docs/laravel-query-builder/v5/features/selecting-fields/)
|
||||
|
||||
## We have badges!
|
||||
|
||||
[](https://packagist.org/packages/spatie/laravel-query-builder)
|
||||
[](https://circleci.com/gh/spatie/laravel-query-builder)
|
||||
[](https://styleci.io/repos/117567334)
|
||||
[](https://scrutinizer-ci.com/g/spatie/laravel-query-builder)
|
||||
[](https://packagist.org/packages/spatie/laravel-query-builder)
|
||||
|
||||

|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Questions and issues
|
||||
weight: 5
|
||||
---
|
||||
|
||||
Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving the Laravel query builder? Feel free to [create an issue on GitHub](https://github.com/spatie/laravel-query-builder/issues), we'll try to address it as soon as possible.
|
||||
|
||||
If you've found a bug regarding security please mail [freek@spatie.be](mailto:freek@spatie.be) instead of using the issue tracker.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Requirements
|
||||
weight: 3
|
||||
---
|
||||
|
||||
The Query Builder package requires **PHP 8 or above** and **Laravel 9 or above**.
|
||||
|
||||
We only support and maintain the latest version. If you do not meet the minimum requirements, you can opt to use an older version of the package.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Support us
|
||||
weight: 4
|
||||
---
|
||||
|
||||
We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
|
||||
|
||||
We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\QueryBuilder\Filters\Filter;
|
||||
use Spatie\QueryBuilder\Filters\FiltersBeginsWithStrict;
|
||||
use Spatie\QueryBuilder\Filters\FiltersCallback;
|
||||
use Spatie\QueryBuilder\Filters\FiltersEndsWithStrict;
|
||||
use Spatie\QueryBuilder\Filters\FiltersExact;
|
||||
use Spatie\QueryBuilder\Filters\FiltersPartial;
|
||||
use Spatie\QueryBuilder\Filters\FiltersScope;
|
||||
use Spatie\QueryBuilder\Filters\FiltersTrashed;
|
||||
|
||||
class AllowedFilter
|
||||
{
|
||||
/** @var Filter */
|
||||
protected $filterClass;
|
||||
|
||||
/** @var string */
|
||||
protected $name;
|
||||
|
||||
/** @var string */
|
||||
protected $internalName;
|
||||
|
||||
/** @var \Illuminate\Support\Collection */
|
||||
protected $ignored;
|
||||
|
||||
/** @var mixed */
|
||||
protected $default;
|
||||
|
||||
/** @var bool */
|
||||
protected $hasDefault = false;
|
||||
|
||||
/** @var bool */
|
||||
protected $nullable = false;
|
||||
|
||||
public function __construct(string $name, Filter $filterClass, ?string $internalName = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
$this->filterClass = $filterClass;
|
||||
|
||||
$this->ignored = Collection::make();
|
||||
|
||||
$this->internalName = $internalName ?? $name;
|
||||
}
|
||||
|
||||
public function filter(QueryBuilder $query, $value)
|
||||
{
|
||||
$valueToFilter = $this->resolveValueForFiltering($value);
|
||||
|
||||
if (! $this->nullable && is_null($valueToFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
($this->filterClass)($query->getEloquentBuilder(), $valueToFilter, $this->internalName);
|
||||
}
|
||||
|
||||
public static function setFilterArrayValueDelimiter(string $delimiter = null): void
|
||||
{
|
||||
if (isset($delimiter)) {
|
||||
QueryBuilderRequest::setFilterArrayValueDelimiter($delimiter);
|
||||
}
|
||||
}
|
||||
|
||||
public static function exact(string $name, ?string $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self
|
||||
{
|
||||
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
|
||||
|
||||
return new static($name, new FiltersExact($addRelationConstraint), $internalName);
|
||||
}
|
||||
|
||||
public static function partial(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self
|
||||
{
|
||||
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
|
||||
|
||||
return new static($name, new FiltersPartial($addRelationConstraint), $internalName);
|
||||
}
|
||||
|
||||
public static function beginsWithStrict(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self
|
||||
{
|
||||
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
|
||||
|
||||
return new static($name, new FiltersBeginsWithStrict($addRelationConstraint), $internalName);
|
||||
}
|
||||
|
||||
public static function endsWithStrict(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self
|
||||
{
|
||||
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
|
||||
|
||||
return new static($name, new FiltersEndsWithStrict($addRelationConstraint), $internalName);
|
||||
}
|
||||
|
||||
public static function scope(string $name, $internalName = null, string $arrayValueDelimiter = null): self
|
||||
{
|
||||
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
|
||||
|
||||
return new static($name, new FiltersScope(), $internalName);
|
||||
}
|
||||
|
||||
public static function callback(string $name, $callback, $internalName = null, string $arrayValueDelimiter = null): self
|
||||
{
|
||||
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
|
||||
|
||||
return new static($name, new FiltersCallback($callback), $internalName);
|
||||
}
|
||||
|
||||
public static function trashed(string $name = 'trashed', $internalName = null): self
|
||||
{
|
||||
return new static($name, new FiltersTrashed(), $internalName);
|
||||
}
|
||||
|
||||
public static function custom(string $name, Filter $filterClass, $internalName = null, string $arrayValueDelimiter = null): self
|
||||
{
|
||||
static::setFilterArrayValueDelimiter($arrayValueDelimiter);
|
||||
|
||||
return new static($name, $filterClass, $internalName);
|
||||
}
|
||||
|
||||
public function getFilterClass(): Filter
|
||||
{
|
||||
return $this->filterClass;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function isForFilter(string $filterName): bool
|
||||
{
|
||||
return $this->name === $filterName;
|
||||
}
|
||||
|
||||
public function ignore(...$values): self
|
||||
{
|
||||
$this->ignored = $this->ignored
|
||||
->merge($values)
|
||||
->flatten();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIgnored(): array
|
||||
{
|
||||
return $this->ignored->toArray();
|
||||
}
|
||||
|
||||
public function getInternalName(): string
|
||||
{
|
||||
return $this->internalName;
|
||||
}
|
||||
|
||||
public function default($value): self
|
||||
{
|
||||
$this->hasDefault = true;
|
||||
$this->default = $value;
|
||||
|
||||
if (is_null($value)) {
|
||||
$this->nullable(true);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDefault()
|
||||
{
|
||||
return $this->default;
|
||||
}
|
||||
|
||||
public function hasDefault(): bool
|
||||
{
|
||||
return $this->hasDefault;
|
||||
}
|
||||
|
||||
public function nullable(bool $nullable = true): self
|
||||
{
|
||||
$this->nullable = $nullable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function unsetDefault(): self
|
||||
{
|
||||
$this->hasDefault = false;
|
||||
unset($this->default);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function resolveValueForFiltering($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$remainingProperties = array_map([$this, 'resolveValueForFiltering'], $value);
|
||||
|
||||
return ! empty($remainingProperties) ? $remainingProperties : null;
|
||||
}
|
||||
|
||||
return ! $this->ignored->contains($value) ? $value : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\QueryBuilder\Includes\IncludedCallback;
|
||||
use Spatie\QueryBuilder\Includes\IncludedCount;
|
||||
use Spatie\QueryBuilder\Includes\IncludedExists;
|
||||
use Spatie\QueryBuilder\Includes\IncludedRelationship;
|
||||
use Spatie\QueryBuilder\Includes\IncludeInterface;
|
||||
|
||||
class AllowedInclude
|
||||
{
|
||||
/** @var string */
|
||||
protected $name;
|
||||
|
||||
/** @var IncludeInterface */
|
||||
protected $includeClass;
|
||||
|
||||
/** @var string|null */
|
||||
protected $internalName;
|
||||
|
||||
public function __construct(string $name, IncludeInterface $includeClass, ?string $internalName = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->includeClass = $includeClass;
|
||||
$this->internalName = $internalName ?? $this->name;
|
||||
}
|
||||
|
||||
public static function relationship(string $name, ?string $internalName = null): Collection
|
||||
{
|
||||
$internalName = $internalName ?? $name;
|
||||
|
||||
return IncludedRelationship::getIndividualRelationshipPathsFromInclude($internalName)
|
||||
->zip(IncludedRelationship::getIndividualRelationshipPathsFromInclude($name))
|
||||
->flatMap(function ($args): Collection {
|
||||
[$relationship, $alias] = $args;
|
||||
|
||||
$includes = collect([
|
||||
new self($alias, new IncludedRelationship(), $relationship),
|
||||
]);
|
||||
|
||||
if (! Str::contains($relationship, '.')) {
|
||||
$countSuffix = config('query-builder.count_suffix', 'Count');
|
||||
$existsSuffix = config('query-builder.exists_suffix', 'Exists');
|
||||
|
||||
$includes = $includes
|
||||
->merge(self::count(
|
||||
$alias.$countSuffix,
|
||||
$relationship.$countSuffix
|
||||
))
|
||||
->merge(self::exists(
|
||||
$alias.$existsSuffix,
|
||||
$relationship.$existsSuffix
|
||||
));
|
||||
}
|
||||
|
||||
return $includes;
|
||||
});
|
||||
}
|
||||
|
||||
public static function count(string $name, ?string $internalName = null): Collection
|
||||
{
|
||||
return collect([
|
||||
new static($name, new IncludedCount(), $internalName),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function exists(string $name, ?string $internalName = null): Collection
|
||||
{
|
||||
return collect([
|
||||
new static($name, new IncludedExists(), $internalName),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function callback(string $name, Closure $callback, ?string $internalName = null): Collection
|
||||
{
|
||||
return collect([
|
||||
new static($name, new IncludedCallback($callback), $internalName),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function custom(string $name, IncludeInterface $includeClass, ?string $internalName = null): Collection
|
||||
{
|
||||
return collect([
|
||||
new static($name, $includeClass, $internalName),
|
||||
]);
|
||||
}
|
||||
|
||||
public function include(QueryBuilder $query): void
|
||||
{
|
||||
if (property_exists($this->includeClass, 'getRequestedFieldsForRelatedTable')) {
|
||||
$this->includeClass->getRequestedFieldsForRelatedTable = function (...$args) use ($query) {
|
||||
return $query->getRequestedFieldsForRelatedTable(...$args);
|
||||
};
|
||||
}
|
||||
|
||||
($this->includeClass)($query->getEloquentBuilder(), $this->internalName);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function isForInclude(string $includeName): bool
|
||||
{
|
||||
return $this->name === $includeName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder;
|
||||
|
||||
use Spatie\QueryBuilder\Enums\SortDirection;
|
||||
use Spatie\QueryBuilder\Exceptions\InvalidDirection;
|
||||
use Spatie\QueryBuilder\Sorts\Sort;
|
||||
use Spatie\QueryBuilder\Sorts\SortsCallback;
|
||||
use Spatie\QueryBuilder\Sorts\SortsField;
|
||||
|
||||
class AllowedSort
|
||||
{
|
||||
/** @var \Spatie\QueryBuilder\Sorts\Sort */
|
||||
protected $sortClass;
|
||||
|
||||
/** @var string */
|
||||
protected $name;
|
||||
|
||||
/** @var string */
|
||||
protected $defaultDirection;
|
||||
|
||||
/** @var string */
|
||||
protected $internalName;
|
||||
|
||||
public function __construct(string $name, Sort $sortClass, ?string $internalName = null)
|
||||
{
|
||||
$this->name = ltrim($name, '-');
|
||||
|
||||
$this->sortClass = $sortClass;
|
||||
|
||||
$this->defaultDirection = static::parseSortDirection($name);
|
||||
|
||||
$this->internalName = $internalName ?? $this->name;
|
||||
}
|
||||
|
||||
public static function parseSortDirection(string $name): string
|
||||
{
|
||||
return strpos($name, '-') === 0 ? SortDirection::DESCENDING : SortDirection::ASCENDING;
|
||||
}
|
||||
|
||||
public function sort(QueryBuilder $query, ?bool $descending = null): void
|
||||
{
|
||||
$descending = $descending ?? ($this->defaultDirection === SortDirection::DESCENDING);
|
||||
|
||||
($this->sortClass)($query->getEloquentBuilder(), $descending, $this->internalName);
|
||||
}
|
||||
|
||||
public static function field(string $name, ?string $internalName = null): self
|
||||
{
|
||||
return new static($name, new SortsField(), $internalName);
|
||||
}
|
||||
|
||||
public static function custom(string $name, Sort $sortClass, ?string $internalName = null): self
|
||||
{
|
||||
return new static($name, $sortClass, $internalName);
|
||||
}
|
||||
|
||||
public static function callback(string $name, $callback, ?string $internalName = null): self
|
||||
{
|
||||
return new static($name, new SortsCallback($callback), $internalName);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function isSort(string $sortName): bool
|
||||
{
|
||||
return $this->name === $sortName;
|
||||
}
|
||||
|
||||
public function getInternalName(): string
|
||||
{
|
||||
return $this->internalName;
|
||||
}
|
||||
|
||||
public function defaultDirection(string $defaultDirection)
|
||||
{
|
||||
if (! in_array($defaultDirection, [
|
||||
SortDirection::ASCENDING,
|
||||
SortDirection::DESCENDING,
|
||||
])) {
|
||||
throw InvalidDirection::make($defaultDirection);
|
||||
}
|
||||
|
||||
$this->defaultDirection = $defaultDirection;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Concerns;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\QueryBuilder\Exceptions\AllowedFieldsMustBeCalledBeforeAllowedIncludes;
|
||||
use Spatie\QueryBuilder\Exceptions\InvalidFieldQuery;
|
||||
use Spatie\QueryBuilder\Exceptions\UnknownIncludedFieldsQuery;
|
||||
|
||||
trait AddsFieldsToQuery
|
||||
{
|
||||
protected ?Collection $allowedFields = null;
|
||||
|
||||
public function allowedFields($fields): static
|
||||
{
|
||||
if ($this->allowedIncludes instanceof Collection) {
|
||||
throw new AllowedFieldsMustBeCalledBeforeAllowedIncludes();
|
||||
}
|
||||
|
||||
$fields = is_array($fields) ? $fields : func_get_args();
|
||||
|
||||
$this->allowedFields = collect($fields)
|
||||
->map(function (string $fieldName) {
|
||||
return $this->prependField($fieldName);
|
||||
});
|
||||
|
||||
$this->ensureAllFieldsExist();
|
||||
|
||||
$this->addRequestedModelFieldsToQuery();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function addRequestedModelFieldsToQuery()
|
||||
{
|
||||
$modelTableName = $this->getModel()->getTable();
|
||||
|
||||
$fields = $this->request->fields();
|
||||
|
||||
$modelFields = $fields->has($modelTableName) ? $fields->get($modelTableName) : $fields->get('_');
|
||||
|
||||
if (empty($modelFields)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$prependedFields = $this->prependFieldsWithTableName($modelFields, $modelTableName);
|
||||
|
||||
$this->select($prependedFields);
|
||||
}
|
||||
|
||||
public function getRequestedFieldsForRelatedTable(string $relation): array
|
||||
{
|
||||
$tableOrRelation = config('query-builder.convert_relation_names_to_snake_case_plural', true)
|
||||
? Str::plural(Str::snake($relation))
|
||||
: $relation;
|
||||
|
||||
$fields = $this->request->fields()
|
||||
->mapWithKeys(fn ($fields, $table) => [$table => $fields])
|
||||
->get($tableOrRelation);
|
||||
|
||||
if (! $fields) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (! $this->allowedFields instanceof Collection) {
|
||||
// We have requested fields but no allowed fields (yet?)
|
||||
|
||||
throw new UnknownIncludedFieldsQuery($fields);
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
protected function ensureAllFieldsExist()
|
||||
{
|
||||
$modelTable = $this->getModel()->getTable();
|
||||
|
||||
$requestedFields = $this->request->fields()
|
||||
->map(function ($fields, $model) use ($modelTable) {
|
||||
$tableName = $model;
|
||||
|
||||
return $this->prependFieldsWithTableName($fields, $model === '_' ? $modelTable : $tableName);
|
||||
})
|
||||
->flatten()
|
||||
->unique();
|
||||
|
||||
$unknownFields = $requestedFields->diff($this->allowedFields);
|
||||
|
||||
if ($unknownFields->isNotEmpty()) {
|
||||
throw InvalidFieldQuery::fieldsNotAllowed($unknownFields, $this->allowedFields);
|
||||
}
|
||||
}
|
||||
|
||||
protected function prependFieldsWithTableName(array $fields, string $tableName): array
|
||||
{
|
||||
return array_map(function ($field) use ($tableName) {
|
||||
return $this->prependField($field, $tableName);
|
||||
}, $fields);
|
||||
}
|
||||
|
||||
protected function prependField(string $field, ?string $table = null): string
|
||||
{
|
||||
if (! $table) {
|
||||
$table = $this->getModel()->getTable();
|
||||
}
|
||||
|
||||
if (Str::contains($field, '.')) {
|
||||
// Already prepended
|
||||
|
||||
return $field;
|
||||
}
|
||||
|
||||
return "{$table}.{$field}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Concerns;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\QueryBuilder\AllowedInclude;
|
||||
use Spatie\QueryBuilder\Exceptions\InvalidIncludeQuery;
|
||||
use Spatie\QueryBuilder\Includes\IncludeInterface;
|
||||
|
||||
trait AddsIncludesToQuery
|
||||
{
|
||||
protected ?Collection $allowedIncludes = null;
|
||||
|
||||
public function allowedIncludes($includes): static
|
||||
{
|
||||
$includes = is_array($includes) ? $includes : func_get_args();
|
||||
|
||||
$this->allowedIncludes = collect($includes)
|
||||
->reject(function ($include) {
|
||||
return empty($include);
|
||||
})
|
||||
->flatMap(function ($include): Collection {
|
||||
if ($include instanceof Collection) {
|
||||
return $include;
|
||||
}
|
||||
|
||||
if ($include instanceof IncludeInterface) {
|
||||
return collect([$include]);
|
||||
}
|
||||
|
||||
if (Str::endsWith($include, config('query-builder.count_suffix', 'Count'))) {
|
||||
return AllowedInclude::count($include);
|
||||
}
|
||||
|
||||
if (Str::endsWith($include, config('query-builder.exists_suffix', 'Exists'))) {
|
||||
return AllowedInclude::exists($include);
|
||||
}
|
||||
|
||||
return AllowedInclude::relationship($include);
|
||||
})
|
||||
->unique(function (AllowedInclude $allowedInclude) {
|
||||
return $allowedInclude->getName();
|
||||
});
|
||||
|
||||
$this->ensureAllIncludesExist();
|
||||
|
||||
$includes = $this->filterNonExistingIncludes($this->request->includes());
|
||||
|
||||
$this->addIncludesToQuery($includes);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function addIncludesToQuery(Collection $includes)
|
||||
{
|
||||
$includes->each(function ($include) {
|
||||
$include = $this->findInclude($include);
|
||||
|
||||
$include->include($this);
|
||||
});
|
||||
}
|
||||
|
||||
protected function findInclude(string $include): ?AllowedInclude
|
||||
{
|
||||
return $this->allowedIncludes
|
||||
->first(function (AllowedInclude $included) use ($include) {
|
||||
return $included->isForInclude($include);
|
||||
});
|
||||
}
|
||||
|
||||
protected function ensureAllIncludesExist()
|
||||
{
|
||||
if (config('query-builder.disable_invalid_includes_query_exception', false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$includes = $this->request->includes();
|
||||
|
||||
$allowedIncludeNames = $this->allowedIncludes->map(function (AllowedInclude $allowedInclude) {
|
||||
return $allowedInclude->getName();
|
||||
});
|
||||
|
||||
$diff = $includes->diff($allowedIncludeNames);
|
||||
|
||||
if ($diff->count()) {
|
||||
throw InvalidIncludeQuery::includesNotAllowed($diff, $allowedIncludeNames);
|
||||
}
|
||||
|
||||
// TODO: Check for non-existing relationships?
|
||||
}
|
||||
|
||||
protected function filterNonExistingIncludes(Collection $includes): Collection
|
||||
{
|
||||
if (config('query-builder.disable_invalid_includes_query_exception', false) == false) {
|
||||
return $includes;
|
||||
}
|
||||
|
||||
return $includes->filter(function ($include) {
|
||||
return $this->findInclude($include);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Concerns;
|
||||
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
use Spatie\QueryBuilder\Exceptions\InvalidFilterQuery;
|
||||
|
||||
trait FiltersQuery
|
||||
{
|
||||
/** @var \Illuminate\Support\Collection */
|
||||
protected $allowedFilters;
|
||||
|
||||
public function allowedFilters($filters): static
|
||||
{
|
||||
$filters = is_array($filters) ? $filters : func_get_args();
|
||||
|
||||
$this->allowedFilters = collect($filters)->map(function ($filter) {
|
||||
if ($filter instanceof AllowedFilter) {
|
||||
return $filter;
|
||||
}
|
||||
|
||||
return AllowedFilter::partial($filter);
|
||||
});
|
||||
|
||||
$this->ensureAllFiltersExist();
|
||||
|
||||
$this->addFiltersToQuery();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function addFiltersToQuery()
|
||||
{
|
||||
$this->allowedFilters->each(function (AllowedFilter $filter) {
|
||||
if ($this->isFilterRequested($filter)) {
|
||||
$value = $this->request->filters()->get($filter->getName());
|
||||
$filter->filter($this, $value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($filter->hasDefault()) {
|
||||
$filter->filter($this, $filter->getDefault());
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function findFilter(string $property): ?AllowedFilter
|
||||
{
|
||||
return $this->allowedFilters
|
||||
->first(function (AllowedFilter $filter) use ($property) {
|
||||
return $filter->isForFilter($property);
|
||||
});
|
||||
}
|
||||
|
||||
protected function isFilterRequested(AllowedFilter $allowedFilter): bool
|
||||
{
|
||||
return $this->request->filters()->has($allowedFilter->getName());
|
||||
}
|
||||
|
||||
protected function ensureAllFiltersExist()
|
||||
{
|
||||
if (config('query-builder.disable_invalid_filter_query_exception', false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filterNames = $this->request->filters()->keys();
|
||||
|
||||
$allowedFilterNames = $this->allowedFilters->map(function (AllowedFilter $allowedFilter) {
|
||||
return $allowedFilter->getName();
|
||||
});
|
||||
|
||||
$diff = $filterNames->diff($allowedFilterNames);
|
||||
|
||||
if ($diff->count()) {
|
||||
throw InvalidFilterQuery::filtersNotAllowed($diff, $allowedFilterNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Concerns;
|
||||
|
||||
use Spatie\QueryBuilder\AllowedSort;
|
||||
use Spatie\QueryBuilder\Exceptions\InvalidSortQuery;
|
||||
|
||||
trait SortsQuery
|
||||
{
|
||||
/** @var \Illuminate\Support\Collection */
|
||||
protected $allowedSorts;
|
||||
|
||||
public function allowedSorts($sorts): static
|
||||
{
|
||||
$sorts = is_array($sorts) ? $sorts : func_get_args();
|
||||
|
||||
$this->allowedSorts = collect($sorts)->map(function ($sort) {
|
||||
if ($sort instanceof AllowedSort) {
|
||||
return $sort;
|
||||
}
|
||||
|
||||
return AllowedSort::field(ltrim($sort, '-'));
|
||||
});
|
||||
|
||||
$this->ensureAllSortsExist();
|
||||
|
||||
$this->addRequestedSortsToQuery(); // allowed is known & request is known, add what we can, if there is no request, -wait
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string|\Spatie\QueryBuilder\AllowedSort $sorts
|
||||
*
|
||||
* @return \Spatie\QueryBuilder\QueryBuilder
|
||||
*/
|
||||
public function defaultSort($sorts): static
|
||||
{
|
||||
$sorts = is_array($sorts) ? $sorts : func_get_args();
|
||||
|
||||
return $this->defaultSorts($sorts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string|\Spatie\QueryBuilder\AllowedSort $sorts
|
||||
*
|
||||
* @return \Spatie\QueryBuilder\QueryBuilder
|
||||
*/
|
||||
public function defaultSorts($sorts): static
|
||||
{
|
||||
if ($this->request->sorts()->isNotEmpty()) {
|
||||
// We've got requested sorts. No need to parse defaults.
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$sorts = is_array($sorts) ? $sorts : func_get_args();
|
||||
|
||||
collect($sorts)
|
||||
->map(function ($sort) {
|
||||
if ($sort instanceof AllowedSort) {
|
||||
return $sort;
|
||||
}
|
||||
|
||||
return AllowedSort::field($sort);
|
||||
})
|
||||
->each(function (AllowedSort $sort) {
|
||||
$sort->sort($this);
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function addRequestedSortsToQuery()
|
||||
{
|
||||
$this->request->sorts()
|
||||
->each(function (string $property) {
|
||||
$descending = $property[0] === '-';
|
||||
|
||||
$key = ltrim($property, '-');
|
||||
|
||||
$sort = $this->findSort($key);
|
||||
|
||||
$sort?->sort($this, $descending);
|
||||
});
|
||||
}
|
||||
|
||||
protected function findSort(string $property): ?AllowedSort
|
||||
{
|
||||
return $this->allowedSorts
|
||||
->first(function (AllowedSort $sort) use ($property) {
|
||||
return $sort->isSort($property);
|
||||
});
|
||||
}
|
||||
|
||||
protected function ensureAllSortsExist(): void
|
||||
{
|
||||
if (config('query-builder.disable_invalid_sort_query_exception', false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$requestedSortNames = $this->request->sorts()->map(function (string $sort) {
|
||||
return ltrim($sort, '-');
|
||||
});
|
||||
|
||||
$allowedSortNames = $this->allowedSorts->map(function (AllowedSort $sort) {
|
||||
return $sort->getName();
|
||||
});
|
||||
|
||||
$unknownSorts = $requestedSortNames->diff($allowedSortNames);
|
||||
|
||||
if ($unknownSorts->isNotEmpty()) {
|
||||
throw InvalidSortQuery::sortsNotAllowed($unknownSorts, $allowedSortNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Enums;
|
||||
|
||||
class SortDirection
|
||||
{
|
||||
public const DESCENDING = 'desc';
|
||||
|
||||
public const ASCENDING = 'asc';
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Exceptions;
|
||||
|
||||
use BadMethodCallException;
|
||||
|
||||
class AllowedFieldsMustBeCalledBeforeAllowedIncludes extends BadMethodCallException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct("The QueryBuilder's `allowedFields` method must be called before the `allowedIncludes` method.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Exceptions;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class InvalidAppendQuery extends InvalidQuery
|
||||
{
|
||||
/** @var \Illuminate\Support\Collection */
|
||||
public $appendsNotAllowed;
|
||||
|
||||
/** @var \Illuminate\Support\Collection */
|
||||
public $allowedAppends;
|
||||
|
||||
public function __construct(Collection $appendsNotAllowed, Collection $allowedAppends)
|
||||
{
|
||||
$this->appendsNotAllowed = $appendsNotAllowed;
|
||||
$this->allowedAppends = $allowedAppends;
|
||||
|
||||
$appendsNotAllowed = $appendsNotAllowed->implode(', ');
|
||||
$allowedAppends = $allowedAppends->implode(', ');
|
||||
$message = "Requested append(s) `{$appendsNotAllowed}` are not allowed. Allowed append(s) are `{$allowedAppends}`.";
|
||||
|
||||
parent::__construct(Response::HTTP_BAD_REQUEST, $message);
|
||||
}
|
||||
|
||||
public static function appendsNotAllowed(Collection $appendsNotAllowed, Collection $allowedAppends)
|
||||
{
|
||||
return new static(...func_get_args());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Spatie\QueryBuilder\Enums\SortDirection;
|
||||
|
||||
class InvalidDirection extends Exception
|
||||
{
|
||||
public static function make(string $sort)
|
||||
{
|
||||
return new static('The direction should be either `'.SortDirection::DESCENDING.'` or `'.SortDirection::ASCENDING)."`. {$sort} given.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Exceptions;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class InvalidFieldQuery extends InvalidQuery
|
||||
{
|
||||
/** @var \Illuminate\Support\Collection */
|
||||
public $unknownFields;
|
||||
|
||||
/** @var \Illuminate\Support\Collection */
|
||||
public $allowedFields;
|
||||
|
||||
public function __construct(Collection $unknownFields, Collection $allowedFields)
|
||||
{
|
||||
$this->unknownFields = $unknownFields;
|
||||
$this->allowedFields = $allowedFields;
|
||||
|
||||
$unknownFields = $unknownFields->implode(', ');
|
||||
$allowedFields = $allowedFields->implode(', ');
|
||||
$message = "Requested field(s) `{$unknownFields}` are not allowed. Allowed field(s) are `{$allowedFields}`.";
|
||||
|
||||
parent::__construct(Response::HTTP_BAD_REQUEST, $message);
|
||||
}
|
||||
|
||||
public static function fieldsNotAllowed(Collection $unknownFields, Collection $allowedFields)
|
||||
{
|
||||
return new static(...func_get_args());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Exceptions;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class InvalidFilterQuery extends InvalidQuery
|
||||
{
|
||||
/** @var \Illuminate\Support\Collection */
|
||||
public $unknownFilters;
|
||||
|
||||
/** @var \Illuminate\Support\Collection */
|
||||
public $allowedFilters;
|
||||
|
||||
public function __construct(Collection $unknownFilters, Collection $allowedFilters)
|
||||
{
|
||||
$this->unknownFilters = $unknownFilters;
|
||||
$this->allowedFilters = $allowedFilters;
|
||||
|
||||
$unknownFilters = $this->unknownFilters->implode(', ');
|
||||
$allowedFilters = $this->allowedFilters->implode(', ');
|
||||
$message = "Requested filter(s) `{$unknownFilters}` are not allowed. Allowed filter(s) are `{$allowedFilters}`.";
|
||||
|
||||
parent::__construct(Response::HTTP_BAD_REQUEST, $message);
|
||||
}
|
||||
|
||||
public static function filtersNotAllowed(Collection $unknownFilters, Collection $allowedFilters)
|
||||
{
|
||||
return new static(...func_get_args());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class InvalidFilterValue extends Exception
|
||||
{
|
||||
public static function make($value)
|
||||
{
|
||||
return new static("Filter value `{$value}` is invalid.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Exceptions;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class InvalidIncludeQuery extends InvalidQuery
|
||||
{
|
||||
/** @var \Illuminate\Support\Collection */
|
||||
public $unknownIncludes;
|
||||
|
||||
/** @var \Illuminate\Support\Collection */
|
||||
public $allowedIncludes;
|
||||
|
||||
public function __construct(Collection $unknownIncludes, Collection $allowedIncludes)
|
||||
{
|
||||
$this->unknownIncludes = $unknownIncludes;
|
||||
$this->allowedIncludes = $allowedIncludes;
|
||||
|
||||
$unknownIncludes = $unknownIncludes->implode(', ');
|
||||
|
||||
$message = "Requested include(s) `{$unknownIncludes}` are not allowed. ";
|
||||
|
||||
if ($allowedIncludes->count()) {
|
||||
$allowedIncludes = $allowedIncludes->implode(', ');
|
||||
$message .= "Allowed include(s) are `{$allowedIncludes}`.";
|
||||
} else {
|
||||
$message .= 'There are no allowed includes.';
|
||||
}
|
||||
|
||||
parent::__construct(Response::HTTP_BAD_REQUEST, $message);
|
||||
}
|
||||
|
||||
public static function includesNotAllowed(Collection $unknownIncludes, Collection $allowedIncludes)
|
||||
{
|
||||
return new static(...func_get_args());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Exceptions;
|
||||
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
abstract class InvalidQuery extends HttpException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Exceptions;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class InvalidSortQuery extends InvalidQuery
|
||||
{
|
||||
/** @var \Illuminate\Support\Collection */
|
||||
public $unknownSorts;
|
||||
|
||||
/** @var \Illuminate\Support\Collection */
|
||||
public $allowedSorts;
|
||||
|
||||
public function __construct(Collection $unknownSorts, Collection $allowedSorts)
|
||||
{
|
||||
$this->unknownSorts = $unknownSorts;
|
||||
$this->allowedSorts = $allowedSorts;
|
||||
|
||||
$allowedSorts = $allowedSorts->implode(', ');
|
||||
$unknownSorts = $unknownSorts->implode(', ');
|
||||
$message = "Requested sort(s) `{$unknownSorts}` is not allowed. Allowed sort(s) are `{$allowedSorts}`.";
|
||||
|
||||
parent::__construct(Response::HTTP_BAD_REQUEST, $message);
|
||||
}
|
||||
|
||||
public static function sortsNotAllowed(Collection $unknownSorts, Collection $allowedSorts)
|
||||
{
|
||||
return new static(...func_get_args());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Exceptions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class InvalidSubject extends InvalidArgumentException
|
||||
{
|
||||
public static function make($subject)
|
||||
{
|
||||
return new static(
|
||||
sprintf(
|
||||
'Subject %s is invalid.',
|
||||
is_object($subject)
|
||||
? sprintf('class `%s`', get_class($subject))
|
||||
: sprintf('type `%s`', gettype($subject))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Exceptions;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class UnknownIncludedFieldsQuery extends InvalidQuery
|
||||
{
|
||||
/** @var \Illuminate\Support\Collection */
|
||||
public $unknownFields;
|
||||
|
||||
public function __construct(array $unknownFields)
|
||||
{
|
||||
$this->unknownFields = collect($unknownFields);
|
||||
|
||||
$unknownFields = $this->unknownFields->implode(', ');
|
||||
|
||||
$message = "Requested field(s) `{$unknownFields}` are not allowed (yet). ";
|
||||
$message .= "If you want to allow these fields, please make sure to call the QueryBuilder's `allowedFields` method before the `allowedIncludes` method.";
|
||||
|
||||
parent::__construct(Response::HTTP_BAD_REQUEST, $message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Filters;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @template TModelClass of \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
interface Filter
|
||||
{
|
||||
/**
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TModelClass> $query
|
||||
* @param mixed $value
|
||||
* @param string $property
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function __invoke(Builder $query, $value, string $property);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Filters;
|
||||
|
||||
/**
|
||||
* @template TModelClass of \Illuminate\Database\Eloquent\Model
|
||||
* @template-implements \Spatie\QueryBuilder\Filters\Filter<TModelClass>
|
||||
*/
|
||||
class FiltersBeginsWithStrict extends FiltersPartial implements Filter
|
||||
{
|
||||
protected function getWhereRawParameters($value, string $property, string $driver): array
|
||||
{
|
||||
return [
|
||||
"{$property} LIKE ?".static::maybeSpecifyEscapeChar($driver),
|
||||
[static::escapeLike($value).'%'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Filters;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @template TModelClass of \Illuminate\Database\Eloquent\Model
|
||||
* @template-implements \Spatie\QueryBuilder\Filters\Filter<TModelClass>
|
||||
*/
|
||||
class FiltersCallback implements Filter
|
||||
{
|
||||
/**
|
||||
* @var callable a PHP callback of the following signature:
|
||||
* `function (\Illuminate\Database\Eloquent\Builder $builder, mixed $value, string $property)`
|
||||
*/
|
||||
private $callback;
|
||||
|
||||
public function __construct($callback)
|
||||
{
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function __invoke(Builder $query, $value, string $property)
|
||||
{
|
||||
return call_user_func($this->callback, $query, $value, $property);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Filters;
|
||||
|
||||
/**
|
||||
* @template TModelClass of \Illuminate\Database\Eloquent\Model
|
||||
* @template-implements \Spatie\QueryBuilder\Filters\Filter<TModelClass>
|
||||
*/
|
||||
class FiltersEndsWithStrict extends FiltersPartial implements Filter
|
||||
{
|
||||
protected function getWhereRawParameters($value, string $property, string $driver): array
|
||||
{
|
||||
|
||||
return [
|
||||
"{$property} LIKE ?".static::maybeSpecifyEscapeChar($driver),
|
||||
['%'.static::escapeLike($value)],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Filters;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @template TModelClass of \Illuminate\Database\Eloquent\Model
|
||||
* @template-implements \Spatie\QueryBuilder\Filters\Filter<TModelClass>
|
||||
*/
|
||||
class FiltersExact implements Filter
|
||||
{
|
||||
protected $relationConstraints = [];
|
||||
|
||||
/** @var bool */
|
||||
protected $addRelationConstraint = true;
|
||||
|
||||
public function __construct(bool $addRelationConstraint = true)
|
||||
{
|
||||
$this->addRelationConstraint = $addRelationConstraint;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function __invoke(Builder $query, $value, string $property)
|
||||
{
|
||||
if ($this->addRelationConstraint) {
|
||||
if ($this->isRelationProperty($query, $property)) {
|
||||
$this->withRelationConstraint($query, $value, $property);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$query->whereIn($query->qualifyColumn($property), $value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query->where($query->qualifyColumn($property), '=', $value);
|
||||
}
|
||||
|
||||
protected function isRelationProperty(Builder $query, string $property): bool
|
||||
{
|
||||
if (! Str::contains($property, '.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (in_array($property, $this->relationConstraints)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$firstRelationship = explode('.', $property)[0];
|
||||
|
||||
if (! method_exists($query->getModel(), $firstRelationship)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return is_a($query->getModel()->{$firstRelationship}(), Relation::class);
|
||||
}
|
||||
|
||||
protected function withRelationConstraint(Builder $query, $value, string $property)
|
||||
{
|
||||
[$relation, $property] = collect(explode('.', $property))
|
||||
->pipe(function (Collection $parts) {
|
||||
return [
|
||||
$parts->except(count($parts) - 1)->implode('.'),
|
||||
$parts->last(),
|
||||
];
|
||||
});
|
||||
|
||||
$query->whereHas($relation, function (Builder $query) use ($value, $property) {
|
||||
$this->relationConstraints[] = $property = $query->qualifyColumn($property);
|
||||
|
||||
$this->__invoke($query, $value, $property);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Filters;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @template TModelClass of \Illuminate\Database\Eloquent\Model
|
||||
* @template-implements \Spatie\QueryBuilder\Filters\Filter<TModelClass>
|
||||
*/
|
||||
class FiltersPartial extends FiltersExact implements Filter
|
||||
{
|
||||
/** {@inheritdoc} */
|
||||
public function __invoke(Builder $query, $value, string $property)
|
||||
{
|
||||
if ($this->addRelationConstraint) {
|
||||
if ($this->isRelationProperty($query, $property)) {
|
||||
$this->withRelationConstraint($query, $value, $property);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$wrappedProperty = $query->getQuery()->getGrammar()->wrap($query->qualifyColumn($property));
|
||||
$databaseDriver = $this->getDatabaseDriver($query);
|
||||
|
||||
if (is_array($value)) {
|
||||
if (count(array_filter($value, 'strlen')) === 0) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$query->where(function (Builder $query) use ($databaseDriver, $value, $wrappedProperty) {
|
||||
foreach (array_filter($value, 'strlen') as $partialValue) {
|
||||
[$sql, $bindings] = $this->getWhereRawParameters($partialValue, $wrappedProperty, $databaseDriver);
|
||||
$query->orWhereRaw($sql, $bindings);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
[$sql, $bindings] = $this->getWhereRawParameters($value, $wrappedProperty, $databaseDriver);
|
||||
$query->whereRaw($sql, $bindings);
|
||||
}
|
||||
|
||||
protected function getDatabaseDriver(Builder $query): string
|
||||
{
|
||||
return $query->getConnection()->getDriverName();
|
||||
}
|
||||
|
||||
protected function getWhereRawParameters($value, string $property, string $driver): array
|
||||
{
|
||||
$value = mb_strtolower((string) $value, 'UTF8');
|
||||
|
||||
return [
|
||||
"LOWER({$property}) LIKE ?".self::maybeSpecifyEscapeChar($driver),
|
||||
['%'.self::escapeLike($value).'%'],
|
||||
];
|
||||
}
|
||||
|
||||
protected static function escapeLike(string $value): string
|
||||
{
|
||||
return str_replace(
|
||||
['\\', '_', '%'],
|
||||
['\\\\', '\\_', '\\%'],
|
||||
$value,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param 'sqlite'|'pgsql'|'sqlsrc'|'mysql' $driver
|
||||
* @return string
|
||||
*/
|
||||
protected static function maybeSpecifyEscapeChar(string $driver): string
|
||||
{
|
||||
if(! in_array($driver, ['sqlite','pgsql','sqlsrv'])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return " ESCAPE '\'";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Filters;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use ReflectionObject;
|
||||
use ReflectionParameter;
|
||||
use ReflectionUnionType;
|
||||
use Spatie\QueryBuilder\Exceptions\InvalidFilterValue;
|
||||
|
||||
/**
|
||||
* @template TModelClass of \Illuminate\Database\Eloquent\Model
|
||||
* @template-implements \Spatie\QueryBuilder\Filters\Filter<TModelClass>
|
||||
*/
|
||||
class FiltersScope implements Filter
|
||||
{
|
||||
/** {@inheritdoc} */
|
||||
public function __invoke(Builder $query, $values, string $property): Builder
|
||||
{
|
||||
$propertyParts = collect(explode('.', $property));
|
||||
|
||||
$scope = Str::camel($propertyParts->pop()); // TODO: Make this configurable?
|
||||
|
||||
$values = array_values(Arr::wrap($values));
|
||||
$values = $this->resolveParameters($query, $values, $scope);
|
||||
|
||||
$relation = $propertyParts->implode('.');
|
||||
|
||||
if ($relation) {
|
||||
return $query->whereHas($relation, function (Builder $query) use (
|
||||
$scope,
|
||||
$values
|
||||
) {
|
||||
return $query->$scope(...$values);
|
||||
});
|
||||
}
|
||||
|
||||
return $query->$scope(...$values);
|
||||
}
|
||||
|
||||
protected function resolveParameters(Builder $query, $values, string $scope): array
|
||||
{
|
||||
try {
|
||||
$parameters = (new ReflectionObject($query->getModel()))
|
||||
->getMethod('scope' . ucfirst($scope))
|
||||
->getParameters();
|
||||
} catch (ReflectionException $e) {
|
||||
return $values;
|
||||
}
|
||||
|
||||
foreach ($parameters as $parameter) {
|
||||
if (! optional($this->getClass($parameter))->isSubclassOf(Model::class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$model = $this->getClass($parameter)->newInstance();
|
||||
$index = $parameter->getPosition() - 1;
|
||||
$value = $values[$index];
|
||||
|
||||
$result = $model->resolveRouteBinding($value);
|
||||
|
||||
if ($result === null) {
|
||||
throw InvalidFilterValue::make($value);
|
||||
}
|
||||
|
||||
$values[$index] = $result;
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
protected function getClass(ReflectionParameter $parameter): ?ReflectionClass
|
||||
{
|
||||
if (version_compare(PHP_VERSION, '8.0', '<')) {
|
||||
return $parameter->getClass();
|
||||
}
|
||||
|
||||
$type = $parameter->getType();
|
||||
|
||||
if (is_null($type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type instanceof ReflectionUnionType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type->isBuiltin()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type->getName() === 'self') {
|
||||
return $parameter->getDeclaringClass();
|
||||
}
|
||||
|
||||
return new ReflectionClass($type->getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Filters;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @template TModelClass of \Illuminate\Database\Eloquent\Model
|
||||
* @template-implements \Spatie\QueryBuilder\Filters\Filter<TModelClass>
|
||||
*/
|
||||
class FiltersTrashed implements Filter
|
||||
{
|
||||
/** {@inheritdoc} */
|
||||
public function __invoke(Builder $query, $value, string $property)
|
||||
{
|
||||
if ($value === 'with') {
|
||||
$query->withTrashed();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($value === 'only') {
|
||||
$query->onlyTrashed();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query->withoutTrashed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Includes;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @template TModelClass of \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
interface IncludeInterface
|
||||
{
|
||||
/**
|
||||
* @param \Illuminate\Database\Eloquent\Builder<TModelClass> $query
|
||||
* @param string $include
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function __invoke(Builder $query, string $include);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Includes;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class IncludedCallback implements IncludeInterface
|
||||
{
|
||||
protected Closure $callback;
|
||||
|
||||
public function __construct(Closure $callback)
|
||||
{
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
public function __invoke(Builder $query, string $relation)
|
||||
{
|
||||
$query->with([
|
||||
$relation => $this->callback,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Includes;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class IncludedCount implements IncludeInterface
|
||||
{
|
||||
public function __invoke(Builder $query, string $count)
|
||||
{
|
||||
$query->withCount(Str::before($count, config('query-builder.count_suffix', 'Count')));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Includes;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class IncludedExists implements IncludeInterface
|
||||
{
|
||||
public function __invoke(Builder $query, string $exists)
|
||||
{
|
||||
$exists = Str::before($exists, config('query-builder.exists_suffix', 'Exists'));
|
||||
|
||||
$query
|
||||
->withExists($exists)
|
||||
->withCasts([
|
||||
"{$exists}_exists" => 'boolean',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Includes;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class IncludedRelationship implements IncludeInterface
|
||||
{
|
||||
/** @var Closure|null */
|
||||
public $getRequestedFieldsForRelatedTable;
|
||||
|
||||
public function __invoke(Builder $query, string $relationship)
|
||||
{
|
||||
$relatedTables = collect(explode('.', $relationship));
|
||||
|
||||
$withs = $relatedTables
|
||||
->mapWithKeys(function ($table, $key) use ($query, $relatedTables) {
|
||||
$fullRelationName = $relatedTables->slice(0, $key + 1)->implode('.');
|
||||
|
||||
if ($this->getRequestedFieldsForRelatedTable) {
|
||||
$fields = ($this->getRequestedFieldsForRelatedTable)($fullRelationName);
|
||||
}
|
||||
|
||||
if (empty($fields)) {
|
||||
return [$fullRelationName];
|
||||
}
|
||||
|
||||
return [$fullRelationName => function ($query) use ($fields) {
|
||||
$query->select($fields);
|
||||
}];
|
||||
})
|
||||
->toArray();
|
||||
|
||||
$query->with($withs);
|
||||
}
|
||||
|
||||
public static function getIndividualRelationshipPathsFromInclude(string $include): Collection
|
||||
{
|
||||
return collect(explode('.', $include))
|
||||
->reduce(function (Collection $includes, string $relationship) {
|
||||
if ($includes->isEmpty()) {
|
||||
return $includes->push($relationship);
|
||||
}
|
||||
|
||||
return $includes->push("{$includes->last()}.{$relationship}");
|
||||
}, collect());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder;
|
||||
|
||||
use ArrayAccess;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Traits\ForwardsCalls;
|
||||
use Spatie\QueryBuilder\Concerns\AddsFieldsToQuery;
|
||||
use Spatie\QueryBuilder\Concerns\AddsIncludesToQuery;
|
||||
use Spatie\QueryBuilder\Concerns\FiltersQuery;
|
||||
use Spatie\QueryBuilder\Concerns\SortsQuery;
|
||||
use Spatie\QueryBuilder\Exceptions\InvalidSubject;
|
||||
|
||||
/**
|
||||
* @mixin EloquentBuilder
|
||||
*/
|
||||
class QueryBuilder implements ArrayAccess
|
||||
{
|
||||
use FiltersQuery;
|
||||
use SortsQuery;
|
||||
use AddsIncludesToQuery;
|
||||
use AddsFieldsToQuery;
|
||||
use ForwardsCalls;
|
||||
|
||||
/** @var \Spatie\QueryBuilder\QueryBuilderRequest */
|
||||
protected $request;
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation */
|
||||
protected $subject;
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $subject
|
||||
* @param null|\Illuminate\Http\Request $request
|
||||
*/
|
||||
public function __construct($subject, ?Request $request = null)
|
||||
{
|
||||
$this->initializeSubject($subject)
|
||||
->initializeRequest($request ?? app(Request::class));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $subject
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function initializeSubject($subject): static
|
||||
{
|
||||
throw_unless(
|
||||
$subject instanceof EloquentBuilder || $subject instanceof Relation,
|
||||
InvalidSubject::make($subject)
|
||||
);
|
||||
|
||||
$this->subject = $subject;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function initializeRequest(?Request $request = null): static
|
||||
{
|
||||
$this->request = $request
|
||||
? QueryBuilderRequest::fromRequest($request)
|
||||
: app(QueryBuilderRequest::class);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEloquentBuilder(): EloquentBuilder
|
||||
{
|
||||
if ($this->subject instanceof EloquentBuilder) {
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
if ($this->subject instanceof Relation) {
|
||||
return $this->subject->getQuery();
|
||||
}
|
||||
|
||||
throw InvalidSubject::make($this->subject);
|
||||
}
|
||||
|
||||
public function getSubject()
|
||||
{
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EloquentBuilder|Relation|string $subject
|
||||
* @param Request|null $request
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function for($subject, ?Request $request = null): static
|
||||
{
|
||||
if (is_subclass_of($subject, Model::class)) {
|
||||
$subject = $subject::query();
|
||||
}
|
||||
|
||||
return new static($subject, $request);
|
||||
}
|
||||
|
||||
public function __call($name, $arguments)
|
||||
{
|
||||
$result = $this->forwardCallTo($this->subject, $name, $arguments);
|
||||
|
||||
/*
|
||||
* If the forwarded method call is part of a chain we can return $this
|
||||
* instead of the actual $result to keep the chain going.
|
||||
*/
|
||||
if ($result === $this->subject) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function clone()
|
||||
{
|
||||
return clone $this;
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->subject = clone $this->subject;
|
||||
}
|
||||
|
||||
public function __get($name)
|
||||
{
|
||||
return $this->subject->{$name};
|
||||
}
|
||||
|
||||
public function __set($name, $value)
|
||||
{
|
||||
$this->subject->{$name} = $value;
|
||||
}
|
||||
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->subject[$offset]);
|
||||
}
|
||||
|
||||
public function offsetGet($offset): bool
|
||||
{
|
||||
return $this->subject[$offset];
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
$this->subject[$offset] = $value;
|
||||
}
|
||||
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
unset($this->subject[$offset]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class QueryBuilderRequest extends Request
|
||||
{
|
||||
private static $includesArrayValueDelimiter = ',';
|
||||
|
||||
private static $appendsArrayValueDelimiter = ',';
|
||||
|
||||
private static $fieldsArrayValueDelimiter = ',';
|
||||
|
||||
private static $sortsArrayValueDelimiter = ',';
|
||||
|
||||
private static $filterArrayValueDelimiter = ',';
|
||||
|
||||
public static function setArrayValueDelimiter(string $delimiter): void
|
||||
{
|
||||
static::$filterArrayValueDelimiter = $delimiter;
|
||||
static::$includesArrayValueDelimiter = $delimiter;
|
||||
static::$appendsArrayValueDelimiter = $delimiter;
|
||||
static::$fieldsArrayValueDelimiter = $delimiter;
|
||||
static::$sortsArrayValueDelimiter = $delimiter;
|
||||
}
|
||||
|
||||
public static function fromRequest(Request $request): self
|
||||
{
|
||||
return static::createFrom($request, new static());
|
||||
}
|
||||
|
||||
public function includes(): Collection
|
||||
{
|
||||
$includeParameterName = config('query-builder.parameters.include', 'include');
|
||||
|
||||
$includeParts = $this->getRequestData($includeParameterName);
|
||||
|
||||
if (is_string($includeParts)) {
|
||||
$includeParts = explode(static::getIncludesArrayValueDelimiter(), $includeParts);
|
||||
}
|
||||
|
||||
return collect($includeParts)->filter();
|
||||
}
|
||||
|
||||
public function appends(): Collection
|
||||
{
|
||||
$appendParameterName = config('query-builder.parameters.append', 'append');
|
||||
|
||||
$appendParts = $this->getRequestData($appendParameterName);
|
||||
|
||||
if (! is_array($appendParts) && ! is_null($appendParts)) {
|
||||
$appendParts = explode(static::getAppendsArrayValueDelimiter(), $appendParts);
|
||||
}
|
||||
|
||||
return collect($appendParts)->filter();
|
||||
}
|
||||
|
||||
public function fields(): Collection
|
||||
{
|
||||
$fieldsParameterName = config('query-builder.parameters.fields', 'fields');
|
||||
$fieldsData = $this->getRequestData($fieldsParameterName);
|
||||
|
||||
$fieldsPerTable = collect(is_string($fieldsData) ? explode(static::getFieldsArrayValueDelimiter(), $fieldsData) : $fieldsData);
|
||||
|
||||
if ($fieldsPerTable->isEmpty()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$fields = [];
|
||||
|
||||
$fieldsPerTable->each(function ($tableFields, $model) use (&$fields) {
|
||||
if (is_numeric($model)) {
|
||||
// If the field is in dot notation, we'll grab the table without the field.
|
||||
// If the field isn't in dot notation we want the base table. We'll use `_` and replace it later.
|
||||
$model = Str::contains($tableFields, '.') ? Str::beforeLast($tableFields, '.') : '_';
|
||||
}
|
||||
|
||||
if (! isset($fields[$model])) {
|
||||
$fields[$model] = [];
|
||||
}
|
||||
|
||||
// If the field is in dot notation, we'll grab the field without the tables:
|
||||
$tableFields = array_map(function (string $field) {
|
||||
return Str::afterLast($field, '.');
|
||||
}, explode(static::getFieldsArrayValueDelimiter(), $tableFields));
|
||||
|
||||
$fields[$model] = array_merge($fields[$model], $tableFields);
|
||||
});
|
||||
|
||||
return collect($fields);
|
||||
}
|
||||
|
||||
public function sorts(): Collection
|
||||
{
|
||||
$sortParameterName = config('query-builder.parameters.sort', 'sort');
|
||||
|
||||
$sortParts = $this->getRequestData($sortParameterName);
|
||||
|
||||
if (is_string($sortParts)) {
|
||||
$sortParts = explode(static::getSortsArrayValueDelimiter(), $sortParts);
|
||||
}
|
||||
|
||||
return collect($sortParts)->filter();
|
||||
}
|
||||
|
||||
public function filters(): Collection
|
||||
{
|
||||
$filterParameterName = config('query-builder.parameters.filter', 'filter');
|
||||
|
||||
$filterParts = $this->getRequestData($filterParameterName, []);
|
||||
|
||||
if (is_string($filterParts)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$filters = collect($filterParts);
|
||||
|
||||
return $filters->map(function ($value) {
|
||||
return $this->getFilterValue($value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
*
|
||||
* @return array|bool|null
|
||||
*/
|
||||
protected function getFilterValue($value)
|
||||
{
|
||||
if (empty($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return collect($value)->map(function ($valueValue) {
|
||||
return $this->getFilterValue($valueValue);
|
||||
})->all();
|
||||
}
|
||||
|
||||
if (Str::contains($value, static::getFilterArrayValueDelimiter())) {
|
||||
return explode(static::getFilterArrayValueDelimiter(), $value);
|
||||
}
|
||||
|
||||
if ($value === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($value === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected function getRequestData(?string $key = null, $default = null)
|
||||
{
|
||||
return $this->input($key, $default);
|
||||
}
|
||||
|
||||
public static function setIncludesArrayValueDelimiter(string $includesArrayValueDelimiter): void
|
||||
{
|
||||
static::$includesArrayValueDelimiter = $includesArrayValueDelimiter;
|
||||
}
|
||||
|
||||
public static function setAppendsArrayValueDelimiter(string $appendsArrayValueDelimiter): void
|
||||
{
|
||||
static::$appendsArrayValueDelimiter = $appendsArrayValueDelimiter;
|
||||
}
|
||||
|
||||
public static function setFieldsArrayValueDelimiter(string $fieldsArrayValueDelimiter): void
|
||||
{
|
||||
static::$fieldsArrayValueDelimiter = $fieldsArrayValueDelimiter;
|
||||
}
|
||||
|
||||
public static function setSortsArrayValueDelimiter(string $sortsArrayValueDelimiter): void
|
||||
{
|
||||
static::$sortsArrayValueDelimiter = $sortsArrayValueDelimiter;
|
||||
}
|
||||
|
||||
public static function setFilterArrayValueDelimiter(string $filterArrayValueDelimiter): void
|
||||
{
|
||||
static::$filterArrayValueDelimiter = $filterArrayValueDelimiter;
|
||||
}
|
||||
|
||||
public static function getIncludesArrayValueDelimiter(): string
|
||||
{
|
||||
return static::$includesArrayValueDelimiter;
|
||||
}
|
||||
|
||||
public static function getAppendsArrayValueDelimiter(): string
|
||||
{
|
||||
return static::$appendsArrayValueDelimiter;
|
||||
}
|
||||
|
||||
public static function getFieldsArrayValueDelimiter(): string
|
||||
{
|
||||
return static::$fieldsArrayValueDelimiter;
|
||||
}
|
||||
|
||||
public static function getSortsArrayValueDelimiter(): string
|
||||
{
|
||||
return static::$sortsArrayValueDelimiter;
|
||||
}
|
||||
|
||||
public static function getFilterArrayValueDelimiter(): string
|
||||
{
|
||||
return static::$filterArrayValueDelimiter;
|
||||
}
|
||||
|
||||
public static function resetDelimiters(): void
|
||||
{
|
||||
self::$includesArrayValueDelimiter = ',';
|
||||
self::$appendsArrayValueDelimiter = ',';
|
||||
self::$fieldsArrayValueDelimiter = ',';
|
||||
self::$sortsArrayValueDelimiter = ',';
|
||||
self::$filterArrayValueDelimiter = ',';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder;
|
||||
|
||||
use Spatie\LaravelPackageTools\Package;
|
||||
use Spatie\LaravelPackageTools\PackageServiceProvider;
|
||||
|
||||
class QueryBuilderServiceProvider extends PackageServiceProvider
|
||||
{
|
||||
public function configurePackage(Package $package): void
|
||||
{
|
||||
$package
|
||||
->name('laravel-query-builder')
|
||||
->hasConfigFile();
|
||||
}
|
||||
|
||||
public function registeringPackage()
|
||||
{
|
||||
$this->app->bind(QueryBuilderRequest::class, function ($app) {
|
||||
return QueryBuilderRequest::fromRequest($app['request']);
|
||||
});
|
||||
}
|
||||
|
||||
public function provides()
|
||||
{
|
||||
return [
|
||||
QueryBuilderRequest::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Sorts;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
interface Sort
|
||||
{
|
||||
public function __invoke(Builder $query, bool $descending, string $property);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Sorts;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class SortsCallback implements Sort
|
||||
{
|
||||
/**
|
||||
* @var callable a PHP callback of the following signature:
|
||||
* `function (\Illuminate\Database\Eloquent\Builder $builder, bool $descending, string $property)`
|
||||
*/
|
||||
private $callback;
|
||||
|
||||
public function __construct($callback)
|
||||
{
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function __invoke(Builder $query, bool $descending, string $property)
|
||||
{
|
||||
return call_user_func($this->callback, $query, $descending, $property);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Spatie\QueryBuilder\Sorts;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class SortsField implements Sort
|
||||
{
|
||||
public function __invoke(Builder $query, bool $descending, string $property)
|
||||
{
|
||||
$query->orderBy($property, $descending ? 'desc' : 'asc');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user