├── .github
└── workflows
│ └── testing.yml
├── .gitignore
├── README.md
├── composer.json
├── config
└── advanced_filter.php
├── phpunit.xml
├── src
├── AdvancedFilterServiceProvider.php
├── Exceptions
│ ├── DatatypeNotFoundException.php
│ ├── OperatorNotFoundException.php
│ ├── UnsupportedDriverException.php
│ └── UnsupportedOperatorException.php
├── Fields
│ ├── Field.php
│ ├── FieldCast.php
│ ├── FieldsFactory.php
│ └── HasFields.php
├── Filter.php
├── FilterRequest.php
├── Filterable.php
├── HasFilter.php
├── Operators
│ ├── Between.php
│ ├── Contains.php
│ ├── EndsWith.php
│ ├── Equals.php
│ ├── GreaterThan.php
│ ├── GreaterThanOrEqual.php
│ ├── In.php
│ ├── LessThan.php
│ ├── LessThanOrEqual.php
│ ├── NotContains.php
│ ├── NotEndsWith.php
│ ├── NotEquals.php
│ ├── NotIn.php
│ ├── NotStartsWith.php
│ ├── Operator.php
│ └── StartsWith.php
└── QueryFormats
│ ├── ArrayQueryFormat.php
│ ├── JsonQueryFormat.php
│ ├── QueryFormat.php
│ └── SeparatorQueryFormat.php
└── tests
├── GeneralSearchTest.php
├── Models
├── BaseModel.php
├── Order.php
├── OrderLine.php
├── Product.php
└── Store.php
├── Operators
├── BetweenTest.php
├── ContainsTest.php
├── EndsWithTest.php
├── EqualsTest.php
├── GreaterThanOrEqualTest.php
├── GreaterThanTest.php
├── InTest.php
├── LessThanOrEqualTest.php
├── LessThanTest.php
├── NotContainsTest.php
├── NotEndsWithTest.php
├── NotEqualsTest.php
├── NotInTest.php
├── NotStartsWithTest.php
└── StartsWithTest.php
├── QueryFormatsTest.php
├── Seeds
├── DatabaseSeeder.php
├── OrderSeeder.php
├── ProductSeeder.php
└── StoreSeeder.php
└── TestCase.php
/.github/workflows/testing.yml:
--------------------------------------------------------------------------------
1 | name: testing
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - DB ${{ matrix.env.DB_CONNECTION }}
13 | strategy:
14 | fail-fast: true
15 | max-parallel: 4
16 | matrix:
17 | php: [ 7.3 ]
18 | laravel: [ 5.8.*, 6.*, 7.*, 8.* ]
19 | dependency-version: [ prefer-stable ]
20 | env:
21 | - { DB_CONNECTION: 'sqlite', DB_DATABASE: ':memory:' }
22 | - { DB_CONNECTION: 'mysql', DB_DATABASE: 'test', DB_USERNAME: 'root', DB_PORT: 3306 }
23 | include:
24 | - laravel: 8.*
25 | testbench: 6.*
26 | - laravel: 7.*
27 | testbench: 5.*
28 | - laravel: 6.*
29 | testbench: 4.*
30 | - laravel: 5.8.*
31 | testbench: 3.8.*
32 |
33 | env: ${{ matrix.env }}
34 |
35 | services:
36 | mysql:
37 | image: mysql:5.7
38 | env:
39 | MYSQL_ALLOW_EMPTY_PASSWORD: yes
40 | MYSQL_DATABASE: test
41 | ports:
42 | - 3306:3306
43 |
44 | steps:
45 | - name: Checkout code
46 | uses: actions/checkout@v1
47 |
48 | - name: Cache dependencies
49 | uses: actions/cache@v1
50 | with:
51 | path: ~/.composer/cache/files
52 | key: dependencies-${{ matrix.dependency-version }}-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
53 |
54 | - name: Setup PHP
55 | uses: shivammathur/setup-php@v2
56 | with:
57 | php-version: ${{ matrix.php }}
58 |
59 | - name: Install dependencies
60 | run: |
61 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
62 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest
63 |
64 | - name: Execute tests
65 | run: vendor/bin/phpunit
66 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.lock
2 | vendor
3 | .phpunit.result.cache
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel Advanced Filter
2 | This package allows you to filter on laravel models
3 |
4 | You can choose fields to filtering and customize its data-types, aliases and excepted operators,
5 | you can add/customize your request format, and you add new operators or overwrite the existed operators
6 |
7 |
8 | ## Installation
9 | You can install the package via composer:
10 | ```
11 | composer require asemalalami/laravel-advanced-filter
12 | ```
13 |
14 | The package will automatically register its service provider.
15 |
16 | You can optionally publish the config file with:
17 | ```
18 | php artisan vendor:publish --provider="AsemAlalami\LaravelAdvancedFilter\AdvancedFilterServiceProvider" --tag="config"
19 | ```
20 |
21 | These default config file that will be published:
22 | [Config File](https://github.com/AsemAlalami/Laravel-Advanced-Filter/blob/master/config/advanced_filter.php)
23 |
24 | ## Usage
25 | - use `HasFilter` trait in the model
26 | - add fields in the implementation of the abstract function `setupFilter`
27 | - call the `filter` scope in your controller
28 | ```php
29 | class Order extends Model
30 | {
31 | use HasFilter;
32 |
33 | protected $casts = [
34 | 'void' => 'boolean',
35 | ];
36 |
37 | public function channel()
38 | {
39 | return $this->belongsTo(Channel::class);
40 | }
41 |
42 | public function orderLines()
43 | {
44 | return $this->hasMany(OrderLine::class);
45 | }
46 |
47 | public function setupFilter()
48 | {
49 | $this->addField('void'); // will cast to 'boolean' from the model casts
50 | $this->addField('total')->setDatatype('numeric');
51 | $this->addFields(['source', 'subsource', 'order_date']);
52 | // field from relation
53 | $this->addFields(['channel.created_at' => 'channel_create'])->setDatatype('date');
54 | $this->addField('orderLines.product.sku', 'product_sku');
55 | // field from relation count
56 | $this->addCountField('orderLines');
57 | // custom field (raw sql)
58 | $this->addCustomField('my_total', '(shipping_cost + subtotal)');
59 | // enable general search
60 | $this->addGeneralSearch(['source', 'orderLines.product.sku'], 'startsWith');
61 | }
62 |
63 | // customize field filter by custom scope
64 | public function scopeWhereSource(Builder $builder, Field $field, string $operator, $value, $conjunction = 'and')
65 | {
66 | if ($operator == 'Equal') {
67 | return $builder->where(function (Builder $builder) use ($value) {
68 | $builder->where('source', $value)
69 | ->orWhere('subsource', $value);
70 | });
71 | }
72 |
73 | // default behavior
74 | return $builder->applyOperator($operator, $field, $value, $conjunction);
75 | }
76 | }
77 |
78 | ...
79 |
80 | class OrderController extends Controller
81 | {
82 | public function index()
83 | {
84 | return Order::filter()->paginate(); // you can pass your custom request
85 | }
86 | }
87 | ```
88 |
89 | ## Query Format
90 | Query format is the shape that you want to send your query(filters) in the request.
91 | the package support 3 formats, and you can create a new format.
92 | - `json` (default): the filters will send as json in the request
93 | ```json
94 | filters=[{"field":"email","operator":"equal","value":"abc"}]
95 | ```
96 | - `array`: the filters will send as array in the request
97 | ```
98 | filters[email][value]=abc&filters[email][operator]=equal
99 | ```
100 | - `separator`: the filters will send as well as in the `array` format, but separated by a separator(`^` default)
101 |
102 | the format sets with a separator symbol `separator:^`
103 | ```
104 | filters^email^value=abc&filters^email^operator=equal
105 | ```
106 | > set the default query format in the config file `query_format` attribute
107 |
108 | #### Create a new query format:
109 | - create a new class and extends it from `QueryFormat`: `class MyFormat extends QueryFormat`
110 | - implement the abstract function `format` that returns `FilterRequest` object
111 | - add the class to the config file in `custom_query_format` attribute: `'custom_query_format' => MyFormat::class,`
112 |
113 | ## Fields
114 | Normal Field options:
115 | - field name is the column name
116 | - alias is the key that you want to send in the request
117 | - data-type: by default it set from model `casts`, if you want to set custom data-type, use `setDatatype`
118 | - operators: the field will accept all operators unless you use `setExceptedOperators` to exclude some operators
119 | - a relational field: only set the field name by `.` separator `channel.name`, `channel.type.name`
120 | > you can define field name by `.` separator, but you want to consider it as a non relational field
121 | > by pass `false` for `inRelation` parameter (used in NoSQL DB or join between tables)
122 | ```php
123 | $this->addField('channels.name', 'channel_name', false);
124 | ```
125 | - customize a field query, you can make a scope for the field to customize the filter behavior.
126 | scope name must be combined 3 sections :
127 | - scope
128 | - the value of `prefix_scope_function` key in config file (`where` is the default)
129 | - field name(or relation name) for example `email`
130 | ```php
131 | public function scopeWhereEmail(Builder $builder, Field $field, string $operator, $value, $conjunction = 'and')
132 | ```
133 | > you can customize a relational field by define the scope in the relation model OR define scope by relation name
134 |
135 | ```php
136 | OrderLine.php
137 |
138 | public function scopeWherePrice(Builder $builder, Field $field, string $operator, $value, $conjunction = 'and')
139 |
140 | OR
141 |
142 | Order.php
143 |
144 | public function scopeWhereOrderLines(Builder $builder, Field $field, string $operator, $value, $conjunction = 'and')
145 | ```
146 | > you can use `applyOperator` function to use the default behavior `$builder->applyOperator($operator, $field, $value, $conjunction);`
147 |
148 | You can add fields to a model by using 4 functions:
149 | - `addField`(string $field, string $alias = null, ?bool $inRelation = null): by default alias value same as field name value
150 | ```php
151 | $this->addField('total')->setDatatype('numeric');
152 | ```
153 | - `addFields`($fields): accept an array of field and aliases:
154 | ```php
155 | $this->addFields(['created_at' => 'create_date', 'order_date'])->setDatatype('date');
156 | ```
157 | - `addCountField`(string $relation, string $alias = null, callable $callback = null): add a field from count of relation,
158 | use can customize the count query and alias(by default is concat relation name(snake case) and `_count`)
159 | ```php
160 | $this->addCountField('orderLines');
161 |
162 | $this->addCountField('orderLines', 'lines_count', function (Builder $builder) {
163 | $builder->where('quantity', '>', 1);
164 | });
165 | ```
166 | >
167 | - `addCustomField`(string $alias, string $sqlRaw, $relation = null): add a field from raw sql query
168 | ```php
169 | $this->addCustomField('my_total', '(`shipping_cost` + `subtotal`)');
170 | $this->addCustomField('line_subtotal', '(`price` + `quantity`)', 'orderLines'); // inside "orderLines" relation
171 | ```
172 |
173 | ## General Search
174 | You can enable general search on some fields, and you can specify the operator (`startsWith` is the default operator)
175 | ```php
176 | $this->addGeneralSearch(['source', 'orderLines.product.sku'], 'startsWith');
177 | ```
178 |
179 | ## Conjunction
180 | Currently, the package support one conjunction between all fields
181 | `and` | `or`, default conjunction attribute in the config file `default_conjunction`
182 |
183 | ## Operators
184 | The package has many operators, you can create new operators,
185 | and you can customize the operators aliases that you want to send in the request
186 | - Equals (`=`, `equals`)
187 | - NotEquals (`!=`, `notEquals`)
188 | - GreaterThan (`>` , `greater`)
189 | - GreaterThanOrEqual (`>=`, `greaterOrEqual`)
190 | - LessThan (`<`, `less`)
191 | - LessThanOrEqual (`<=`, `lessOrEqual`)
192 | - In (`|`, `in`)
193 | - NotIn (`!|`, `notIn`)
194 | - Contains (`*`, `contains`)
195 | - NotContains (`!*`, `notContains`)
196 | - StartsWith (`^`, `startsWith`)
197 | - NotStartsWith (`!^`, `notStartsWith`)
198 | - EndsWith (`$`, `endsWith`)
199 | - NotEndsWith (`!$`, `notEndsWith`)
200 | - Between (`><`, `between`)
201 |
202 | #### Create a new Operator:
203 | - create a new class and extends it from `Operator`: `class MyOperator extends Operator`
204 | - implement the abstract function `apply` and `getSqlOperator` (used as a default sql operator for count and custom field)
205 | - add the class in the config file in `custom_operators` attribute: `'custom_operators' => [MyOperator::class => ['my-op', '*']],`
206 |
207 | ## Data Types:
208 | - boolean
209 | - date
210 | - datetime
211 | - numeric
212 | - string
213 |
214 | ## Config
215 | [Config File](https://github.com/AsemAlalami/Laravel-Advanced-Filter/blob/master/config/advanced_filter.php)
216 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "asemalalami/laravel-advanced-filter",
3 | "description": "Laravel Advanced Filter",
4 | "keywords": [
5 | "laravel",
6 | "filter",
7 | "filters"
8 | ],
9 | "type": "project",
10 | "license": "MIT",
11 | "authors": [
12 | {
13 | "name": "Asem Alalami",
14 | "email": "asem.almi@gmail.com"
15 | }
16 | ],
17 | "minimum-stability": "dev",
18 | "autoload": {
19 | "psr-4": {
20 | "AsemAlalami\\LaravelAdvancedFilter\\": "src"
21 | }
22 | },
23 | "autoload-dev": {
24 | "psr-4": {
25 | "AsemAlalami\\LaravelAdvancedFilter\\Test\\": "tests"
26 | }
27 | },
28 | "require": {
29 | "ext-json": "*",
30 | "illuminate/database": "^5.8|^6.0|^7.0|^8.0|^9.0",
31 | "illuminate/support": "^5.8|^6.0|^7.0|^8.0|^9.0"
32 | },
33 | "require-dev": {
34 | "phpunit/phpunit": "8.5.x-dev",
35 | "orchestra/testbench": "5.x-dev"
36 | },
37 | "extra": {
38 | "laravel": {
39 | "providers": [
40 | "AsemAlalami\\LaravelAdvancedFilter\\AdvancedFilterServiceProvider"
41 | ]
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/config/advanced_filter.php:
--------------------------------------------------------------------------------
1 | [
16 | 'Equals' => ['=', 'equals'],
17 | 'NotEquals' => ['!=', 'notEquals'],
18 | 'GreaterThan' => ['>', 'greater'],
19 | 'GreaterThanOrEqual' => ['>=', 'greaterOrEqual'],
20 | 'LessThan' => ['<', 'less'],
21 | 'LessThanOrEqual' => ['<=', 'lessOrEqual'],
22 | 'In' => ['|', 'in'],
23 | 'NotIn' => ['!|', 'notIn'],
24 | 'Contains' => ['*', 'contains'],
25 | 'NotContains' => ['!*', 'notContains'],
26 | 'StartsWith' => ['^', 'startsWith'],
27 | 'NotStartsWith' => ['!^', 'notStartsWith'],
28 | 'EndsWith' => ['$', 'endsWith'],
29 | 'NotEndsWith' => ['!$', 'notEndsWith'],
30 | 'Between' => ['><', 'between'],
31 | ],
32 |
33 | /*
34 | |--------------------------------------------------------------------------
35 | | Custom Operators
36 | |--------------------------------------------------------------------------
37 | |
38 | | Add your operators here
39 | |
40 | | example: operator class and its aliases
41 | | MyOperator::class => ['my-op', '*']
42 | |
43 | */
44 |
45 | 'custom_operators' => [
46 |
47 | ],
48 |
49 | /*
50 | |--------------------------------------------------------------------------
51 | | Default Operators
52 | |--------------------------------------------------------------------------
53 | |
54 | | The Default operators if the field sent in the request without operator
55 | | or for general search
56 | |
57 | */
58 |
59 | 'default_operator' => 'equals',
60 | 'default_general_search_operator' => 'startsWith',
61 |
62 | /*
63 | |--------------------------------------------------------------------------
64 | | Prefix Operator Function
65 | |--------------------------------------------------------------------------
66 | |
67 | | This option used when binging operators to Builder
68 | | The operators bound to Builder by using macros
69 | |
70 | | example:
71 | | if you want to filter by GreaterThan operator
72 | | $orders->filterWhereGreaterThan('total', 100)
73 | |
74 | */
75 |
76 | 'prefix_operator_function' => 'filterWhere',
77 |
78 | /*
79 | |--------------------------------------------------------------------------
80 | | Default Conjunction
81 | |--------------------------------------------------------------------------
82 | |
83 | | This option used when request sent without conjunction
84 | |
85 | | values: and | or
86 | |
87 | */
88 |
89 | 'default_conjunction' => 'and',
90 |
91 | /*
92 | |--------------------------------------------------------------------------
93 | | Default Query Format
94 | |--------------------------------------------------------------------------
95 | |
96 | | This option controls the default query format.
97 | | This format is used when sending request.
98 | |
99 | | Supported:
100 | | - "separator:^" : filters^email^value=abc&filters^email^operator=equal
101 | | - "array" : filters[email][value]=abc&filters[email][operator]=equal
102 | | - "json" (Default) : filters=[{"field":"email","operator":"equal","value":"abc"}]
103 | |
104 | */
105 |
106 | 'query_format' => 'json',
107 |
108 | /*
109 | |--------------------------------------------------------------------------
110 | | Custom Query Format
111 | |--------------------------------------------------------------------------
112 | |
113 | | Add your custom query format here
114 | | this format will set as a default query format of the request
115 | |
116 | | example: MyQueryFormat::class
117 | |
118 | */
119 |
120 | 'custom_query_format' => null,
121 |
122 | /*
123 | |--------------------------------------------------------------------------
124 | | Request Parameters Names
125 | |--------------------------------------------------------------------------
126 | |
127 | | This options to customize your request parameters names
128 | |
129 | */
130 |
131 | // the parameter name that contains the fields
132 | 'param_filter_name' => 'filters', // or as prefix in "separate" query format
133 | // the parameter name that set the conjunction
134 | 'param_conjunction_name' => 'conjunction',
135 | // the parameters names that define the field
136 | 'field_params' => [
137 | 'field' => 'field', // filed name, only used in "json" query format
138 | 'operator' => 'operator', // field operator
139 | 'value' => 'value', // field value
140 | ],
141 | // the parameter name that contains the general search value
142 | 'param_general_search_name' => 'query',
143 |
144 | /*
145 | |--------------------------------------------------------------------------
146 | | Cast date in the database
147 | |--------------------------------------------------------------------------
148 | |
149 | | This options will use "whereDate" function to compare date fields
150 | | the default will compare by start/end of day (as between)
151 | | this feature for big-data if you have index on the column
152 | |
153 | | And should set it to TRUE if your columns type (in the database) is not "datetime or date"
154 | |
155 | */
156 |
157 | 'cast_db_date' => false,
158 |
159 | /*
160 | |--------------------------------------------------------------------------
161 | | Prefix Scope Function
162 | |--------------------------------------------------------------------------
163 | |
164 | | This options used when you want to customize field filter behavior by defining scope for it
165 | |
166 | | example:
167 | | you need to customize behavior for "email" field, the scope function name with be:
168 | | scopeWhereEmail(Builder $builder, Field $field, string $operator, $value, $conjunction = 'and')
169 | |
170 | */
171 |
172 | 'prefix_scope_function' => 'where',
173 |
174 | /*
175 | |--------------------------------------------------------------------------
176 | | Empty Value As NULL
177 | |--------------------------------------------------------------------------
178 | |
179 | | This option used when you want to considered empty value as NULL
180 | |
181 | */
182 |
183 | 'empty_as_null' => false,
184 | ];
185 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | tests
15 |
16 |
17 |
18 |
19 | src/
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/AdvancedFilterServiceProvider.php:
--------------------------------------------------------------------------------
1 | publishes([
19 | __DIR__ . '/../config/advanced_filter.php' => config_path('advanced_filter.php'),
20 | ], 'config');
21 | }
22 |
23 | /**
24 | * Register any application services.
25 | *
26 | * @return void
27 | */
28 | public function register()
29 | {
30 | $this->mergeConfigFrom(
31 | __DIR__ . '/../config/advanced_filter.php', 'advanced_filter'
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Exceptions/DatatypeNotFoundException.php:
--------------------------------------------------------------------------------
1 | model = $model;
44 | $this->name = $name;
45 | $this->alias = $alias ?: $name;
46 | $this->isFromRelation = $isFromRelation;
47 | $this->nameExploded = explode('.', $this->name);
48 | }
49 |
50 | public function isFromRelation()
51 | {
52 | return $this->isFromRelation === null ? (count($this->nameExploded) > 1 && !$this->isCount()) : $this->isFromRelation;
53 | }
54 |
55 | /**
56 | * @param bool|null $isFromRelation
57 | */
58 | public function setIsFromRelation(?bool $isFromRelation): void
59 | {
60 | $this->isFromRelation = $isFromRelation;
61 | }
62 |
63 | public function getRelation()
64 | {
65 | return implode('.', array_slice($this->nameExploded, 0, count($this->nameExploded) - 1));
66 | }
67 |
68 | public function getColumn()
69 | {
70 | return $this->isCustom() ? $this->customSqlRaw : (array_slice($this->nameExploded, -1)[0] ?: null);
71 | }
72 |
73 | public function isCount()
74 | {
75 | return is_null($this->getColumn());
76 | }
77 |
78 | public function isCustom()
79 | {
80 | return !empty($this->customSqlRaw);
81 | }
82 |
83 | /**
84 | * @param string|null $datatype
85 | *
86 | * @return $this
87 | * @throws DatatypeNotFoundException
88 | */
89 | public function setDatatype(?string $datatype)
90 | {
91 | if (!empty($datatype)) {
92 | if (in_array($datatype, static::$primitiveDatatypes)) {
93 | $this->datatype = $datatype;
94 | } else {
95 | throw new DatatypeNotFoundException($datatype);
96 | }
97 | }
98 |
99 |
100 | return $this;
101 | }
102 |
103 | public function setCountCallback(?callable $callback)
104 | {
105 | $this->countCallback = $callback;
106 |
107 | return $this;
108 | }
109 |
110 | public function setCustomSqlRaw(string $sqlRaw)
111 | {
112 | $this->customSqlRaw = $sqlRaw;
113 |
114 | return $this;
115 | }
116 |
117 | /**
118 | * @param array|string|string[] $operators
119 | *
120 | * @return $this
121 | */
122 | public function setExceptedOperators($operators)
123 | {
124 | $this->exceptOperators = Arr::wrap($operators);
125 |
126 | return $this;
127 | }
128 |
129 | public function getDatatype()
130 | {
131 | if (empty($this->datatype)) {
132 | $this->datatype = $this->getFieldCastType($this->name, $this->getModel()->getCasts());
133 | }
134 |
135 | return $this->datatype;
136 | }
137 |
138 | /**
139 | * @return Model
140 | */
141 | public function getModel()
142 | {
143 | $model = $this->model;
144 |
145 | if ($this->isFromRelation()) {
146 | foreach (explode('.', $this->getRelation()) as $relation) {
147 | $model = $model->{$relation}()->getRelated();
148 | }
149 | }
150 |
151 | return $model;
152 | }
153 |
154 | /**
155 | * Determined if the operator does not belong to excepted operators
156 | *
157 | * @param string $operator
158 | *
159 | * @return bool
160 | */
161 | public function isAllowedOperator(string $operator)
162 | {
163 | return !in_array($operator, $this->exceptOperators);
164 | }
165 |
166 | public function getScopeFunctionName()
167 | {
168 | $prefix = config('advanced_filter.prefix_scope_function', 'where');
169 | $column = ucfirst(Str::camel($this->getColumn()));
170 |
171 | return "{$prefix}{$column}";
172 | }
173 |
174 | public function getScopeRelationFunctionName()
175 | {
176 | $prefix = config('advanced_filter.prefix_scope_function', 'where');
177 | $relation = ucfirst(Str::camel(str_replace('.', '_', $this->getRelation())));
178 |
179 | return "{$prefix}{$relation}";
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/Fields/FieldCast.php:
--------------------------------------------------------------------------------
1 | isDatetime($casts[$field])) {
33 | return 'datetime';
34 | }
35 |
36 | if ($this->isDate($casts[$field])) {
37 | return 'date';
38 | }
39 |
40 | if ($this->isNumeric($casts[$field])) {
41 | return 'numeric';
42 | }
43 |
44 | if ($this->isBoolean($casts[$field])) {
45 | return 'boolean';
46 | }
47 |
48 | }
49 |
50 | return 'string';
51 | }
52 |
53 | protected function isDatetime(string $cast)
54 | {
55 | return Str::startsWith($cast, 'datetime');
56 | }
57 |
58 | protected function isDate(string $cast)
59 | {
60 | return Str::startsWith($cast, 'date');
61 | }
62 |
63 | protected function isNumeric(string $cast)
64 | {
65 | return Str::startsWith($cast, [
66 | 'int',
67 | 'integer',
68 | 'real',
69 | 'float',
70 | 'double',
71 | 'decimal',
72 | ]);
73 | }
74 |
75 | protected function isBoolean(string $cast)
76 | {
77 | return Str::startsWith($cast, ['bool', 'boolean']);
78 | }
79 |
80 | public function getCastedValue($value)
81 | {
82 | if (is_null($value) || is_array($value)) {
83 | return $value;
84 | }
85 |
86 | // return null if the value is empty and "empty_as_null" config value is true
87 | if (config('advanced_filter.empty_as_null', false) && empty($value)) {
88 | return null;
89 | }
90 |
91 | switch ($this->getDatatype()) {
92 | case 'numeric':
93 | return (float)$value;
94 | case 'string':
95 | return (string)$value;
96 | case 'boolean':
97 | return (bool)$value;
98 | case 'date':
99 | return $this->asDateTime($value)->startOfDay();
100 | case 'datetime':
101 | return $this->asDateTime($value);
102 | case 'timestamp':
103 | return $this->asDateTime($value)->getTimestamp();
104 | }
105 |
106 | return $value;
107 | }
108 |
109 | /**
110 | * Return a timestamp as DateTime object.
111 | *
112 | * @param mixed $value
113 | * @return \Illuminate\Support\Carbon
114 | */
115 | protected function asDateTime($value)
116 | {
117 | // If this value is already a Carbon instance, we shall just return it as is.
118 | // This prevents us having to re-instantiate a Carbon instance when we know
119 | // it already is one, which wouldn't be fulfilled by the DateTime check.
120 | if ($value instanceof CarbonInterface) {
121 | return Date::instance($value);
122 | }
123 |
124 | // If the value is already a DateTime instance, we will just skip the rest of
125 | // these checks since they will be a waste of time, and hinder performance
126 | // when checking the field. We will just return the DateTime right away.
127 | if ($value instanceof DateTimeInterface) {
128 | return Date::parse(
129 | $value->format('Y-m-d H:i:s.u'), $value->getTimezone()
130 | );
131 | }
132 |
133 | // If this value is an integer, we will assume it is a UNIX timestamp's value
134 | // and format a Carbon object from this timestamp. This allows flexibility
135 | // when defining your date fields as they might be UNIX timestamps here.
136 | if (is_numeric($value)) {
137 | return Date::createFromTimestamp($value);
138 | }
139 |
140 | // If the value is in simply year, month, day format, we will instantiate the
141 | // Carbon instances from that format. Again, this provides for simple date
142 | // fields on the database, while still supporting Carbonized conversion.
143 | if ($this->isStandardDateFormat($value)) {
144 | return Date::instance(Carbon::createFromFormat('Y-m-d', $value)->startOfDay());
145 | }
146 |
147 | $format = $this->getDateFormat();
148 |
149 | // Finally, we will just assume this date is in the format used by default on
150 | // the database connection and use that format to create the Carbon object
151 | // that is returned back out to the developers after we convert it here.
152 | if (Date::hasFormat($value, $format)) {
153 | return Date::createFromFormat($format, $value);
154 | }
155 |
156 | return Date::parse($value);
157 | }
158 |
159 | /**
160 | * Determine if the given value is a standard date format.
161 | *
162 | * @param string $value
163 | * @return bool
164 | */
165 | protected function isStandardDateFormat($value)
166 | {
167 | return preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value);
168 | }
169 |
170 | /**
171 | * Get the format for database stored dates.
172 | *
173 | * @return string
174 | */
175 | public function getDateFormat()
176 | {
177 | return $this->getModel()->getDateFormat() ?: $this->getModel()->getConnection()->getQueryGrammar()->getDateFormat();
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/Fields/FieldsFactory.php:
--------------------------------------------------------------------------------
1 | model = $model;
28 |
29 | $this->setFields($fields);
30 | }
31 |
32 | /**
33 | * @param string|string[] $fields
34 | *
35 | * @return $this
36 | */
37 | public function setFields($fields)
38 | {
39 | $fields = Arr::wrap($fields);
40 |
41 | foreach ($fields as $field => $alias) {
42 | if (is_int($field)) {
43 | $field = $alias;
44 | }
45 |
46 | $this->fields[$field] = $alias;
47 | }
48 |
49 | return $this;
50 | }
51 |
52 | /**
53 | * @param string|null $datatype
54 | *
55 | * @return FieldsFactory
56 | */
57 | public function setDatatype(?string $datatype)
58 | {
59 | $this->datatype = $datatype;
60 |
61 | return $this;
62 | }
63 |
64 | /**
65 | * @param string|string[] $exceptedOperators
66 | *
67 | * @return $this
68 | */
69 | public function setExceptedOperators($exceptedOperators)
70 | {
71 | $this->exceptedOperators = Arr::wrap($exceptedOperators);
72 |
73 | return $this;
74 | }
75 |
76 | /**
77 | * @return Field[]
78 | * @throws DatatypeNotFoundException
79 | */
80 | public function getFields()
81 | {
82 | $fields = [];
83 | foreach ($this->fields as $field => $alias) {
84 | $fields[] = (new Field($this->model, $field, $alias))
85 | ->setExceptedOperators($this->exceptedOperators)
86 | ->setDatatype($this->datatype);
87 | }
88 |
89 | return $fields;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Fields/HasFields.php:
--------------------------------------------------------------------------------
1 | [], 'operator' => null];
19 |
20 | /**
21 | * Add a normal/relational field
22 | *
23 | * @param string $field
24 | * @param string|null $alias
25 | * @param bool|null $inRelation
26 | *
27 | * @return Field
28 | */
29 | public function addField(string $field, string $alias = null, ?bool $inRelation = null)
30 | {
31 | $field = new Field($this, $field, $alias, $inRelation);
32 |
33 | $this->fields[$field->name] = $field;
34 |
35 | return $field;
36 | }
37 |
38 | /**
39 | * Add normal/relational fields
40 | *
41 | * @param $fields
42 | *
43 | * @return FieldsFactory
44 | */
45 | public function addFields($fields)
46 | {
47 | $fieldFactory = new FieldsFactory($this, $fields);
48 |
49 | $this->fieldsFactories[] = $fieldFactory;
50 |
51 | return $fieldFactory;
52 | }
53 |
54 | /**
55 | * Add a count field
56 | *
57 | * @param string $relation
58 | * @param string|null $alias
59 | * @param callable|null $callback
60 | *
61 | * @throws DatatypeNotFoundException
62 | */
63 | public function addCountField(string $relation, string $alias = null, callable $callback = null)
64 | {
65 | $field = new Field($this, "{$relation}.", $alias ?: $this->getCountFieldAlias($relation));
66 |
67 | $this->fields[$field->name] = $field->setDatatype('numeric')->setCountCallback($callback);
68 | }
69 |
70 | /**
71 | * Add a custom field
72 | *
73 | * @param string $alias
74 | * @param string $sqlRaw
75 | * @param null $relation
76 | *
77 | * @return Field
78 | */
79 | public function addCustomField(string $alias, string $sqlRaw, $relation = null)
80 | {
81 | $field = new Field($this, $relation ? "{$relation}.{$alias}" : $alias, $alias);
82 |
83 | $this->fields[$field->name] = $field->setCustomSqlRaw($sqlRaw);
84 |
85 | return $field;
86 | }
87 |
88 | public function addGeneralSearch(array $fields, string $operator = null)
89 | {
90 | $this->generalSearch = ['fields' => $fields, 'operator' => $operator];
91 | }
92 |
93 | /**
94 | * Resolve factories fields and set fields aliases
95 | *
96 | * @throws DatatypeNotFoundException
97 | */
98 | private function resolveFields()
99 | {
100 | // add fields from factories to fields
101 | foreach ($this->fieldsFactories as $fieldsFactory) {
102 | foreach ($fieldsFactory->getFields() as $field) {
103 | if (!array_key_exists($field->name, $this->fields)) {
104 | $this->fields[$field->name] = $field;
105 | }
106 | }
107 | }
108 |
109 | // set aliases fields
110 | foreach ($this->fields as $field) {
111 | $this->fieldsAliases[$field->alias] = $field->name;
112 | }
113 | }
114 |
115 | /**
116 | * Determined if the alias exists in filterable fields
117 | *
118 | * @param string $alias
119 | *
120 | * @return Field|bool the alias does not exist
121 | */
122 | protected function getFilterableField(string $alias)
123 | {
124 | if (array_key_exists($alias, $this->fieldsAliases)) {
125 | return $this->fields[$this->fieldsAliases[$alias]];
126 | }
127 |
128 | return false;
129 | }
130 |
131 | /**
132 | * Get default alias for count field from a relation
133 | *
134 | * @param string $relation
135 | *
136 | * @return string
137 | */
138 | private function getCountFieldAlias(string $relation)
139 | {
140 | return Str::snake($relation) . '_count';
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/Filter.php:
--------------------------------------------------------------------------------
1 | filters[] = ['field' => $fieldName, 'operator' => $operator, 'value' => $value];
19 |
20 | return $this;
21 | }
22 |
23 | public function getFilters()
24 | {
25 | return $this->filters;
26 | }
27 |
28 | public function setConjunction(string $conjunction)
29 | {
30 | $this->conjunction = $conjunction;
31 |
32 | return $this;
33 | }
34 |
35 | public function getConjunction()
36 | {
37 | return $this->conjunction;
38 | }
39 |
40 | /**
41 | * @return null
42 | */
43 | public function getGeneralSearch()
44 | {
45 | return $this->generalSearch;
46 | }
47 |
48 | /**
49 | * @param string|null $generalSearch
50 | */
51 | public function setGeneralSearch(?string $generalSearch): void
52 | {
53 | $this->generalSearch = $generalSearch;
54 | }
55 |
56 | public static function createFromRequest(Request $request = null)
57 | {
58 | $request = $request ?: request();
59 |
60 | $filterRequest = QueryFormat::factory($request);
61 | $filterRequest->setConjunction($filterRequest->getConjunctionFromRequest($request));
62 | $filterRequest->setGeneralSearch($filterRequest->getGeneralSearchFromRequest($request));
63 |
64 | return $filterRequest;
65 | }
66 |
67 | private function getConjunctionFromRequest(Request $request = null)
68 | {
69 | $paramConjunctionName = config('advanced_filter.param_conjunction_name', 'conjunction');
70 | $defaultConjunction = config('advanced_filter.default_conjunction', 'and');
71 |
72 | return $request ?
73 | $request->input($paramConjunctionName, $defaultConjunction) :
74 | request($paramConjunctionName, $defaultConjunction);
75 | }
76 |
77 | private function getGeneralSearchFromRequest(Request $request = null)
78 | {
79 | $paramGeneralSearchName = config('advanced_filter.param_general_search_name', 'query');
80 |
81 | return $request ?
82 | $request->input($paramGeneralSearchName) :
83 | request($paramGeneralSearchName);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/Filterable.php:
--------------------------------------------------------------------------------
1 | $aliases) {
33 | if (is_int($operator)) {
34 | $operator = $aliases;
35 | }
36 |
37 | $operatorClass = $this->getOperatorsNamespace() . $operator; // class path of the operator
38 |
39 | try {
40 | /** @var Operator $operator */
41 | $operator = new $operatorClass;
42 | $operator->setAliases(Arr::wrap($aliases));
43 |
44 | $this->bindOperator($operator);
45 | } catch (Error $exception) {
46 | throw new OperatorNotFoundException($operator);
47 | }
48 | }
49 |
50 | return $this;
51 | }
52 |
53 | /**
54 | * Bind Custom Operators to Builder
55 | *
56 | * @return $this
57 | */
58 | private function bindCustomOperators()
59 | {
60 | $customOperators = config('advanced_filter.custom_operators', []);
61 | foreach ($customOperators as $customOperator => $aliases) {
62 | if (is_int($customOperator)) {
63 | $customOperator = $aliases;
64 | }
65 |
66 | /** @var Operator $operator */
67 | $operator = new $customOperator;
68 | if ($operator instanceof Operator) {
69 | $operator->setAliases(Arr::wrap($aliases));
70 |
71 | $this->bindOperator($operator);
72 | } else {
73 | throw new InvalidArgumentException('Custom operator must be instance of Operator');
74 | }
75 | }
76 |
77 | return $this;
78 | }
79 |
80 | /**
81 | * Bind operators to Build by using macros
82 | *
83 | * @param Operator $operator
84 | */
85 | private function bindOperator(Operator $operator)
86 | {
87 | // macro to apply operator of field
88 | // TODO: maybe from config
89 | Builder::macro('applyOperator', function (string $operator, $field, $value, string $conjunction = 'and') {
90 | return $this->{Operator::getFunction($operator)}($field, $value, $conjunction);
91 | });
92 |
93 | Builder::macro(Operator::getFunction($operator->name), function (...$parameters) use ($operator) {
94 | // convert string field to Field class
95 | $field = $parameters[0];
96 | if (is_string($parameters[0])) {
97 | $field = new Field($this->getModel(), $field);
98 |
99 | $parameters[0] = $field;
100 | }
101 |
102 | // push Builder on first index
103 | array_unshift($parameters, $this);
104 |
105 | return $operator->execute(...$parameters);
106 | });
107 |
108 | foreach ($operator->aliases as $alias) {
109 | $this->operatorAliases[$alias] = $operator->name;
110 | }
111 | }
112 |
113 | /**
114 | * Get default operators namespace
115 | *
116 | * @return string
117 | */
118 | private function getOperatorsNamespace()
119 | {
120 | return __NAMESPACE__ . '\\Operators\\';
121 | }
122 |
123 | /**
124 | * Get the operator name from operator alias
125 | *
126 | * if the alias does not exists it will return the default operator from config file
127 | *
128 | * @param string $operatorAlias
129 | *
130 | * @return string
131 | */
132 | public function getOperatorFromAliases(string $operatorAlias)
133 | {
134 | return array_key_exists($operatorAlias, $this->operatorAliases) ?
135 | $this->operatorAliases[$operatorAlias] :
136 | config('advanced_filter.default_operator', 'Equal');
137 | }
138 |
139 | public function initializeFilterable()
140 | {
141 | $this->bindOperators();
142 |
143 | $this->bindCustomOperators();
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/HasFilter.php:
--------------------------------------------------------------------------------
1 | apply($builder, $filterRequest);
40 |
41 | $builder = $this->applyGeneralSearch($builder, $filterRequest);
42 |
43 | return $builder;
44 | }
45 |
46 | /**
47 | * Filter fields from request
48 | *
49 | * @param Builder $builder
50 | * @param FilterRequest $filterRequest
51 | *
52 | * @return Builder
53 | * @throws UnsupportedDriverException
54 | */
55 | private function apply(Builder $builder, FilterRequest $filterRequest)
56 | {
57 | $conjunction = $filterRequest->getConjunction();
58 |
59 | foreach ($filterRequest->getFilters() as $filter) {
60 | if ($field = $this->getFilterableField($filter['field'])) {
61 |
62 | $operator = $this->getOperatorFromAliases($filter['operator']);
63 | $value = $field->getCastedValue($filter['value']);
64 |
65 | // don't filter if the operator is in excepted operators
66 | if (!$field->isAllowedOperator($operator)) {
67 | continue;
68 | }
69 |
70 | $builder = $this->filterField($builder, $field, $operator, $value, $conjunction);
71 | }
72 | }
73 |
74 | return $builder;
75 | }
76 |
77 | /**
78 | * Filter general search from the request
79 | *
80 | * @param Builder $builder
81 | * @param FilterRequest $filterRequest
82 | *
83 | * @return Builder
84 | * @throws UnsupportedDriverException
85 | */
86 | private function applyGeneralSearch(Builder $builder, FilterRequest $filterRequest)
87 | {
88 | if (!empty($filterRequest->getGeneralSearch())) {
89 | $operator = $this->generalSearch['operator'] ?: config('advanced_filter.default_general_search_operator');
90 |
91 | $builder->where(function (Builder $builder) use ($filterRequest, $operator) {
92 | foreach ($this->generalSearch['fields'] as $fieldName) {
93 | $field = new Field($builder->getModel(), $fieldName);
94 |
95 | $this->filterField($builder, $field, $operator, $filterRequest->getGeneralSearch(), 'or');
96 | }
97 | });
98 | }
99 |
100 | return $builder;
101 | }
102 |
103 | /**
104 | * Filter a field
105 | *
106 | * @param Builder $builder
107 | * @param Field $field
108 | * @param $operator
109 | * @param $value
110 | * @param string $conjunction
111 | *
112 | * @return Builder
113 | * @throws UnsupportedDriverException
114 | */
115 | private function filterField(Builder $builder, Field $field, $operator, $value, $conjunction = 'and')
116 | {
117 | // apply the filter inside the relation if the field from relation
118 | if ($field->isFromRelation()) {
119 | // apply on custom scope if the relation has a scope
120 | if ($this->modelHasScope($builder, $field->getScopeRelationFunctionName())) {
121 | return $builder->{$field->getScopeRelationFunctionName()}($field, $operator, $value, $conjunction);
122 | } else {
123 | if ($builder->getConnection()->getName() == 'mongodb') {
124 | throw new UnsupportedDriverException('MongoDB', 'relational');
125 | }
126 |
127 | return $builder->has($field->getRelation(), '>=', 1, $conjunction,
128 | function (Builder $builder) use ($field, $value, $operator) {
129 | // consider as a non relation field inside the relation
130 | return $this->filterNonRelationalField($builder, $field, $operator, $value);
131 | }
132 | );
133 | }
134 | } else {
135 | // a non relational field
136 | return $this->filterNonRelationalField($builder, $field, $operator, $value, $conjunction);
137 | }
138 | }
139 |
140 | /**
141 | * Filter a non relational field
142 | *
143 | * it checks if the field has a custom scope
144 | *
145 | * @param Builder $builder
146 | * @param Field $field
147 | * @param $operator
148 | * @param $value
149 | * @param string $conjunction
150 | *
151 | * @return mixed
152 | */
153 | private function filterNonRelationalField(Builder $builder, Field $field, $operator, $value, $conjunction = 'and')
154 | {
155 | // apply on custom scope if the field has a scope
156 | if ($this->modelHasScope($builder, $field->getScopeFunctionName())) {
157 | return $builder->{$field->getScopeFunctionName()}($field, $operator, $value, $conjunction);
158 | }
159 |
160 | return $builder->{Operator::getFunction($operator)}($field, $value, $conjunction);
161 | }
162 |
163 | /**
164 | * Determine if the given model has a scope.
165 | *
166 | * @param Builder $builder
167 | * @param $scopeName
168 | * @return bool
169 | */
170 | private function modelHasScope(Builder $builder, $scopeName)
171 | {
172 | return $builder->getModel() && method_exists($builder->getModel(), 'scope' . ucfirst($scopeName));
173 | }
174 |
175 | public function initializeHasFilter()
176 | {
177 | $this->setupFilter();
178 |
179 | $this->resolveFields();
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/Operators/Between.php:
--------------------------------------------------------------------------------
1 | getSqlValue($value);
21 |
22 | if (empty($value)) {
23 | return $builder;
24 | }
25 |
26 | $values = ['from' => $field->getCastedValue($value['from']), 'to' => $field->getCastedValue($value['to'])];
27 |
28 | if ($field->getDatatype() == 'date') {
29 | $castInDB = config('advanced_filter.cast_db_date', false);
30 | if ($castInDB) {
31 | return $builder->where(function (Builder $builder) use ($values, $field, $value) {
32 | $builder->whereDate($field->getColumn(), '>=', $values['from'])
33 | ->whereDate($field->getColumn(), '<=', $values['to']);
34 | }, null, null, $conjunction);
35 | }
36 |
37 | $values['to'] = $values['to']->endOfDay(); // when using the between operator
38 | }
39 |
40 | return $builder->whereBetween($field->getColumn(), array_values($values), $conjunction);
41 | }
42 |
43 | /**
44 | * @inheritDoc
45 | */
46 | public function getSqlOperator(): string
47 | {
48 | return 'BETWEEN';
49 | }
50 |
51 | protected function getSqlValue($value)
52 | {
53 | if (!is_array($value) || !array_key_exists('from', $value) || !array_key_exists('to', $value)) {
54 | throw new InvalidArgumentException('The between value must be array that contains "from" and "to" keys');
55 | }
56 |
57 | return $value;
58 | }
59 |
60 | public function applyOnCustom(Builder $builder, Field $field, $value, string $conjunction = 'and'): Builder
61 | {
62 | if (empty($value)) {
63 | return $builder;
64 | }
65 |
66 | $sql = "{$field->getColumn()} {$this->getSqlOperator()} ";
67 |
68 | if ($field->getDatatype() == 'numeric') {
69 | $sql .= '(? + 0.0) and (? + 0.0)';
70 | } else {
71 | $sql .= '? and ?';
72 | }
73 |
74 | return $builder->whereRaw($sql, [array_values($this->getSqlValue($value))], $conjunction);
75 | }
76 |
77 | public function applyOnCount(Builder $builder, Field $field, $value, string $conjunction = 'and'): Builder
78 | {
79 | throw new UnsupportedOperatorException($this->name, 'count');
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Operators/Contains.php:
--------------------------------------------------------------------------------
1 | where($field->getColumn(), $this->getSqlOperator(), $this->getSqlValue($value), $conjunction);
19 | }
20 |
21 | /**
22 | * @inheritDoc
23 | */
24 | public function getSqlOperator(): string
25 | {
26 | return 'LIKE';
27 | }
28 |
29 | protected function getSqlValue($value)
30 | {
31 | return "%{$value}%";
32 | }
33 |
34 | public function applyOnCount(Builder $builder, Field $field, $value, string $conjunction = 'and'): Builder
35 | {
36 | throw new UnsupportedOperatorException($this->name, 'count');
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Operators/EndsWith.php:
--------------------------------------------------------------------------------
1 | whereNull($field->getColumn(), $conjunction);
21 | }
22 |
23 | if ($field->getDatatype() == 'date') {
24 | $castInDB = config('advanced_filter.cast_db_date', false);
25 |
26 | if ($castInDB) {
27 | return $builder->whereDate($field->getColumn(), $this->getSqlOperator(), $value, $conjunction);
28 | } else {
29 | // "between" will not keep indexing on the column, just as an option :)
30 | return $builder->whereBetween($field->getColumn(), [$value, (clone $value)->endOfDay()], $conjunction);
31 | }
32 | }
33 |
34 | return $builder->where($field->getColumn(), $this->getSqlOperator(), $value, $conjunction);
35 | }
36 |
37 | public function getSqlOperator(): string
38 | {
39 | return '=';
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Operators/GreaterThan.php:
--------------------------------------------------------------------------------
1 | getColumn();
18 |
19 | if ($field->getDatatype() == 'date') {
20 | $castInDB = config('advanced_filter.cast_db_date', false);
21 |
22 | if ($castInDB) {
23 | return $builder->whereDate($column, '>', $value, $conjunction);
24 | }
25 |
26 | $value = $value->endOfDay();
27 | }
28 |
29 | return $builder->where($column, '>', $value, $conjunction);
30 | }
31 |
32 | public function getSqlOperator(): string
33 | {
34 | return '>';
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Operators/GreaterThanOrEqual.php:
--------------------------------------------------------------------------------
1 | getColumn();
18 |
19 | if ($field->getDatatype() == 'date') {
20 | $castInDB = config('advanced_filter.cast_db_date', false);
21 |
22 | if ($castInDB) {
23 | return $builder->whereDate($column, '>=', $value, $conjunction);
24 | }
25 | }
26 |
27 | return $builder->where($column, '>=', $value, $conjunction);
28 | }
29 |
30 | public function getSqlOperator(): string
31 | {
32 | return '>=';
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Operators/In.php:
--------------------------------------------------------------------------------
1 | getCastedValue($v);
28 | }, $this->getSqlValue($value));
29 |
30 | if ($field->getDatatype() == 'date') {
31 | return $this->applyOnDate($builder, $field, $values, $conjunction);
32 | }
33 |
34 | // to use in NotIn operator
35 | $notIn = $this->getSqlOperator() != 'IN';
36 |
37 | return $builder->whereIn($field->getColumn(), $values, $conjunction, $notIn);
38 | }
39 |
40 | /**
41 | * @inheritDoc
42 | */
43 | public function getSqlOperator(): string
44 | {
45 | return 'IN';
46 | }
47 |
48 | protected function getSqlValue($value)
49 | {
50 | return Arr::wrap($value);
51 | }
52 |
53 | public function applyOnCustom(Builder $builder, Field $field, $value, string $conjunction = 'and'): Builder
54 | {
55 | if (empty($value)) {
56 | return $builder;
57 | }
58 |
59 | $value = implode(",", $this->getSqlValue($value));
60 |
61 | $sql = "{$field->getColumn()} {$this->getSqlOperator()} ({$value})";
62 |
63 | return $builder->whereRaw($sql, [], $conjunction);
64 | }
65 |
66 | public function applyOnCount(Builder $builder, Field $field, $value, string $conjunction = 'and'): Builder
67 | {
68 | throw new UnsupportedOperatorException($this->name, 'count');
69 | }
70 |
71 | private function applyOnDate(Builder $builder, Field $field, $values, string $conjunction = 'and')
72 | {
73 | if ($builder->getConnection()->getName() == 'mongodb') {
74 | throw new UnsupportedDriverException('MongoDB', 'In operator on Date');
75 | }
76 |
77 | // cast to date string
78 | $values = array_map(function ($v) use ($field) {
79 | return $v->toDateString();
80 | }, $values);
81 |
82 | if ($builder->getConnection()->getName() == 'sqlite') {
83 | return $this->applyDateOnSQLite($builder, $field->getColumn(), $values, $conjunction);
84 | }
85 |
86 | $builder->getQuery()->wheres[] = [
87 | 'boolean' => $conjunction,
88 | 'type' => 'date',
89 | 'column' => $field->getColumn(),
90 | 'operator' => $this->getSqlOperator(),
91 | 'value' => new Expression("('" . implode("', '", $values) . "')"),
92 | ];
93 |
94 | return $builder;
95 | }
96 |
97 | private function applyDateOnSQLite(Builder $builder, string $column, $values, string $conjunction = 'and')
98 | {
99 | $bind = implode(',', array_fill(0, count($values), '?'));
100 |
101 | $sql = "strftime('%Y-%m-%d', `{$column}`) {$this->getSqlOperator()} ({$bind})";
102 |
103 | return $builder->whereRaw($sql, $values, $conjunction);
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/Operators/LessThan.php:
--------------------------------------------------------------------------------
1 | getColumn();
18 |
19 | if ($field->getDatatype() == 'date') {
20 | $castInDB = config('advanced_filter.cast_db_date', false);
21 |
22 | if ($castInDB) {
23 | return $builder->whereDate($column, '<', $value, $conjunction);
24 | }
25 |
26 | $value = $value->startOfDay();
27 | }
28 |
29 | return $builder->where($column, '<', $value, $conjunction);
30 | }
31 |
32 | public function getSqlOperator(): string
33 | {
34 | return '<';
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Operators/LessThanOrEqual.php:
--------------------------------------------------------------------------------
1 | getColumn();
18 |
19 | if ($field->getDatatype() == 'date') {
20 | $castInDB = config('advanced_filter.cast_db_date', false);
21 |
22 | if ($castInDB) {
23 | return $builder->whereDate($column, '<=', $value, $conjunction);
24 | }
25 | }
26 |
27 | return $builder->where($column, '<=', $value, $conjunction);
28 | }
29 |
30 | public function getSqlOperator(): string
31 | {
32 | return '<=';
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Operators/NotContains.php:
--------------------------------------------------------------------------------
1 | whereNotNull($field->getColumn(), $conjunction);
16 | }
17 |
18 | if ($field->getDatatype() == 'date') {
19 | $castInDB = config('advanced_filter.cast_db_date', false);
20 |
21 | if ($castInDB) {
22 | return $builder->whereDate($field->getColumn(), $this->getSqlOperator(), $value, $conjunction);
23 | } else {
24 | return $builder->whereNotBetween($field->getColumn(), [$value, (clone $value)->endOfDay()], $conjunction);
25 | }
26 | }
27 |
28 | if ($field->getDatatype() == 'datetime') {
29 | return $builder->whereNotBetween($field->getColumn(), [$value, (clone $value)->endOfMinute()], $conjunction);
30 | }
31 |
32 | return $builder->where($field->getColumn(), $this->getSqlOperator(), $value, $conjunction);
33 | }
34 |
35 | public function getSqlOperator(): string
36 | {
37 | return '!=';
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Operators/NotIn.php:
--------------------------------------------------------------------------------
1 | has($field->getRelation(), $this->getSqlOperator(), $value, $conjunction, $field->countCallback);
52 | }
53 |
54 | /**
55 | * The function calls when trying to apply the operator on a custom field
56 | *
57 | * @param Builder $builder
58 | * @param Field $field
59 | * @param $value
60 | * @param string $conjunction
61 | *
62 | * @return Builder
63 | */
64 | public function applyOnCustom(Builder $builder, Field $field, $value, string $conjunction = 'and'): Builder
65 | {
66 | $sql = "{$field->getColumn()} {$this->getSqlOperator()} ";
67 |
68 | /** @link https://github.com/laravel/framework/issues/31201#issuecomment-615682788 */
69 | if ($field->getDatatype() == 'numeric') {
70 | $sql .= '(? + 0.0)';
71 | } else {
72 | $sql .= '?';
73 | }
74 |
75 | return $builder->whereRaw($sql, [$this->getSqlValue($value)], $conjunction);
76 | }
77 |
78 | /**
79 | * Apply the operator on the field depends on field type
80 | *
81 | * @param Builder $builder
82 | * @param Field $field
83 | * @param $value
84 | * @param string $conjunction
85 | *
86 | * @return Builder
87 | */
88 | public function execute(Builder $builder, Field $field, $value, string $conjunction = 'and'): Builder
89 | {
90 | // if the field is custom, call applyOnCustom function
91 | if ($field->isCustom()) {
92 | if ($builder->getConnection()->getName() == 'mongodb') {
93 | throw new UnsupportedDriverException('MongoDB', 'custom');
94 | }
95 |
96 | return $this->applyOnCustom($builder, $field, $value, $conjunction);
97 | }
98 |
99 | // if the field is count, call applyOnCount function
100 | if ($field->isCount()) {
101 | if ($builder->getConnection()->getName() == 'mongodb') {
102 | throw new UnsupportedDriverException('MongoDB', 'custom');
103 | }
104 |
105 | return $this->applyOnCount($builder, $field, $value, $conjunction);
106 | }
107 |
108 | return $this->apply($builder, $field, $value, $conjunction);
109 | }
110 |
111 | protected function getSqlValue($value)
112 | {
113 | return $value;
114 | }
115 |
116 | public static function getFunction($operatorName)
117 | {
118 | $prefixOperatorFunction = config('advanced_filter.prefix_operator_function', 'filterWhere');
119 | $operatorName = ucfirst($operatorName);
120 |
121 | return "{$prefixOperatorFunction}{$operatorName}";
122 | }
123 |
124 | public function setAliases(array $aliases)
125 | {
126 | $this->aliases = $aliases;
127 | }
128 |
129 | public function __get($name)
130 | {
131 | if ($name == 'name' && empty($this->name)) {
132 | return class_basename($this);
133 | }
134 |
135 | if ($name == 'aliases' && empty($this->aliases)) {
136 | return [mb_strtolower(class_basename($this))];
137 | }
138 |
139 | return $this->{$name};
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/Operators/StartsWith.php:
--------------------------------------------------------------------------------
1 | $filter) {
17 | if (!is_int($fieldName)) {
18 | $operator = $filter[$this->fieldParams['operator']] ?? $this->defaultOperator;
19 | $value = $filter[$this->fieldParams['value']] ?? null;
20 |
21 | $requestFilter->addFilter($fieldName, $operator, $value);
22 | }
23 | }
24 |
25 | return $requestFilter;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/QueryFormats/JsonQueryFormat.php:
--------------------------------------------------------------------------------
1 | fieldParams['field']] ?? null;
20 | if ($fieldName) {
21 | $operator = $filter[$this->fieldParams['operator']] ?? $this->defaultOperator;
22 | $value = $filter[$this->fieldParams['value']] ?? null;
23 |
24 | $requestFilter->addFilter($fieldName, $operator, $value);
25 | }
26 | }
27 |
28 | return $requestFilter;
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/QueryFormats/QueryFormat.php:
--------------------------------------------------------------------------------
1 | fieldParams = config('advanced_filter.field_params');
24 | $this->defaultOperator = config('advanced_filter.default_operator');
25 | }
26 |
27 | public abstract function format($filters): FilterRequest;
28 |
29 | public static function factory(Request $request)
30 | {
31 | if ($custom = static::loadCustomQueryFormat($request)) {
32 | return $custom;
33 | }
34 |
35 | return static::loadQueryFormat($request);
36 | }
37 |
38 | private static function loadCustomQueryFormat(Request $request)
39 | {
40 | $customQueryFormat = config('advanced_filter.custom_query_format');
41 |
42 | if (!empty($customQueryFormat)) {
43 | try {
44 | $customFormat = new $customQueryFormat();
45 | if ($customFormat instanceof QueryFormat) {
46 | return $customFormat->format($request);
47 | }
48 | } catch (\Exception $exception) {
49 | throw new \InvalidArgumentException('must be a valid query format');
50 | }
51 | }
52 |
53 | return false;
54 | }
55 |
56 | private static function loadQueryFormat(Request $request)
57 | {
58 | $filterName = config('advanced_filter.param_filter_name', 'filters');
59 |
60 | switch (static::getQueryFormat()) {
61 | case self::QUERY_FORMAT_JSON:
62 | return (new JsonQueryFormat())->format($request->input($filterName, '[]'));
63 | case self::QUERY_FORMAT_SEPARATOR:
64 | return (new SeparatorQueryFormat())->format($request->all());
65 | case self::QUERY_FORMAT_ARRAY:
66 | return (new ArrayQueryFormat())->format($request->input($filterName, []));
67 | }
68 |
69 | throw new \InvalidArgumentException('must be a valid query format');
70 | }
71 |
72 | public static function getQueryFormat()
73 | {
74 | return substr(config('advanced_filter.query_format', self::QUERY_FORMAT_JSON), 0, 9);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/QueryFormats/SeparatorQueryFormat.php:
--------------------------------------------------------------------------------
1 | getSeparatorFromFormat();
17 |
18 | // convert string parameters to array
19 | $parameters = [];
20 | foreach ($filters as $paramName => $paramValue) {
21 | if (Str::startsWith($paramName, "{$prefix}{$separator}")) {
22 | $stringArray = $this->stringBySeparatorToStringArray($paramName, $separator);
23 | $stringArray .= "={$paramValue}"; // add the parameter value
24 | parse_str($stringArray, $filter); // parse to array
25 |
26 | $parameters = array_merge_recursive($parameters, $filter);
27 | }
28 | }
29 |
30 | // use ArrayQueryFormat
31 | return (new ArrayQueryFormat())->format($parameters[$prefix]);
32 | }
33 |
34 | private function getSeparatorFromFormat()
35 | {
36 | $queryFormat = config('advanced_filter.query_format', 'json');
37 | $defaultSeparator = '^';
38 |
39 | return (explode("separator:", $queryFormat)[1] ?? $defaultSeparator) ?: $defaultSeparator;
40 | }
41 |
42 | /**
43 | * Convert string separated to string array
44 | *
45 | * @param string $string
46 | * @param string $separator
47 | *
48 | * @return string
49 | */
50 | private function stringBySeparatorToStringArray(string $string, string $separator)
51 | {
52 | $stringArray = '';
53 | foreach (explode($separator, $string) as $index => $item) {
54 | if ($index == 0) {
55 | $stringArray .= "{$item}";
56 | } else {
57 | $stringArray .= "[{$item}]";
58 | }
59 | }
60 |
61 | return $stringArray;
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/tests/GeneralSearchTest.php:
--------------------------------------------------------------------------------
1 | get();
19 |
20 | $this->assertCount(2, $orders);
21 |
22 | $this->assertEquals(['LAF_0002', 'LAF_0003'], $orders->pluck('reference')->toArray());
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Models/BaseModel.php:
--------------------------------------------------------------------------------
1 | 'date'];
31 | protected $dates = ['ship_date'];
32 |
33 | public function store()
34 | {
35 | return $this->belongsTo(Store::class);
36 | }
37 |
38 | public function orderLines()
39 | {
40 | return $this->hasMany(OrderLine::class);
41 | }
42 |
43 | public function setupFilter()
44 | {
45 | $this->addFields(['reference' => 'order_number', 'order_date', 'created_at']);
46 | $this->addFields(['subtotal', 'shipping_cost' => 'shipping'])->setDatatype('numeric');
47 | $this->addField('ship_date')->setDatatype('datetime');
48 |
49 | $this->addFields(['store_id', 'store.name' => 'store_name']);
50 | if (env('DB_CONNECTION') == 'sqlite') {
51 | $this->addCustomField('store_reference', '( `stores`.`name` || "-" || `orders`.`reference`)', 'store');
52 | } else {
53 | $this->addCustomField('store_reference', 'CONCAT( `stores`.`name`, "-", `orders`.`reference`)', 'store');
54 | }
55 |
56 |
57 | $this->addCountField('orderLines', 'lines_count');
58 |
59 | $this->addCustomField('line_subtotal', '(`price` * `quantity`)', 'orderLines')->setDatatype('numeric');
60 | $this->addField('orderLines.price', 'line_price');
61 |
62 | $this->addField('orderLines.product_id', 'product_id');
63 | $this->addField('orderLines.product.name', 'product_name');
64 | $this->addField('orderLines.product.sku', 'product_sku');
65 |
66 | $this->addGeneralSearch(['reference', 'orderLines.product.sku']);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/tests/Models/OrderLine.php:
--------------------------------------------------------------------------------
1 | belongsTo(Order::class);
26 | }
27 |
28 | public function product()
29 | {
30 | return $this->belongsTo(Product::class);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/Models/Product.php:
--------------------------------------------------------------------------------
1 | hasMany(OrderLine::class);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/Models/Store.php:
--------------------------------------------------------------------------------
1 | hasMany(Order::class);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Operators/BetweenTest.php:
--------------------------------------------------------------------------------
1 | <","value":' . $subtotal . '}]';
17 | $request = Request::create("test?{$queryFilters}");
18 |
19 | $orders = Order::filter($request)->get();
20 |
21 | $this->assertCount(3, $orders);
22 |
23 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0003'], $orders->pluck('reference')->toArray());
24 | }
25 |
26 | /** @test */
27 | public function it_can_filter_date_fields()
28 | {
29 | $orderDate = '{"from":"2020-09-26", "to":"2020-10-01"}';
30 | $queryFilters = 'filters=[{"field":"order_date","operator":"><","value":' . $orderDate . '}]';
31 | $request = Request::create("test?{$queryFilters}");
32 |
33 | $orders = Order::filter($request)->get();
34 |
35 | $this->assertCount(2, $orders);
36 |
37 | $this->assertEquals(['LAF_0002', 'LAF_0005'], $orders->pluck('reference')->toArray());
38 | }
39 |
40 |
41 | /** @test */
42 | public function it_can_filter_datetime_fields()
43 | {
44 | $shipDate = '{"from":"2020-09-30 05:00:00", "to":"2020-10-02 05:00:00"}';
45 | $queryFilters = 'filters=[{"field":"ship_date","operator":"><","value":' . $shipDate . '}]';
46 | $request = Request::create("test?{$queryFilters}");
47 |
48 | $orders = Order::filter($request)->get();
49 |
50 | $this->assertCount(1, $orders);
51 |
52 | $this->assertEquals(['LAF_0004'], $orders->pluck('reference')->toArray());
53 | }
54 |
55 | /** @test */
56 | public function it_can_filter_custom_fields()
57 | {
58 | $lineSubtotal = '{"from":5.7, "to":8.6}';
59 | $queryFilters = 'filters=[{"field":"line_subtotal","operator":"><","value":' . $lineSubtotal . '}]';
60 | $request = Request::create("test?{$queryFilters}");
61 |
62 | $orders = Order::filter($request)->get();
63 |
64 | $this->assertCount(2, $orders);
65 |
66 | $this->assertEquals(['LAF_0004', 'LAF_0005'], $orders->pluck('reference')->toArray());
67 | }
68 |
69 | /** @test */
70 | public function it_can_filter_count_fields()
71 | {
72 | $linesCount = '{"from":1, "to":2}';
73 | $queryFilters = 'filters=[{"field":"lines_count","operator":"><","value":' . $linesCount . '}]';
74 | $request = Request::create("test?{$queryFilters}");
75 |
76 | $this->expectException(UnsupportedOperatorException::class);
77 |
78 | Order::filter($request)->get();
79 | }
80 |
81 | /** @test */
82 | public function it_can_filter_relation_fields()
83 | {
84 | $productSku = '{"from": 5, "to":10}';
85 | $queryFilters = 'filters=[{"field":"line_price","operator":"><","value":' . $productSku . '}]';
86 | $request = Request::create("test?{$queryFilters}");
87 |
88 | $orders = Order::filter($request)->get();
89 |
90 | $this->assertCount(3, $orders);
91 |
92 | $this->assertEquals(['LAF_0002', 'LAF_0003', 'LAF_0004'], $orders->pluck('reference')->toArray());
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/tests/Operators/ContainsTest.php:
--------------------------------------------------------------------------------
1 | get();
20 |
21 | $this->assertCount(1, $orders);
22 |
23 | $this->assertEquals(['LAF_0005'], $orders->pluck('reference')->toArray());
24 | }
25 |
26 | /** @test */
27 | public function it_can_filter_numeric_fields()
28 | {
29 | $subtotal = 5;
30 | $queryFilters = 'filters=[{"field":"subtotal","operator":"*","value":"' . $subtotal . '"}]';
31 | $request = Request::create("test?{$queryFilters}");
32 |
33 | $orders = Order::filter($request)->get();
34 |
35 | $this->assertCount(3, $orders);
36 |
37 | $this->assertEquals(['LAF_0001', 'LAF_0003', 'LAF_0004'], $orders->pluck('reference')->toArray());
38 | }
39 |
40 | /** @test */
41 | public function it_can_filter_custom_fields()
42 | {
43 | $storeReference = 'Consulting';
44 | $queryFilters = 'filters=[{"field":"store_reference","operator":"*","value":"' . $storeReference . '"}]';
45 | $request = Request::create("test?{$queryFilters}");
46 |
47 | $orders = Order::filter($request)->get();
48 |
49 | $this->assertCount(3, $orders);
50 |
51 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0004'], $orders->pluck('reference')->toArray());
52 | }
53 |
54 | /** @test */
55 | public function it_can_not_filter_count_fields()
56 | {
57 | $linesCount = 2;
58 | $queryFilters = 'filters=[{"field":"lines_count","operator":"*","value":"' . $linesCount . '"}]';
59 | $request = Request::create("test?{$queryFilters}");
60 |
61 | $this->expectException(UnsupportedOperatorException::class);
62 |
63 | Order::filter($request)->get();
64 | }
65 |
66 | /** @test */
67 | public function it_can_filter_relation_fields()
68 | {
69 | $productName = 'Nam';
70 | $queryFilters = 'filters=[{"field":"product_name","operator":"*","value":"' . $productName . '"}]';
71 | $request = Request::create("test?{$queryFilters}");
72 |
73 | $orders = Order::filter($request)->get();
74 |
75 | $this->assertCount(3, $orders);
76 |
77 | $this->assertEquals(['LAF_0001', 'LAF_0003', 'LAF_0005'], $orders->pluck('reference')->toArray());
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/tests/Operators/EndsWithTest.php:
--------------------------------------------------------------------------------
1 | get();
20 |
21 | $this->assertCount(1, $orders);
22 |
23 | $this->assertEquals(['LAF_0005'], $orders->pluck('reference')->toArray());
24 | }
25 |
26 | /** @test */
27 | public function it_can_filter_numeric_fields()
28 | {
29 | // on mysql will not work as expected, because of the decimal digits
30 | $subtotal = env('DB_CONNECTION') == 'sqlite' ? 5 : 50;
31 | $queryFilters = 'filters=[{"field":"subtotal","operator":"$","value":"' . $subtotal . '"}]';
32 | $request = Request::create("test?{$queryFilters}");
33 |
34 | $orders = Order::filter($request)->get();
35 |
36 | if (env('DB_CONNECTION') == 'sqlite') {
37 | $this->assertCount(2, $orders);
38 |
39 | $this->assertEquals(['LAF_0001', 'LAF_0003'], $orders->pluck('reference')->toArray());
40 | } else {
41 | $this->assertCount(1, $orders);
42 |
43 | $this->assertEquals(['LAF_0001'], $orders->pluck('reference')->toArray());
44 | }
45 | }
46 |
47 | /** @test */
48 | public function it_can_filter_custom_fields()
49 | {
50 | $storeReference = 'Consulting-LAF_0002';
51 | $queryFilters = 'filters=[{"field":"store_reference","operator":"$","value":"' . $storeReference . '"}]';
52 | $request = Request::create("test?{$queryFilters}");
53 |
54 | $orders = Order::filter($request)->get();
55 |
56 | $this->assertCount(1, $orders);
57 |
58 | $this->assertEquals(['LAF_0002'], $orders->pluck('reference')->toArray());
59 | }
60 |
61 | /** @test */
62 | public function it_can_not_filter_count_fields()
63 | {
64 | $linesCount = 2;
65 | $queryFilters = 'filters=[{"field":"lines_count","operator":"$","value":"' . $linesCount . '"}]';
66 | $request = Request::create("test?{$queryFilters}");
67 |
68 | $this->expectException(UnsupportedOperatorException::class);
69 |
70 | Order::filter($request)->get();
71 | }
72 |
73 | /** @test */
74 | public function it_can_filter_relation_fields()
75 | {
76 | $productName = 'libero';
77 | $queryFilters = 'filters=[{"field":"product_name","operator":"$","value":"' . $productName . '"}]';
78 | $request = Request::create("test?{$queryFilters}");
79 |
80 | $orders = Order::filter($request)->get();
81 |
82 | $this->assertCount(2, $orders);
83 |
84 | $this->assertEquals(['LAF_0002', 'LAF_0003'], $orders->pluck('reference')->toArray());
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/Operators/EqualsTest.php:
--------------------------------------------------------------------------------
1 | first();
19 |
20 | $this->assertNotNull($order);
21 |
22 | $this->assertEquals($reference, $order->reference);
23 | }
24 |
25 | /** @test */
26 | public function it_can_filter_numeric_fields()
27 | {
28 | $subtotal = 25;
29 | $queryFilters = 'filters=[{"field":"subtotal","operator":"equals","value":"' . $subtotal . '"}]';
30 | $request = Request::create("test?{$queryFilters}");
31 |
32 | $order = Order::filter($request)->first();
33 |
34 | $this->assertNotNull($order);
35 |
36 | $this->assertEquals('LAF_0003', $order->reference);
37 | }
38 |
39 | /** @test */
40 | public function it_can_filter_date_fields()
41 | {
42 | $orderDate = '2020-10-2';
43 | $queryFilters = 'filters=[{"field":"order_date","operator":"equals","value":"' . $orderDate . '"}]';
44 | $request = Request::create("test?{$queryFilters}");
45 |
46 | $orders = Order::filter($request)->get();
47 |
48 | $this->assertCount(2, $orders);
49 |
50 | $this->assertEquals(['LAF_0001', 'LAF_0003'], $orders->pluck('reference')->toArray());
51 | }
52 |
53 |
54 | /** @test */
55 | public function it_can_filter_datetime_fields()
56 | {
57 | $shipDate = '2020-10-03 10:30:00';
58 | $queryFilters = 'filters=[{"field":"ship_date","operator":"equals","value":"' . $shipDate . '"}]';
59 | $request = Request::create("test?{$queryFilters}");
60 |
61 | $orders = Order::filter($request)->get();
62 |
63 | $this->assertCount(1, $orders);
64 |
65 | $this->assertEquals(['LAF_0002'], $orders->pluck('reference')->toArray());
66 | }
67 |
68 | /** @test */
69 | public function it_can_filter_custom_fields()
70 | {
71 | $lineSubtotal = 8.6;
72 | $queryFilters = 'filters=[{"field":"line_subtotal","operator":"equals","value":"' . $lineSubtotal . '"}]';
73 | $request = Request::create("test?{$queryFilters}");
74 |
75 | $orders = Order::filter($request)->get();
76 |
77 | $this->assertCount(1, $orders);
78 |
79 | $this->assertEquals(['LAF_0005'], $orders->pluck('reference')->toArray());
80 | }
81 |
82 | /** @test */
83 | public function it_can_filter_count_fields()
84 | {
85 | $linesCount = 2;
86 | $queryFilters = 'filters=[{"field":"lines_count","operator":"equals","value":"' . $linesCount . '"}]';
87 | $request = Request::create("test?{$queryFilters}");
88 |
89 | $orders = Order::filter($request)->get();
90 |
91 | $this->assertCount(2, $orders);
92 |
93 | $this->assertEquals(['LAF_0002', 'LAF_0003'], $orders->pluck('reference')->toArray());
94 | }
95 |
96 | /** @test */
97 | public function it_can_filter_relation_fields()
98 | {
99 | $storeName = 'Sociis Corporation';
100 | $queryFilters = 'filters=[{"field":"store_name","operator":"equals","value":"' . $storeName . '"}]';
101 | $request = Request::create("test?{$queryFilters}");
102 |
103 | $orders = Order::filter($request)->get();
104 |
105 | $this->assertCount(1, $orders);
106 |
107 | $this->assertEquals(['LAF_0005'], $orders->pluck('reference')->toArray());
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/tests/Operators/GreaterThanOrEqualTest.php:
--------------------------------------------------------------------------------
1 | =","value":"' . $subtotal . '"}]';
16 | $request = Request::create("test?{$queryFilters}");
17 |
18 | $orders = Order::filter($request)->get();
19 |
20 | $this->assertCount(3, $orders);
21 |
22 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0003'], $orders->pluck('reference')->toArray());
23 | }
24 |
25 | /** @test */
26 | public function it_can_filter_date_fields()
27 | {
28 | $orderDate = '2020-10-1';
29 | $queryFilters = 'filters=[{"field":"order_date","operator":">=","value":"' . $orderDate . '"}]';
30 | $request = Request::create("test?{$queryFilters}");
31 |
32 | $orders = Order::filter($request)->get();
33 |
34 | $this->assertCount(3, $orders);
35 |
36 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0003'], $orders->pluck('reference')->toArray());
37 | }
38 |
39 |
40 | /** @test */
41 | public function it_can_filter_datetime_fields()
42 | {
43 | $shipDate = '2020-09-30 5:25:04';
44 | $queryFilters = 'filters=[{"field":"ship_date","operator":">=","value":"' . $shipDate . '"}]';
45 | $request = Request::create("test?{$queryFilters}");
46 |
47 | $orders = Order::filter($request)->get();
48 |
49 | $this->assertCount(2, $orders);
50 |
51 | $this->assertEquals(['LAF_0002', 'LAF_0004'], $orders->pluck('reference')->toArray());
52 | }
53 |
54 | /** @test */
55 | public function it_can_filter_custom_fields()
56 | {
57 | $lineSubtotal = 8.6;
58 | $queryFilters = 'filters=[{"field":"line_subtotal","operator":">=","value":"' . $lineSubtotal . '"}]';
59 | $request = Request::create("test?{$queryFilters}");
60 |
61 | $orders = Order::filter($request)->get();
62 |
63 | $this->assertCount(4, $orders);
64 |
65 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0003', 'LAF_0005'], $orders->pluck('reference')->toArray());
66 | }
67 |
68 | /** @test */
69 | public function it_can_filter_count_fields()
70 | {
71 | $linesCount = 1;
72 | $queryFilters = 'filters=[{"field":"lines_count","operator":">=","value":"' . $linesCount . '"}]';
73 | $request = Request::create("test?{$queryFilters}");
74 |
75 | $orders = Order::filter($request)->get();
76 |
77 | $this->assertCount(5, $orders);
78 |
79 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0003', 'LAF_0004', 'LAF_0005'],
80 | $orders->pluck('reference')->toArray());
81 | }
82 |
83 | /** @test */
84 | public function it_can_filter_relation_fields()
85 | {
86 | $linePrice = 10;
87 | $queryFilters = 'filters=[{"field":"line_price","operator":">=","value":"' . $linePrice . '"}]';
88 | $request = Request::create("test?{$queryFilters}");
89 |
90 | $orders = Order::filter($request)->get();
91 |
92 | $this->assertCount(3, $orders);
93 |
94 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0003'], $orders->pluck('reference')->toArray());
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/tests/Operators/GreaterThanTest.php:
--------------------------------------------------------------------------------
1 | ","value":"' . $subtotal . '"}]';
16 | $request = Request::create("test?{$queryFilters}");
17 |
18 | $orders = Order::filter($request)->get();
19 |
20 | $this->assertCount(2, $orders);
21 |
22 | $this->assertEquals(['LAF_0002', 'LAF_0003'], $orders->pluck('reference')->toArray());
23 | }
24 |
25 | /** @test */
26 | public function it_can_filter_date_fields()
27 | {
28 | $orderDate = '2020-10-1';
29 | $queryFilters = 'filters=[{"field":"order_date","operator":">","value":"' . $orderDate . '"}]';
30 | $request = Request::create("test?{$queryFilters}");
31 |
32 | $orders = Order::filter($request)->get();
33 |
34 | $this->assertCount(2, $orders);
35 |
36 | $this->assertEquals(['LAF_0001', 'LAF_0003'], $orders->pluck('reference')->toArray());
37 | }
38 |
39 |
40 | /** @test */
41 | public function it_can_filter_datetime_fields()
42 | {
43 | // sqlite does not support datetime datatype, but you can use "strftime", I will not support that
44 | if (env('DB_CONNECTION') == 'sqlite') {
45 | return $this->assertTrue(true);
46 | }
47 |
48 | $shipDate = '2020-09-30 5:25:04';
49 | $queryFilters = 'filters=[{"field":"ship_date","operator":">","value":"' . $shipDate . '"}]';
50 | $request = Request::create("test?{$queryFilters}");
51 |
52 | $orders = Order::filter($request)->get();
53 |
54 | $this->assertCount(1, $orders);
55 |
56 | $this->assertEquals(['LAF_0002'], $orders->pluck('reference')->toArray());
57 | }
58 |
59 | /** @test */
60 | public function it_can_filter_custom_fields()
61 | {
62 | $lineSubtotal = 8.6;
63 | $queryFilters = 'filters=[{"field":"line_subtotal","operator":">","value":"' . $lineSubtotal . '"}]';
64 | $request = Request::create("test?{$queryFilters}");
65 |
66 | $orders = Order::filter($request)->get();
67 |
68 | $this->assertCount(3, $orders);
69 |
70 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0003'], $orders->pluck('reference')->toArray());
71 | }
72 |
73 | /** @test */
74 | public function it_can_filter_count_fields()
75 | {
76 | $linesCount = 1;
77 | $queryFilters = 'filters=[{"field":"lines_count","operator":">","value":"' . $linesCount . '"}]';
78 | $request = Request::create("test?{$queryFilters}");
79 |
80 | $orders = Order::filter($request)->get();
81 |
82 | $this->assertCount(2, $orders);
83 |
84 | $this->assertEquals(['LAF_0002', 'LAF_0003'], $orders->pluck('reference')->toArray());
85 | }
86 |
87 | /** @test */
88 | public function it_can_filter_relation_fields()
89 | {
90 | $linePrice = 10;
91 | $queryFilters = 'filters=[{"field":"line_price","operator":">","value":"' . $linePrice . '"}]';
92 | $request = Request::create("test?{$queryFilters}");
93 |
94 | $orders = Order::filter($request)->get();
95 |
96 | $this->assertCount(2, $orders);
97 |
98 | $this->assertEquals(['LAF_0001', 'LAF_0003'], $orders->pluck('reference')->toArray());
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/tests/Operators/InTest.php:
--------------------------------------------------------------------------------
1 | get();
20 |
21 | $this->assertCount(2, $orders);
22 |
23 | $this->assertEquals(['LAF_0001', 'LAF_0005'], $orders->pluck('reference')->toArray());
24 | }
25 |
26 | /** @test */
27 | public function it_can_filter_numeric_fields()
28 | {
29 | $subtotal = "[25,5.7]";
30 | $queryFilters = 'filters=[{"field":"subtotal","operator":"|","value":' . $subtotal . '}]';
31 | $request = Request::create("test?{$queryFilters}");
32 |
33 | $orders = Order::filter($request)->get();
34 |
35 | $this->assertCount(2, $orders);
36 |
37 | $this->assertEquals(['LAF_0003', 'LAF_0004'], $orders->pluck('reference')->toArray());
38 | }
39 |
40 | /** @test */
41 | public function it_can_filter_date_fields()
42 | {
43 | $orderDate = '["2020-10-02","2020-09-25"]';
44 | $queryFilters = 'filters=[{"field":"order_date","operator":"|","value":' . $orderDate . '}]';
45 | $request = Request::create("test?{$queryFilters}");
46 |
47 | $orders = Order::filter($request)->get();
48 |
49 | $this->assertCount(3, $orders);
50 |
51 | $this->assertEquals(['LAF_0001', 'LAF_0003', 'LAF_0004'], $orders->pluck('reference')->toArray());
52 | }
53 |
54 |
55 | /** @test */
56 | public function it_can_filter_datetime_fields()
57 | {
58 | $shipDate = '["2020-10-03 10:30:00", "2020-09-30 05:25:04"]';
59 | $queryFilters = 'filters=[{"field":"ship_date","operator":"|","value":' . $shipDate . '}]';
60 | $request = Request::create("test?{$queryFilters}");
61 |
62 | $orders = Order::filter($request)->get();
63 |
64 | $this->assertCount(2, $orders);
65 |
66 | $this->assertEquals(['LAF_0002', 'LAF_0004'], $orders->pluck('reference')->toArray());
67 | }
68 |
69 | /** @test */
70 | public function it_can_filter_custom_fields()
71 | {
72 | $lineSubtotal = "[8.6,5.7]";
73 | $queryFilters = 'filters=[{"field":"line_subtotal","operator":"|","value":' . $lineSubtotal . '}]';
74 | $request = Request::create("test?{$queryFilters}");
75 |
76 | $orders = Order::filter($request)->get();
77 |
78 | $this->assertCount(2, $orders);
79 |
80 | $this->assertEquals(['LAF_0004', 'LAF_0005'], $orders->pluck('reference')->toArray());
81 | }
82 |
83 | /** @test */
84 | public function it_can_filter_count_fields()
85 | {
86 | $linesCount = "[1,2]";
87 | $queryFilters = 'filters=[{"field":"lines_count","operator":"|","value":' . $linesCount . '}]';
88 | $request = Request::create("test?{$queryFilters}");
89 |
90 | $this->expectException(UnsupportedOperatorException::class);
91 |
92 | Order::filter($request)->get();
93 | }
94 |
95 | /** @test */
96 | public function it_can_filter_relation_fields()
97 | {
98 | $productSku = '["2971-KW","KU5-8ZD"]';
99 | $queryFilters = 'filters=[{"field":"product_sku","operator":"|","value":' . $productSku . '}]';
100 | $request = Request::create("test?{$queryFilters}");
101 |
102 | $orders = Order::filter($request)->get();
103 |
104 | $this->assertCount(3, $orders);
105 |
106 | $this->assertEquals(['LAF_0001', 'LAF_0003', 'LAF_0004'], $orders->pluck('reference')->toArray());
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/tests/Operators/LessThanOrEqualTest.php:
--------------------------------------------------------------------------------
1 | get();
19 |
20 | $this->assertCount(3, $orders);
21 |
22 | $this->assertEquals(['LAF_0001', 'LAF_0004', 'LAF_0005'], $orders->pluck('reference')->toArray());
23 | }
24 |
25 | /** @test */
26 | public function it_can_filter_date_fields()
27 | {
28 | $orderDate = '2020-10-1';
29 | $queryFilters = 'filters=[{"field":"order_date","operator":"<=","value":"' . $orderDate . '"}]';
30 | $request = Request::create("test?{$queryFilters}");
31 |
32 | $orders = Order::filter($request)->get();
33 |
34 | $this->assertCount(3, $orders);
35 |
36 | $this->assertEquals(['LAF_0002', 'LAF_0004', 'LAF_0005'], $orders->pluck('reference')->toArray());
37 | }
38 |
39 |
40 | /** @test */
41 | public function it_can_filter_datetime_fields()
42 | {
43 | $shipDate = '2020-09-30 5:25:04';
44 | $queryFilters = 'filters=[{"field":"ship_date","operator":"<=","value":"' . $shipDate . '"}]';
45 | $request = Request::create("test?{$queryFilters}");
46 |
47 | $orders = Order::filter($request)->get();
48 |
49 | $this->assertCount(1, $orders);
50 |
51 | $this->assertEquals(['LAF_0004'], $orders->pluck('reference')->toArray());
52 | }
53 |
54 | /** @test */
55 | public function it_can_filter_custom_fields()
56 | {
57 | $lineSubtotal = 8.6;
58 | $queryFilters = 'filters=[{"field":"line_subtotal","operator":"<=","value":"' . $lineSubtotal . '"}]';
59 | $request = Request::create("test?{$queryFilters}");
60 |
61 | $orders = Order::filter($request)->get();
62 |
63 | $this->assertCount(2, $orders);
64 |
65 | $this->assertEquals(['LAF_0004', 'LAF_0005'], $orders->pluck('reference')->toArray());
66 | }
67 |
68 | /** @test */
69 | public function it_can_filter_count_fields()
70 | {
71 | $linesCount = 2;
72 | $queryFilters = 'filters=[{"field":"lines_count","operator":"<=","value":"' . $linesCount . '"}]';
73 | $request = Request::create("test?{$queryFilters}");
74 |
75 | $orders = Order::filter($request)->get();
76 |
77 | $this->assertCount(5, $orders);
78 |
79 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0003', 'LAF_0004', 'LAF_0005'],
80 | $orders->pluck('reference')->toArray());
81 | }
82 |
83 | /** @test */
84 | public function it_can_filter_relation_fields()
85 | {
86 | $linePrice = 10;
87 | $queryFilters = 'filters=[{"field":"line_price","operator":"<=","value":"' . $linePrice . '"}]';
88 | $request = Request::create("test?{$queryFilters}");
89 |
90 | $orders = Order::filter($request)->get();
91 |
92 | $this->assertCount(4, $orders);
93 |
94 | $this->assertEquals(['LAF_0002', 'LAF_0003', 'LAF_0004', 'LAF_0005'], $orders->pluck('reference')->toArray());
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/tests/Operators/LessThanTest.php:
--------------------------------------------------------------------------------
1 | get();
19 |
20 | $this->assertCount(2, $orders);
21 |
22 | $this->assertEquals(['LAF_0004', 'LAF_0005'], $orders->pluck('reference')->toArray());
23 | }
24 |
25 | /** @test */
26 | public function it_can_filter_date_fields()
27 | {
28 | $orderDate = '2020-10-1';
29 | $queryFilters = 'filters=[{"field":"order_date","operator":"<","value":"' . $orderDate . '"}]';
30 | $request = Request::create("test?{$queryFilters}");
31 |
32 | $orders = Order::filter($request)->get();
33 |
34 | $this->assertCount(2, $orders);
35 |
36 | $this->assertEquals(['LAF_0004', 'LAF_0005'], $orders->pluck('reference')->toArray());
37 | }
38 |
39 |
40 | /** @test */
41 | public function it_can_filter_datetime_fields()
42 | {
43 | $shipDate = '2020-09-30 5:25:04';
44 | $queryFilters = 'filters=[{"field":"ship_date","operator":"<","value":"' . $shipDate . '"}]';
45 | $request = Request::create("test?{$queryFilters}");
46 |
47 | $orders = Order::filter($request)->get();
48 |
49 | $this->assertEmpty($orders);
50 | }
51 |
52 | /** @test */
53 | public function it_can_filter_custom_fields()
54 | {
55 | $lineSubtotal = 8.6;
56 | $queryFilters = 'filters=[{"field":"line_subtotal","operator":"<","value":"' . $lineSubtotal . '"}]';
57 | $request = Request::create("test?{$queryFilters}");
58 |
59 | $orders = Order::filter($request)->get();
60 |
61 | $this->assertCount(1, $orders);
62 |
63 | $this->assertEquals(['LAF_0004'], $orders->pluck('reference')->toArray());
64 | }
65 |
66 | /** @test */
67 | public function it_can_filter_count_fields()
68 | {
69 | $linesCount = 2;
70 | $queryFilters = 'filters=[{"field":"lines_count","operator":"<","value":"' . $linesCount . '"}]';
71 | $request = Request::create("test?{$queryFilters}");
72 |
73 | $orders = Order::filter($request)->get();
74 |
75 | $this->assertCount(3, $orders);
76 |
77 | $this->assertEquals(['LAF_0001', 'LAF_0004', 'LAF_0005'], $orders->pluck('reference')->toArray());
78 | }
79 |
80 | /** @test */
81 | public function it_can_filter_relation_fields()
82 | {
83 | $linePrice = 10;
84 | $queryFilters = 'filters=[{"field":"line_price","operator":"<","value":"' . $linePrice . '"}]';
85 | $request = Request::create("test?{$queryFilters}");
86 |
87 | $orders = Order::filter($request)->get();
88 |
89 | $this->assertCount(3, $orders);
90 |
91 | $this->assertEquals(['LAF_0002', 'LAF_0004', 'LAF_0005'], $orders->pluck('reference')->toArray());
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/tests/Operators/NotContainsTest.php:
--------------------------------------------------------------------------------
1 | get();
20 |
21 | $this->assertCount(4, $orders);
22 |
23 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0003', 'LAF_0004'], $orders->pluck('reference')->toArray());
24 | }
25 |
26 | /** @test */
27 | public function it_can_filter_numeric_fields()
28 | {
29 | $subtotal = 5;
30 | $queryFilters = 'filters=[{"field":"subtotal","operator":"!*","value":"' . $subtotal . '"}]';
31 | $request = Request::create("test?{$queryFilters}");
32 |
33 | $orders = Order::filter($request)->get();
34 |
35 | $this->assertCount(2, $orders);
36 |
37 | $this->assertEquals(['LAF_0002', 'LAF_0005'], $orders->pluck('reference')->toArray());
38 | }
39 |
40 | /** @test */
41 | public function it_can_filter_custom_fields()
42 | {
43 | $storeReference = 'Consulting';
44 | $queryFilters = 'filters=[{"field":"store_reference","operator":"!*","value":"' . $storeReference . '"}]';
45 | $request = Request::create("test?{$queryFilters}");
46 |
47 | $orders = Order::filter($request)->get();
48 |
49 | $this->assertCount(2, $orders);
50 |
51 | $this->assertEquals(['LAF_0003', 'LAF_0005'], $orders->pluck('reference')->toArray());
52 | }
53 |
54 | /** @test */
55 | public function it_can_not_filter_count_fields()
56 | {
57 | $linesCount = 2;
58 | $queryFilters = 'filters=[{"field":"lines_count","operator":"!*","value":"' . $linesCount . '"}]';
59 | $request = Request::create("test?{$queryFilters}");
60 |
61 | $this->expectException(UnsupportedOperatorException::class);
62 |
63 | Order::filter($request)->get();
64 | }
65 |
66 | /** @test */
67 | public function it_can_filter_relation_fields()
68 | {
69 | $productName = 'Nam';
70 | $queryFilters = 'filters=[{"field":"product_name","operator":"!*","value":"' . $productName . '"}]';
71 | $request = Request::create("test?{$queryFilters}");
72 |
73 | $orders = Order::filter($request)->get();
74 |
75 | $this->assertCount(3, $orders);
76 |
77 | $this->assertEquals(['LAF_0002', 'LAF_0003', 'LAF_0004'], $orders->pluck('reference')->toArray());
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/tests/Operators/NotEndsWithTest.php:
--------------------------------------------------------------------------------
1 | get();
20 |
21 | $this->assertCount(4, $orders);
22 |
23 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0003', 'LAF_0004'], $orders->pluck('reference')->toArray());
24 | }
25 |
26 | /** @test */
27 | public function it_can_filter_numeric_fields()
28 | {
29 | $subtotal = env('DB_CONNECTION') == 'sqlite' ? 5 : 50;
30 | $queryFilters = 'filters=[{"field":"subtotal","operator":"!$","value":"' . $subtotal . '"}]';
31 | $request = Request::create("test?{$queryFilters}");
32 |
33 | $orders = Order::filter($request)->get();
34 |
35 | if (env('DB_CONNECTION') == 'sqlite') {
36 | $this->assertCount(3, $orders);
37 |
38 | $this->assertEquals(['LAF_0002', 'LAF_0004', 'LAF_0005'], $orders->pluck('reference')->toArray());
39 | } else {
40 | $this->assertCount(4, $orders);
41 |
42 | $this->assertEquals(['LAF_0002', 'LAF_0003', 'LAF_0004', 'LAF_0005'], $orders->pluck('reference')->toArray());
43 | }
44 | }
45 |
46 | /** @test */
47 | public function it_can_filter_custom_fields()
48 | {
49 | $storeReference = 'Consulting-LAF_0002';
50 | $queryFilters = 'filters=[{"field":"store_reference","operator":"!$","value":"' . $storeReference . '"}]';
51 | $request = Request::create("test?{$queryFilters}");
52 |
53 | $orders = Order::filter($request)->get();
54 |
55 | $this->assertCount(4, $orders);
56 |
57 | $this->assertEquals(['LAF_0001', 'LAF_0003', 'LAF_0004', 'LAF_0005'], $orders->pluck('reference')->toArray());
58 | }
59 |
60 | /** @test */
61 | public function it_can_not_filter_count_fields()
62 | {
63 | $linesCount = 2;
64 | $queryFilters = 'filters=[{"field":"lines_count","operator":"!$","value":"' . $linesCount . '"}]';
65 | $request = Request::create("test?{$queryFilters}");
66 |
67 | $this->expectException(UnsupportedOperatorException::class);
68 |
69 | Order::filter($request)->get();
70 | }
71 |
72 | /** @test */
73 | public function it_can_filter_relation_fields()
74 | {
75 | $productName = 'libero';
76 | $queryFilters = 'filters=[{"field":"product_name","operator":"!$","value":"' . $productName . '"}]';
77 | $request = Request::create("test?{$queryFilters}");
78 |
79 | $orders = Order::filter($request)->get();
80 |
81 | $this->assertCount(5, $orders);
82 |
83 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0003', 'LAF_0004', 'LAF_0005'],
84 | $orders->pluck('reference')->toArray());
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/Operators/NotEqualsTest.php:
--------------------------------------------------------------------------------
1 | get();
19 |
20 | $this->assertCount(4, $orders);
21 |
22 | $this->assertNotContainsEquals('LAF_0005', $orders->pluck('reference')->toArray());
23 | }
24 |
25 | /** @test */
26 | public function it_can_filter_numeric_fields()
27 | {
28 | $subtotal = 25;
29 | $queryFilters = 'filters=[{"field":"subtotal","operator":"!=","value":"' . $subtotal . '"}]';
30 | $request = Request::create("test?{$queryFilters}");
31 |
32 | $orders = Order::filter($request)->get();
33 |
34 | $this->assertCount(4, $orders);
35 |
36 | $this->assertNotContains('LAF_0003', $orders->pluck('reference')->toArray());
37 | }
38 |
39 | /** @test */
40 | public function it_can_filter_date_fields()
41 | {
42 | $orderDate = '2020-10-2';
43 | $queryFilters = 'filters=[{"field":"order_date","operator":"!=","value":"' . $orderDate . '"}]';
44 | $request = Request::create("test?{$queryFilters}");
45 |
46 | $orders = Order::filter($request)->get();
47 |
48 | $this->assertCount(3, $orders);
49 |
50 | $this->assertNotContains(['LAF_0001', 'LAF_0003'], $orders->pluck('reference')->toArray());
51 | }
52 |
53 |
54 | /** @test */
55 | public function it_can_filter_datetime_fields()
56 | {
57 | $shipDate = '2020-10-3 10:30:00';
58 | $queryFilters = 'filters=[{"field":"ship_date","operator":"!=","value":"' . $shipDate . '"}]';
59 | $request = Request::create("test?{$queryFilters}");
60 |
61 | $orders = Order::filter($request)->get();
62 |
63 | $this->assertCount(1, $orders);
64 |
65 | $this->assertNotContains('LAF_0002', $orders->pluck('reference')->toArray());
66 | }
67 |
68 | /** @test */
69 | public function it_can_filter_custom_fields()
70 | {
71 | $lineSubtotal = 8.6;
72 | $queryFilters = 'filters=[{"field":"line_subtotal","operator":"!=","value":"' . $lineSubtotal . '"}]';
73 | $request = Request::create("test?{$queryFilters}");
74 |
75 | $orders = Order::filter($request)->get();
76 |
77 | $this->assertCount(4, $orders);
78 |
79 | $this->assertNotContains('LAF_0005', $orders->pluck('reference')->toArray());
80 | }
81 |
82 | /** @test */
83 | public function it_can_filter_count_fields()
84 | {
85 | $linesCount = 2;
86 | $queryFilters = 'filters=[{"field":"lines_count","operator":"!=","value":"' . $linesCount . '"}]';
87 | $request = Request::create("test?{$queryFilters}");
88 |
89 | $orders = Order::filter($request)->get();
90 |
91 | $this->assertCount(3, $orders);
92 |
93 | $this->assertNotContains(['LAF_0002', 'LAF_0003'], $orders->pluck('reference')->toArray());
94 | }
95 |
96 | /** @test */
97 | public function it_can_filter_relation_fields()
98 | {
99 | $storeName = 'Sociis Corporation';
100 | $queryFilters = 'filters=[{"field":"store_name","operator":"!=","value":"' . $storeName . '"}]';
101 | $request = Request::create("test?{$queryFilters}");
102 |
103 | $orders = Order::filter($request)->get();
104 |
105 | $this->assertCount(4, $orders);
106 |
107 | $this->assertNotContains('LAF_0005', $orders->pluck('reference')->toArray());
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/tests/Operators/NotInTest.php:
--------------------------------------------------------------------------------
1 | get();
21 |
22 | $this->assertCount(3, $orders);
23 |
24 | $this->assertEquals(['LAF_0002', 'LAF_0003', 'LAF_0004'], $orders->pluck('reference')->toArray());
25 | }
26 |
27 | /** @test */
28 | public function it_can_filter_numeric_fields()
29 | {
30 | $subtotal = "[25,5.7]";
31 | $queryFilters = 'filters=[{"field":"subtotal","operator":"!|","value":' . $subtotal . '}]';
32 | $request = Request::create("test?{$queryFilters}");
33 |
34 | $orders = Order::filter($request)->get();
35 |
36 | $this->assertCount(3, $orders);
37 |
38 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0005'], $orders->pluck('reference')->toArray());
39 | }
40 |
41 | /** @test */
42 | public function it_can_filter_date_fields()
43 | {
44 | $orderDate = '["2020-10-02","2020-09-25"]';
45 | $queryFilters = 'filters=[{"field":"order_date","operator":"!|","value":' . $orderDate . '}]';
46 | $request = Request::create("test?{$queryFilters}");
47 |
48 | $orders = Order::filter($request)->get();
49 |
50 | $this->assertCount(2, $orders);
51 |
52 | $this->assertEquals(['LAF_0002', 'LAF_0005'], $orders->pluck('reference')->toArray());
53 | }
54 |
55 |
56 | /** @test */
57 | public function it_can_filter_datetime_fields()
58 | {
59 | $shipDate = '["2020-10-03 10:30:00", "2020-09-30 05:25:04"]';
60 | $queryFilters = 'filters=[{"field":"ship_date","operator":"!|","value":' . $shipDate . '}]';
61 | $request = Request::create("test?{$queryFilters}");
62 |
63 | $orders = Order::filter($request)->get();
64 | // 0 because of NULL-safe
65 | /** @link https://dev.mysql.com/doc/refman/8.0/en/comparison-operators.html#operator_equal-to */
66 | $this->assertCount(0, $orders);
67 | }
68 |
69 | /** @test */
70 | public function it_can_filter_custom_fields()
71 | {
72 | $lineSubtotal = "[8.6,5.7]";
73 | $queryFilters = 'filters=[{"field":"line_subtotal","operator":"!|","value":' . $lineSubtotal . '}]';
74 | $request = Request::create("test?{$queryFilters}");
75 |
76 | $orders = Order::filter($request)->get();
77 |
78 | $this->assertCount(3, $orders);
79 |
80 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0003'], $orders->pluck('reference')->toArray());
81 | }
82 |
83 | /** @test */
84 | public function it_can_filter_count_fields()
85 | {
86 | $linesCount = "[1,2]";
87 | $queryFilters = 'filters=[{"field":"lines_count","operator":"!|","value":' . $linesCount . '}]';
88 | $request = Request::create("test?{$queryFilters}");
89 |
90 | $this->expectException(UnsupportedOperatorException::class);
91 |
92 | Order::filter($request)->get();
93 | }
94 |
95 | /** @test */
96 | public function it_can_filter_relation_fields()
97 | {
98 | $productSku = '["2971-KW","KU5-8ZD"]';
99 | $queryFilters = 'filters=[{"field":"product_sku","operator":"!|","value":' . $productSku . '}]';
100 | $request = Request::create("test?{$queryFilters}");
101 |
102 | $orders = Order::filter($request)->get();
103 |
104 | $this->assertCount(3, $orders);
105 |
106 | $this->assertEquals(['LAF_0002', 'LAF_0003', 'LAF_0005'], $orders->pluck('reference')->toArray());
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/tests/Operators/NotStartsWithTest.php:
--------------------------------------------------------------------------------
1 | get();
20 |
21 | $this->assertEmpty($orders);
22 | }
23 |
24 | /** @test */
25 | public function it_can_filter_numeric_fields()
26 | {
27 | $subtotal = 5;
28 | $queryFilters = 'filters=[{"field":"subtotal","operator":"!^","value":"' . $subtotal . '"}]';
29 | $request = Request::create("test?{$queryFilters}");
30 |
31 | $orders = Order::filter($request)->get();
32 |
33 | $this->assertCount(4, $orders);
34 |
35 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0003', 'LAF_0005'], $orders->pluck('reference')->toArray());
36 | }
37 |
38 | /** @test */
39 | public function it_can_filter_custom_fields()
40 | {
41 | $storeReference = 'Sociis Corporation-LAF';
42 | $queryFilters = 'filters=[{"field":"store_reference","operator":"!^","value":"' . $storeReference . '"}]';
43 | $request = Request::create("test?{$queryFilters}");
44 |
45 | $orders = Order::filter($request)->get();
46 |
47 | $this->assertCount(4, $orders);
48 |
49 | $this->assertEquals(['LAF_0001', 'LAF_0002', 'LAF_0003', 'LAF_0004'], $orders->pluck('reference')->toArray());
50 | }
51 |
52 | /** @test */
53 | public function it_can_not_filter_count_fields()
54 | {
55 | $linesCount = 2;
56 | $queryFilters = 'filters=[{"field":"lines_count","operator":"!^","value":"' . $linesCount . '"}]';
57 | $request = Request::create("test?{$queryFilters}");
58 |
59 | $this->expectException(UnsupportedOperatorException::class);
60 |
61 | Order::filter($request)->get();
62 | }
63 |
64 | /** @test */
65 | public function it_can_filter_relation_fields()
66 | {
67 | $productName = 'Nam';
68 | $queryFilters = 'filters=[{"field":"product_name","operator":"!^","value":"' . $productName . '"}]';
69 | $request = Request::create("test?{$queryFilters}");
70 |
71 | $orders = Order::filter($request)->get();
72 |
73 | $this->assertCount(4, $orders);
74 |
75 | $this->assertEquals(['LAF_0002', 'LAF_0003', 'LAF_0004', 'LAF_0005'], $orders->pluck('reference')->toArray());
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/tests/Operators/StartsWithTest.php:
--------------------------------------------------------------------------------
1 | get();
20 |
21 | $this->assertCount(Order::count(), $orders);
22 | }
23 |
24 | /** @test */
25 | public function it_can_filter_numeric_fields()
26 | {
27 | $subtotal = 5;
28 | $queryFilters = 'filters=[{"field":"subtotal","operator":"^","value":"' . $subtotal . '"}]';
29 | $request = Request::create("test?{$queryFilters}");
30 |
31 | $orders = Order::filter($request)->get();
32 |
33 | $this->assertCount(1, $orders);
34 |
35 | $this->assertEquals(['LAF_0004'], $orders->pluck('reference')->toArray());
36 | }
37 |
38 | /** @test */
39 | public function it_can_filter_custom_fields()
40 | {
41 | $storeReference = 'Sociis Corporation-LAF';
42 | $queryFilters = 'filters=[{"field":"store_reference","operator":"^","value":"' . $storeReference . '"}]';
43 | $request = Request::create("test?{$queryFilters}");
44 |
45 | $orders = Order::filter($request)->get();
46 |
47 | $this->assertCount(1, $orders);
48 |
49 | $this->assertEquals(['LAF_0005'], $orders->pluck('reference')->toArray());
50 | }
51 |
52 | /** @test */
53 | public function it_can_not_filter_count_fields()
54 | {
55 | $linesCount = 2;
56 | $queryFilters = 'filters=[{"field":"lines_count","operator":"^","value":"' . $linesCount . '"}]';
57 | $request = Request::create("test?{$queryFilters}");
58 |
59 | $this->expectException(UnsupportedOperatorException::class);
60 |
61 | Order::filter($request)->get();
62 | }
63 |
64 | /** @test */
65 | public function it_can_filter_relation_fields()
66 | {
67 | $productName = 'Nam';
68 | $queryFilters = 'filters=[{"field":"product_name","operator":"^","value":"' . $productName . '"}]';
69 | $request = Request::create("test?{$queryFilters}");
70 |
71 | $orders = Order::filter($request)->get();
72 |
73 | $this->assertCount(2, $orders);
74 |
75 | $this->assertEquals(['LAF_0001', 'LAF_0003'], $orders->pluck('reference')->toArray());
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/tests/QueryFormatsTest.php:
--------------------------------------------------------------------------------
1 | 'email',
12 | 'operator' => 'equal',
13 | 'value' => 'abc',
14 | ];
15 |
16 | private $arrayValueFilter = [
17 | 'field' => 'email',
18 | 'operator' => 'in',
19 | 'value' => ['abc1', 'abc2'],
20 | ];
21 |
22 |
23 | private $betweenFilter = [
24 | 'field' => 'email',
25 | 'operator' => 'between',
26 | 'value' => ['from' => 10, 'to' => 20],
27 | ];
28 |
29 | /** @test */
30 | public function array_query_format()
31 | {
32 | $this->app['config']->set('advanced_filter.query_format', 'array');
33 |
34 | $queryFilters = 'filters[email][value]=abc&filters[email][operator]=equal';
35 | $request = Request::create("test?{$queryFilters}");
36 |
37 | $filters = QueryFormat::factory($request)->getFilters();
38 |
39 | $this->assertEquals([$this->normalFilter], $filters);
40 | }
41 |
42 | /** @test */
43 | public function json_query_format()
44 | {
45 | $this->app['config']->set('advanced_filter.query_format', 'json');
46 |
47 | $queryFilters = 'filters=[{"field":"email","operator":"equal","value":"abc"}]';
48 | $request = Request::create("test?{$queryFilters}");
49 |
50 | $filters = QueryFormat::factory($request)->getFilters();
51 |
52 | $this->assertEquals([$this->normalFilter], $filters);
53 | }
54 |
55 | /** @test */
56 | public function separator_query_format()
57 | {
58 | $this->app['config']->set('advanced_filter.query_format', 'separator:^');
59 |
60 | $queryFilters = 'filters^email^value=abc&filters^email^operator=equal';
61 | $request = Request::create("test?{$queryFilters}");
62 |
63 | $filters = QueryFormat::factory($request)->getFilters();
64 |
65 | $this->assertEquals([$this->normalFilter], $filters);
66 | }
67 |
68 | /** @test */
69 | public function array_query_format_for_array_value()
70 | {
71 | $this->app['config']->set('advanced_filter.query_format', 'array');
72 |
73 | $queryFilters = 'filters[email][value][0]=abc1&filters[email][value][1]=abc2&filters[email][operator]=in';
74 | $request = Request::create("test?{$queryFilters}");
75 |
76 | $filters = QueryFormat::factory($request)->getFilters();
77 |
78 | $this->assertEquals([$this->arrayValueFilter], $filters);
79 | }
80 |
81 | /** @test */
82 | public function json_query_format_for_array_value()
83 | {
84 | $this->app['config']->set('advanced_filter.query_format', 'json');
85 |
86 | $queryFilters = 'filters=[{"field":"email","operator":"in","value":["abc1","abc2"]}]';
87 | $request = Request::create("test?{$queryFilters}");
88 |
89 | $filters = QueryFormat::factory($request)->getFilters();
90 |
91 | $this->assertEquals([$this->arrayValueFilter], $filters);
92 | }
93 |
94 | /** @test */
95 | public function separator_query_format_for_array_value()
96 | {
97 | $this->app['config']->set('advanced_filter.query_format', 'separator:^');
98 |
99 | $queryFilters = 'filters^email^value^0=abc1&filters^email^value^1=abc2&filters^email^operator=in';
100 | $request = Request::create("test?{$queryFilters}");
101 |
102 | $filters = QueryFormat::factory($request)->getFilters();
103 |
104 | $this->assertEquals([$this->arrayValueFilter], $filters);
105 | }
106 |
107 | /** @test */
108 | public function array_query_format_for_between()
109 | {
110 | $this->app['config']->set('advanced_filter.query_format', 'array');
111 |
112 | $queryFilters = 'filters[email][value][from]=10&filters[email][value][to]=20&filters[email][operator]=between';
113 | $request = Request::create("test?{$queryFilters}");
114 |
115 | $filters = QueryFormat::factory($request)->getFilters();
116 |
117 | $this->assertEquals([$this->betweenFilter], $filters);
118 | }
119 |
120 | /** @test */
121 | public function json_query_format_for_between()
122 | {
123 | $this->app['config']->set('advanced_filter.query_format', 'json');
124 |
125 | $queryFilters = 'filters=[{"field":"email","operator":"between","value":{"from":10,"to":20}}]';
126 | $request = Request::create("test?{$queryFilters}");
127 |
128 | $filters = QueryFormat::factory($request)->getFilters();
129 |
130 | $this->assertEquals([$this->betweenFilter], $filters);
131 | }
132 |
133 | /** @test */
134 | public function separator_query_format_for_between()
135 | {
136 | $this->app['config']->set('advanced_filter.query_format', 'separator:^');
137 |
138 | $queryFilters = 'filters^email^value^from=10&filters^email^value^to=20&filters^email^operator=between';
139 | $request = Request::create("test?{$queryFilters}");
140 |
141 | $filters = QueryFormat::factory($request)->getFilters();
142 |
143 | $this->assertEquals([$this->betweenFilter], $filters);
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/tests/Seeds/DatabaseSeeder.php:
--------------------------------------------------------------------------------
1 | call(ProductSeeder::class);
17 | $this->call(StoreSeeder::class);
18 | $this->call(OrderSeeder::class);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/Seeds/OrderSeeder.php:
--------------------------------------------------------------------------------
1 | 1,
16 | 'reference' => 'LAF_0001',
17 | 'order_date' => '2020-10-2',
18 | 'subtotal' => 15.50,
19 | 'shipping_cost' => 1
20 | ])->orderLines()->create(['product_id' => 1, 'price' => 15, 'quantity' => 1]);
21 |
22 |
23 | Order::create([
24 | 'store_id' => 1,
25 | 'reference' => 'LAF_0002',
26 | 'order_date' => '2020-10-1',
27 | 'ship_date' => '2020-10-03 10:30:00',
28 | 'subtotal' => 20.00,
29 | 'shipping_cost' => 0
30 | ])->orderLines()->createMany([
31 | ['product_id' => 2, 'price' => 5, 'quantity' => 2],
32 | ['product_id' => 3, 'price' => 10, 'quantity' => 1]
33 | ]);
34 |
35 |
36 | Order::create([
37 | 'store_id' => 2,
38 | 'reference' => 'LAF_0003',
39 | 'order_date' => '2020-10-2',
40 | 'subtotal' => 25.00,
41 | 'shipping_cost' => 1
42 | ])->orderLines()->createMany([
43 | ['product_id' => 1, 'price' => 15, 'quantity' => 1],
44 | ['product_id' => 3, 'price' => 10, 'quantity' => 1]
45 | ]);
46 |
47 |
48 | Order::create([
49 | 'store_id' => 3,
50 | 'reference' => 'LAF_0004',
51 | 'order_date' => '2020-09-25',
52 | 'ship_date' => '2020-09-30 05:25:04',
53 | 'subtotal' => 5.70,
54 | 'shipping_cost' => 1.5
55 | ])->orderLines()->create(['product_id' => 5, 'price' => 5.7, 'quantity' => 1]);
56 |
57 |
58 | Order::create([
59 | 'store_id' => 5,
60 | 'reference' => 'LAF_0005',
61 | 'order_date' => '2020-09-26',
62 | 'subtotal' => 8.60,
63 | 'shipping_cost' => 3
64 | ])->orderLines()->create(['product_id' => 6, 'price' => 4.3, 'quantity' => 2]);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/Seeds/ProductSeeder.php:
--------------------------------------------------------------------------------
1 | 'Nam porttitor scelerisque neque. Nullam', 'sku' => '2971-KW']);
15 | Product::create(['name' => 'tellus id nunc interdum feugiat.', 'sku' => 'M0E-8H3']);
16 | Product::create(['name' => 'magna tellus faucibus leo, libero', 'sku' => '64759-40420']);
17 | Product::create(['name' => 'eu neque pellentesque massa lobortis', 'sku' => 'L5X-7J3']);
18 | Product::create(['name' => 'arcu vel quam dignissim pharetra.', 'sku' => 'KU5-8ZD']);
19 | Product::create(['name' => 'magna. Nam ligula elit, pretium', 'sku' => 'LT5W-9CV']);
20 | Product::create(['name' => 'aliquam eros turpis non enim.', 'sku' => '943262']);
21 | Product::create(['name' => 'pharetra nibh. Aliquam ornare, libero', 'sku' => '50336']);
22 | Product::create(['name' => 'purus, accumsan interdum libero dui', 'sku' => '7889-LY']);
23 | Product::create(['name' => 'sit amet ultricies sem magna', 'sku' => 'K6V-4G6']);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/Seeds/StoreSeeder.php:
--------------------------------------------------------------------------------
1 | 'Placerat Consulting']);
16 | Store::create(['name' => 'Hendrerit A Arcu Ltd']);
17 | Store::create(['name' => 'Proin Eget Odio Consulting']);
18 | Store::create(['name' => 'Orci In Consequat Associates']);
19 | Store::create(['name' => 'Sociis Corporation']);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | artisan('migrate:fresh');
20 |
21 | $this->setupDatabase($this->app['db']->connection()->getSchemaBuilder());
22 |
23 | $this->seed(DatabaseSeeder::class);
24 |
25 | RefreshDatabaseState::$migrated = true;
26 | }
27 | }
28 |
29 | protected function getPackageProviders($app)
30 | {
31 | return [
32 | AdvancedFilterServiceProvider::class,
33 | // MongodbServiceProvider::class
34 | ];
35 | }
36 |
37 | protected function getEnvironmentSetUp($app)
38 | {
39 | // setup mongodb config
40 | if (env('DB_CONNECTION') == 'mongodb') {
41 | Config::set('database.connections.mongodb', [
42 | 'driver' => 'mongodb',
43 | 'host' => env('DB_HOST', '127.0.0.1'),
44 | 'port' => env('DB_PORT', 27017),
45 | 'database' => env('DB_DATABASE', 'test'),
46 | 'username' => env('DB_USERNAME'),
47 | 'password' => env('DB_PASSWORD'),
48 | 'options' => [
49 | 'database' => 'admin' // sets the authentication database required by mongo 3
50 | ]
51 | ]);
52 | }
53 | }
54 |
55 | private function setupDatabase(Builder $schema)
56 | {
57 | $schema->create('stores', function (Blueprint $table) {
58 | $table->increments('id');
59 | $table->string('name');
60 | $table->timestamps();
61 | });
62 |
63 | $schema->create('products', function (Blueprint $table) {
64 | $table->increments('id');
65 | $table->string('name');
66 | $table->string('sku');
67 | $table->timestamps();
68 | });
69 |
70 | $schema->create('orders', function (Blueprint $table) {
71 | $table->increments('id');
72 | $table->unsignedInteger('store_id');
73 | $table->string('reference');
74 | $table->dateTime('order_date');
75 | $table->dateTime('ship_date')->nullable();
76 | $table->decimal('subtotal');
77 | $table->decimal('shipping_cost');
78 | $table->timestamps();
79 | });
80 |
81 | $schema->create('order_lines', function (Blueprint $table) {
82 | $table->increments('id');
83 | $table->unsignedInteger('order_id');
84 | $table->unsignedInteger('product_id');
85 | $table->float('price');
86 | $table->integer('quantity');
87 | $table->timestamps();
88 | });
89 | }
90 | }
91 |
--------------------------------------------------------------------------------