diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 32782aa..23e76cf 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -2,9 +2,9 @@ name: run-tests on: push: - branches: [main] + branches: [ main ] pull_request: - branches: [main] + branches: [ main ] jobs: test: @@ -13,13 +13,16 @@ jobs: fail-fast: true matrix: os: [ ubuntu-latest ] - php: [ 8.2 ] - laravel: [ 10.* ] + php: [ 8.2, 8.3 ] + laravel: [ 10.*, 11.* ] stability: [ prefer-stable ] include: - laravel: 10.* testbench: 8.* carbon: ^2.63 + - laravel: 11.* + testbench: 9.* + carbon: ^2.63 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} @@ -29,7 +32,7 @@ jobs: env: MYSQL_USER: user MYSQL_PASSWORD: secret - MYSQL_DATABASE: laravel-query-builder-powered + MYSQL_DATABASE: testing MYSQL_ROOT_PASSWORD: root ports: - 3306 diff --git a/README.md b/README.md index 56da6db..c8c92dd 100644 --- a/README.md +++ b/README.md @@ -36,14 +36,77 @@ return [ ## Usage +### Pagination + +This package uses Laravel pagination by default; However, it allows you to specify through parameters of +queries, the number of records to obtain, you can even obtain all the records. + +```php +GET /books?per_page=30 +``` + +It is recommended to use the `result` method instead of `paginate` or `get`. Since `result` encapsulates the logic of +both yes, it is requested to show all, below `result` will use `get` but if you want to see a number of records +below will use `paginate` with the amount provided. + +```php +use Spatie\QueryBuilder\AllowedFilter; +use TeamQ\QueryBuilder\QueryBuilder; +use TeamQ\QueryBuilder\Filters\TextFilter; + +$query = QueryBuilder::for(Book::class); + +$query->result(); +``` + ### Filters -| Filter | Class | -|--------|-------------------------------------------| -| Text | `TeamQ\QueryBuilder\Filters\TextFilter` | -| Number | `TeamQ\QueryBuilder\Filters\NumberFilter` | -| Date | `TeamQ\QueryBuilder\Filters\DateFilter` | -| Global | `TeamQ\QueryBuilder\Filters\GlobalFilter` | +| Filter | Class | Operators | +|--------|-------------------------------------------|-----------| +| Text | `TeamQ\QueryBuilder\Filters\TextFilter` | Text | +| Number | `TeamQ\QueryBuilder\Filters\NumberFilter` | Number | +| Date | `TeamQ\QueryBuilder\Filters\DateFilter` | Number | +| Global | `TeamQ\QueryBuilder\Filters\GlobalFilter` | - | + +
+ Text comparison operators + +| Comparison operator | Key | +|---------------------|-----| +| Equal | 1 | +| Not Equal | 2 | +| Start With | 3 | +| Not Start With | 4 | +| End With | 5 | +| Not End With | 6 | +| Contains | 7 | +| Not Contains | 8 | +| In | 9 | +| Not In | 10 | +| Filled | 11 | +| Not Filled | 12 | + +
+ +
+ Number comparison operators + +| Comparison operator | Key | +|-----------------------|-----| +| Equal | 1 | +| Not Equal | 2 | +| Greater Than | 3 | +| Greater Than Or Equal | 4 | +| Less Than | 5 | +| Less Than Or Equal | 6 | +| Between | 7 | +| Not Between | 8 | +| In | 9 | +| Not In | 10 | +| Filled | 11 | +| Not Filled | 12 | + +
You can use advanced filters that have support for multiple comparison operators. The available comparison operators are located in `TeamQ\QueryBuilder\Enums\Comparators` @@ -56,7 +119,7 @@ GET /books?filter[isbn][value]=54213&filter[isbn][operator]=5 ```php use Spatie\QueryBuilder\AllowedFilter; -use Spatie\QueryBuilder\QueryBuilder; +use TeamQ\QueryBuilder\QueryBuilder; use TeamQ\QueryBuilder\Filters\TextFilter; QueryBuilder::for(Book::class) @@ -70,7 +133,7 @@ and pass the type of `join` to use. ```php use Spatie\QueryBuilder\AllowedFilter; -use Spatie\QueryBuilder\QueryBuilder; +use TeamQ\QueryBuilder\QueryBuilder; use TeamQ\QueryBuilder\Filters\TextFilter; use TeamQ\QueryBuilder\Enums\JoinType; @@ -80,7 +143,89 @@ QueryBuilder::for(Book::class) ]); ``` -#### Global Filter +#### _Text Filter_ + +The following example uses the comparison operator `5`, which is equivalent to asking if there is a book where +isbn `End With` 54213 + +```php +GET /books?filter[isbn][value]=54213&filter[isbn][operator]=5 +``` + +```sql +select * +from `books` +where lower(`isbn`) like '%54213' +``` + +```php +use Spatie\QueryBuilder\AllowedFilter; +use TeamQ\QueryBuilder\QueryBuilder; +use TeamQ\QueryBuilder\Filters\TextFilter; + +QueryBuilder::for(Book::class) + ->allowedFilters([ + AllowedFilter::custom('isbn', new TextFilter()), + ]); +``` + +#### _Number Filter_ + +The following example uses the comparison operator `9`, which is equivalent to asking if there is a book where +id `In` 1, 5 or 9 + +For this example an array of values was used. Arraying values is supported by all types of operators (text and number). + +```php +GET /books?filter[id][value][0]=1&filter[id][value][1]=5&filter[id][value][2]=9&filter[id][operator]=9 +``` + +```sql +select * +from `books` +where id in (1, 5, 9) +``` + +```php +use Spatie\QueryBuilder\AllowedFilter; +use TeamQ\QueryBuilder\QueryBuilder; +use TeamQ\QueryBuilder\Filters\NumberFilter; + +QueryBuilder::for(Book::class) + ->allowedFilters([ + AllowedFilter::custom('id', new NumberFilter()), + ]); +``` + +#### _Date Filter_ + +The following example uses the comparison operator `8`, which is equivalent to asking if there is a book where +created at `Not Between` 2019-08-01 and 2019-08-10 + +For this example an array of values was used. Arraying values is supported by all types of operators (text and number). + +```php +GET /books?filter[created_at][value][0]=2019-08-01&filter[created_at][value][1]=2019-08-10&filter[id][operator]=8 +``` + +```sql +select * +from `books` +where created_at not between '2019-08-01' and '2019-08-10' +``` + +```php +use Spatie\QueryBuilder\AllowedFilter; +use TeamQ\QueryBuilder\QueryBuilder; +use TeamQ\QueryBuilder\Filters\DateFilter; + +QueryBuilder::for(Book::class) + ->allowedFilters([ + AllowedFilter::custom('created_at', new DateFilter()), + ]); +``` + +#### _Global Filter_ The global filter implements general search functionality for the model and its relationships. @@ -91,7 +236,7 @@ To use this filter, you must pass the model fields to be filtered or their relat ```php use Spatie\QueryBuilder\AllowedFilter; -use Spatie\QueryBuilder\QueryBuilder; +use TeamQ\QueryBuilder\QueryBuilder; use TeamQ\QueryBuilder\Filters\GlobalFilter; QueryBuilder::for(Book::class) @@ -103,6 +248,8 @@ QueryBuilder::for(Book::class) ]); ``` +--- + ### Sorts | Sort | Class | @@ -110,14 +257,14 @@ QueryBuilder::for(Book::class) | Relation | `TeamQ\QueryBuilder\Sorts\RelationSort` | | Case | `TeamQ\QueryBuilder\Sorts\CaseSort` | -#### RelationSort +#### _RelationSort_ To sort by fields of related tables you must use `join`, there is no easy way to do it from eloquent, so you can use `RelationSort`, this class receives the type of `join` as a parameter. ```php use Spatie\QueryBuilder\AllowedFilter; -use Spatie\QueryBuilder\QueryBuilder; +use TeamQ\QueryBuilder\QueryBuilder; use TeamQ\QueryBuilder\Sorts\RelationSort; QueryBuilder::for(Book::class) @@ -126,7 +273,7 @@ QueryBuilder::for(Book::class) ]) ``` -#### CaseSort +#### _CaseSort_ If you use enums or states, where each enum or state is represented by a number, you may want to sort by name of that enum or state and not by the number, then you can use `CaseSort`. @@ -138,7 +285,7 @@ related model. By default, it is `Inner Join`. ```php use Spatie\QueryBuilder\AllowedFilter; -use Spatie\QueryBuilder\QueryBuilder; +use TeamQ\QueryBuilder\QueryBuilder; use TeamQ\QueryBuilder\Sorts\CaseSort; QueryBuilder::for(Book::class) diff --git a/composer.json b/composer.json index f9e5d6e..62fb6a7 100644 --- a/composer.json +++ b/composer.json @@ -18,24 +18,16 @@ } ], "require": { - "php": "^8.2", - "illuminate/contracts": "^10.0", + "php": "^8.2|^8.3", "kirschbaum-development/eloquent-power-joins": "^3.2", "spatie/laravel-package-tools": "^1.14.0", - "spatie/laravel-query-builder": "^5.2" + "spatie/laravel-query-builder": "^5.2|^6.0" }, "require-dev": { - "roave/security-advisories": "dev-latest", "laravel/pint": "^1.0", - "nunomaduro/collision": "^7.9", - "nunomaduro/larastan": "^2.0.1", - "orchestra/testbench": "^8.0", + "orchestra/testbench": "^8.0|^9.0", "pestphp/pest": "^2.0", - "pestphp/pest-plugin-arch": "^2.0", - "pestphp/pest-plugin-laravel": "^2.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0" + "pestphp/pest-plugin-laravel": "^2.0" }, "autoload": { "psr-4": { @@ -49,7 +41,6 @@ }, "scripts": { "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", - "analyse": "vendor/bin/phpstan analyse", "test": "vendor/bin/pest", "test-coverage": "vendor/bin/pest --coverage", "format": "vendor/bin/pint" diff --git a/docker-compose.yml b/docker-compose.yml index 3eb976b..f27d1f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,23 +1,24 @@ -version: "3.6" - services: app: build: context: . dockerfile: docker/Dockerfile - container_name: laravel-query-builder-powered + container_name: laravel-datatable-api volumes: - ".:/var/www/html" depends_on: - mysql + networks: + - laravel-datatable-net mysql: image: 'mysql/mysql-server:8.0' + container_name: laravel-datatable-db ports: - '${FORWARD_DB_PORT:-3306}:3306' environment: MYSQL_ROOT_PASSWORD: 'root' MYSQL_ROOT_HOST: "%" - MYSQL_DATABASE: 'laravel-query-builder-powered' + MYSQL_DATABASE: 'testing' MYSQL_USER: 'laravel' MYSQL_PASSWORD: 'laravel' MYSQL_ALLOW_EMPTY_PASSWORD: 1 @@ -28,3 +29,8 @@ services: - ping retries: 3 timeout: 5s + networks: + - laravel-datatable-net +networks: + laravel-datatable-net: + driver: bridge diff --git a/docker/Dockerfile b/docker/Dockerfile index 5d87581..003e8fb 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # Base image -FROM php:8.2-alpine +FROM php:8.3-alpine # Create app folder RUN mkdir -p /var/www/html/ diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c8ff637..03c3365 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,7 +16,7 @@ backupStaticProperties="false" > - + tests @@ -31,7 +31,7 @@ - + diff --git a/src/Filters/DateFilter.php b/src/Filters/DateFilter.php index 380ab51..2399a8f 100644 --- a/src/Filters/DateFilter.php +++ b/src/Filters/DateFilter.php @@ -13,6 +13,10 @@ */ class DateFilter extends Filter { + /** + * Operators that receive values as arrays or should be filtered as an array. + * Ex: In, NotIn, Between, etc. + */ protected const ARRAY_OPERATORS = [ Comparators\Number::Between, Comparators\Number::NotBetween, diff --git a/src/Filters/Filter.php b/src/Filters/Filter.php index 374385a..19749dc 100644 --- a/src/Filters/Filter.php +++ b/src/Filters/Filter.php @@ -16,6 +16,10 @@ abstract class Filter implements IFilter { use HasPropertyRelationship; + /** + * Operators that receive values as arrays or should be filtered as an array. + * Ex: In, NotIn, Between, etc. + */ protected const ARRAY_OPERATORS = []; /** @@ -71,9 +75,14 @@ public function __invoke(Builder $query, $value, string $property): void if ($value !== false) { if (is_array($value)) { + // If the value received is an array of values and the operator we receive is an operator + // of arrays, then this follows the normal flow. if (in_array($operator, static::ARRAY_OPERATORS, true)) { $this->handle($query, $value, $property, $operator); } else { + // But if it is not an array operator, then we encapsulate the query logic in + // a subquery, applying the comparison operator provided for each of the + // y values connected by "or". $query->where(function (Builder $query) use ($value, $property, $operator) { foreach ($value as $item) { $this->handle($query, $item, $property, $operator, 'or'); diff --git a/src/Filters/NumberFilter.php b/src/Filters/NumberFilter.php index 5b3cf78..87826b4 100644 --- a/src/Filters/NumberFilter.php +++ b/src/Filters/NumberFilter.php @@ -13,6 +13,10 @@ */ class NumberFilter extends Filter { + /** + * Operators that receive values as arrays or should be filtered as an array. + * Ex: In, NotIn, Between, etc. + */ protected const ARRAY_OPERATORS = [ Comparators\Number::Between, Comparators\Number::NotBetween, diff --git a/src/Filters/TextFilter.php b/src/Filters/TextFilter.php index 2f4541a..cc67b4d 100644 --- a/src/Filters/TextFilter.php +++ b/src/Filters/TextFilter.php @@ -13,6 +13,10 @@ */ class TextFilter extends Filter { + /** + * Operators that receive values as arrays or should be filtered as an array. + * Ex: In, NotIn, Between, etc. + */ protected const ARRAY_OPERATORS = [ Comparators\Text::In, Comparators\Text::NotIn,