├── .github
└── issue_template.md
├── .styleci.yml
├── LICENSE
├── README.md
├── codesize.xml
├── composer.json
├── config
└── select.php
├── src
├── AppServiceProvider.php
├── Exceptions
│ └── Query.php
├── Services
│ └── Options.php
└── Traits
│ ├── OptionsBuilder.php
│ └── TypeaheadBuilder.php
└── tests
└── features
└── SelectTest.php
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 |
2 | This is a **bug | feature request**.
3 |
4 |
5 | ### Prerequisites
6 | * [ ] Are you running the latest version?
7 | * [ ] Are you reporting to the correct repository?
8 | * [ ] Did you check the documentation?
9 | * [ ] Did you perform a cursory search?
10 |
11 | ### Description
12 |
13 |
14 | ### Steps to Reproduce
15 |
20 |
21 | ### Expected behavior
22 |
23 |
24 | ### Actual behavior
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | risky: true
2 |
3 | preset: laravel
4 |
5 | enabled:
6 | - strict
7 | - unalign_double_arrow
8 |
9 | disabled:
10 | - short_array_syntax
11 |
12 | finder:
13 | exclude:
14 | - "public"
15 | - "resources"
16 | - "tests"
17 | name:
18 | - "*.php"
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 laravel-enso
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Select
2 |
3 | [](https://www.codacy.com/gh/laravel-enso/select?utm_source=github.com&utm_medium=referral&utm_content=laravel-enso/select&utm_campaign=Badge_Grade)
4 | [](https://github.styleci.io/repos/85489940)
5 | [](https://packagist.org/packages/laravel-enso/select)
6 | [](https://packagist.org/packages/laravel-enso/select)
7 | [](https://packagist.org/packages/laravel-enso/select)
8 |
9 | Single and multi-select server-side option list builder
10 |
11 | This package can work independently of the [Enso](https://github.com/laravel-enso/Enso) ecosystem.
12 |
13 | The front end assets that utilize this api are present in the [select](https://github.com/enso-ui/select) package.
14 |
15 | For live examples and demos, you may visit [laravel-enso.com](https://www.laravel-enso.com)
16 |
17 | [](https://laravel-enso.github.io/select/videos/bulma_demo_01.mp4)
18 |
19 | click on the photo to view a short demo in compatible browsers
20 |
21 | ### Installation, Configuration & Usage
22 |
23 | Be sure to check out the full documentation for this package available at [docs.laravel-enso.com](https://docs.laravel-enso.com/backend/select.html)
24 |
25 | ## Contributions
26 |
27 | are welcome. Pull requests are great, but issues are good too.
28 |
29 | ## License
30 |
31 | This package is released under the MIT license.
32 |
--------------------------------------------------------------------------------
/codesize.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 | custom rules
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "laravel-enso/select",
3 | "description": "Bootstrap Select data builder with server-side data fetching capability and a VueJS component",
4 | "keywords": [
5 | "laravel-enso",
6 | "select",
7 | "bootstrap-select",
8 | "select-server-side"
9 | ],
10 | "homepage": "https://github.com/laravel-enso/select",
11 | "type": "library",
12 | "license": "MIT",
13 | "authors": [
14 | {
15 | "name": "Adrian Ocneanu",
16 | "email": "aocneanu@gmail.com",
17 | "homepage": "https://laravel-enso.com",
18 | "role": "Developer"
19 | }
20 | ],
21 | "require": {
22 | "php": "^8.0",
23 | "laravel/framework": "^10.0|^11.0",
24 | "laravel-enso/filters": "^2.0",
25 | "laravel-enso/helpers": "^3.0"
26 | },
27 | "autoload": {
28 | "psr-4": {
29 | "LaravelEnso\\Select\\": "src/"
30 | }
31 | },
32 | "extra": {
33 | "laravel": {
34 | "providers": [
35 | "LaravelEnso\\Select\\AppServiceProvider"
36 | ],
37 | "aliases": []
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/config/select.php:
--------------------------------------------------------------------------------
1 | 'id',
17 |
18 | /*
19 | |--------------------------------------------------------------------------
20 | | Query attributes
21 | |--------------------------------------------------------------------------
22 | | The default query attributes used for every select. You can override
23 | | it by adding a protected $queryAttributes property in the local
24 | | Options controller.
25 | */
26 |
27 | 'queryAttributes' => ['name'],
28 |
29 | /*
30 | |--------------------------------------------------------------------------
31 | | SQL comparison operator
32 | |--------------------------------------------------------------------------
33 | | The comparison operator will be the default used for every select.
34 | | Possible values for comparison operator: LIKE, ILIKE
35 | */
36 |
37 | 'comparisonOperator' => ComparisonOperators::Like,
38 |
39 | /*
40 | |--------------------------------------------------------------------------
41 | | Search Mode
42 | |--------------------------------------------------------------------------
43 | | Controls the global way in which wildcards are used in the query.
44 | | Can be customized for each select. Possible values for search mode:
45 | | SearchModes::Full, SearchModes::StartsWith, SearchModes::EndsWith
46 | */
47 |
48 | 'searchMode' => SearchModes::Full,
49 |
50 | /*
51 | |--------------------------------------------------------------------------
52 | | Sort By Options
53 | |--------------------------------------------------------------------------
54 | | The sort by options used for every select.
55 | | Possible options are SORT_REGULAR, SORT_NUMERIC, SORT_STRING,
56 | | SORT_NATURAL, SORT_FLAG_CASE, ...
57 | | Ex : Case-insensitive sorting : SORT_NATURAL|SORT_FLAG_CASE
58 | | @link https://php.net/manual/en/array.constants.php
59 | */
60 |
61 | 'sortByOptions' => SORT_REGULAR,
62 | ];
63 |
--------------------------------------------------------------------------------
/src/AppServiceProvider.php:
--------------------------------------------------------------------------------
1 | load()
12 | ->publish();
13 | }
14 |
15 | private function load()
16 | {
17 | $this->mergeConfigFrom(__DIR__.'/../config/select.php', 'enso.select');
18 |
19 | return $this;
20 | }
21 |
22 | private function publish()
23 | {
24 | $this->publishes([
25 | __DIR__.'/../config' => config_path('enso'),
26 | ], ['select-config', 'enso-config']);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Exceptions/Query.php:
--------------------------------------------------------------------------------
1 | trackBy = Config::get('enso.select.trackBy');
34 | $this->queryAttributes = new Collection(Config::get('enso.select.queryAttributes'));
35 | $this->searchMode = Config::get('enso.select.searchMode');
36 | $this->resource = null;
37 | $this->appends = null;
38 | }
39 |
40 | public function toResponse($request)
41 | {
42 | $this->request = $request;
43 |
44 | return $this->resource
45 | ? App::make($this->resource, ['resource' => null])::collection($this->data())
46 | : $this->data();
47 | }
48 |
49 | public function trackBy(string $trackBy): self
50 | {
51 | $this->trackBy = $trackBy;
52 |
53 | return $this;
54 | }
55 |
56 | public function queryAttributes(array $queryAttributes): self
57 | {
58 | $this->queryAttributes = new Collection($queryAttributes);
59 |
60 | return $this;
61 | }
62 |
63 | public function searchMode(string $searchMode): self
64 | {
65 | $this->searchMode = $searchMode;
66 |
67 | return $this;
68 | }
69 |
70 | public function resource(?string $resource): self
71 | {
72 | $this->resource = $resource;
73 |
74 | return $this;
75 | }
76 |
77 | public function appends(?array $appends): self
78 | {
79 | $this->appends = $appends;
80 |
81 | return $this;
82 | }
83 |
84 | private function data(): Collection
85 | {
86 | return $this->init()
87 | ->applyParams()
88 | ->applyPivotParams()
89 | ->selected()
90 | ->search()
91 | ->order()
92 | ->limit()
93 | ->get();
94 | }
95 |
96 | private function init(): self
97 | {
98 | $this->value = $this->request->has('value')
99 | ? (array) $this->request->get('value')
100 | : [];
101 |
102 | $attribute = $this->queryAttributes->first();
103 | $this->orderBy = $this->isNested($attribute) ? null : $attribute;
104 |
105 | return $this;
106 | }
107 |
108 | private function applyParams(): self
109 | {
110 | $this->params()->each(fn ($value, $column) => $this->query->when(
111 | $value === null,
112 | fn ($query) => $query->whereNull($column),
113 | fn ($query) => $query->whereIn($column, (array) $value)
114 | ));
115 |
116 | return $this;
117 | }
118 |
119 | private function applyPivotParams(): self
120 | {
121 | $this->pivotParams()->each(fn ($value, $relation) => $this->query
122 | ->whereHas(Str::beforeLast($relation, '.'), fn ($query) => $query->when(
123 | $value === null,
124 | fn ($query) => $query->whereNull(Str::afterLast($relation, '.')),
125 | fn ($query) => $query->whereIn(Str::afterLast($relation, '.'), (array) $value)
126 | )));
127 |
128 | return $this;
129 | }
130 |
131 | private function selected(): self
132 | {
133 | $this->selected = (clone $this->query)
134 | ->whereIn($this->trackBy, $this->value)
135 | ->get();
136 |
137 | return $this;
138 | }
139 |
140 | private function search(): self
141 | {
142 | $search = $this->request->get('query');
143 |
144 | if (! $search) {
145 | return $this;
146 | }
147 |
148 | (new Search($this->query, $this->attributes(), $search))
149 | ->relations($this->relations())
150 | ->searchMode($this->searchMode)
151 | ->comparisonOperator(Config::get('enso.select.comparisonOperator'))
152 | ->handle();
153 |
154 | return $this;
155 | }
156 |
157 | private function attributes(): array
158 | {
159 | return $this->queryAttributes
160 | ->reject(fn ($attribute) => $this->isNested($attribute))
161 | ->toArray();
162 | }
163 |
164 | private function relations(): array
165 | {
166 | return $this->queryAttributes
167 | ->filter(fn ($attribute) => $this->isNested($attribute))
168 | ->toArray();
169 | }
170 |
171 | private function order(): self
172 | {
173 | $this->query->when($this->orderBy, fn ($query) => $query->orderBy($this->orderBy));
174 |
175 | return $this;
176 | }
177 |
178 | private function limit(): self
179 | {
180 | $limit = $this->request->get('paginate') ?? self::Limit;
181 |
182 | $this->query->limit($limit);
183 |
184 | return $this;
185 | }
186 |
187 | private function get(): Collection
188 | {
189 | return $this->query->whereNotIn($this->trackBy, $this->value)->get()
190 | ->toBase()
191 | ->merge($this->selected)
192 | ->when($this->orderBy !== null, fn ($results) => $results
193 | ->sortBy($this->orderBy, Config::get('enso.select.sortByOptions')))
194 | ->values()
195 | ->when($this->appends, fn ($results) => $results->each->setAppends($this->appends));
196 | }
197 |
198 | private function params(): Collection
199 | {
200 | return new Collection(json_decode($this->request->get('params'), true));
201 | }
202 |
203 | private function pivotParams(): Collection
204 | {
205 | $params = json_decode($this->request->get('pivotParams'), true);
206 |
207 | return Collection::wrap($params)->dot();
208 | }
209 |
210 | private function isNested($attribute): bool
211 | {
212 | return Str::contains($attribute, '.');
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/src/Traits/OptionsBuilder.php:
--------------------------------------------------------------------------------
1 | response($request);
14 | }
15 |
16 | private function response(Request $request)
17 | {
18 | $query = method_exists($this, 'query') ? $this->query($request) : App::make($this->model)::query();
19 |
20 | return App::make(Options::class, ['query' => $query])
21 | ->when($request->has('trackBy'), fn ($options) => $options->trackBy($request->get('trackBy')))
22 | ->when($request->has('searchMode'), fn ($options) => $options->searchMode($request->get('searchMode')))
23 | ->when(isset($this->queryAttributes), fn ($options) => $options->queryAttributes($this->queryAttributes))
24 | ->when(isset($this->resource), fn ($options) => $options->resource($this->resource))
25 | ->when(isset($this->appends), fn ($options) => $options->appends($this->appends));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Traits/TypeaheadBuilder.php:
--------------------------------------------------------------------------------
1 | convert($request);
14 |
15 | return $this->response($request);
16 | }
17 |
18 | private function convert(Request $request)
19 | {
20 | $params = json_decode($request->get('params'));
21 |
22 | $request->replace([
23 | 'query' => $request->get('query'),
24 | 'paginate' => $request->get('paginate'),
25 | 'params' => json_encode($params?->params ?? null),
26 | 'searchMode' => $request->get('searchMode'),
27 | 'pivotParams' => json_encode($params?->pivot ?? null),
28 | 'customParams' => json_encode($params?->custom ?? null),
29 | ]);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/features/SelectTest.php:
--------------------------------------------------------------------------------
1 | faker = Factory::create();
27 |
28 | $this->createTestModelTable();
29 | $this->createRelationTable();
30 |
31 | $this->testModel = $this->createTestModel();
32 |
33 | $this->createRelation();
34 | }
35 |
36 | /** @test */
37 | public function can_get_options_without_filters()
38 | {
39 | $response = $this->requestResponse();
40 |
41 | $this->assertCount(SelectTestModel::count(), $response);
42 | $this->assertTrue($this->whithinResponse($response));
43 | }
44 |
45 | /** @test */
46 | public function can_get_empty_options_with_restricting_query()
47 | {
48 | $response = $this->requestResponse(['query' => 'NO_VALUE']);
49 |
50 | $this->assertCount(0, $response);
51 | }
52 |
53 | /** @test */
54 | public function can_get_empty_options_with_restricting_params()
55 | {
56 | $response = $this->requestResponse(['params' => ['id' => 0]]);
57 |
58 | $this->assertCount(0, $response);
59 | }
60 |
61 | /** @test */
62 | public function can_get_empty_options_with_restricting_pivot_params()
63 | {
64 | $response = $this->requestResponse([
65 | 'pivotParams' => ['relation' => ['id' => 0]],
66 | ]);
67 |
68 | $this->assertCount(0, $response);
69 | }
70 |
71 | /** @test */
72 | public function can_get_selected_options_with_restricting_filter()
73 | {
74 | $response = $this->requestResponse([
75 | 'value' => $this->testModel->id,
76 | 'query' => 'NO_VALUE',
77 | ]);
78 |
79 | $this->assertCount(1, $response);
80 |
81 | $this->assertTrue($this->whithinResponse($response));
82 | }
83 |
84 | /** @test */
85 | public function can_get_filtered_options()
86 | {
87 | $response = $this->requestResponse([
88 | 'query' => $this->testModel->email,
89 | ]);
90 |
91 | $this->assertTrue($this->whithinResponse($response));
92 | }
93 |
94 | /** @test */
95 | public function can_get_filtered_on_nested_attrs_options()
96 | {
97 | $response = $this->requestResponse([
98 | 'query' => $this->testModel->relation->name,
99 | ]);
100 |
101 | $this->assertTrue($this->whithinResponse($response));
102 | }
103 |
104 | /** @test */
105 | public function can_get_options_with_param()
106 | {
107 | $response = $this->requestResponse([
108 | 'params' => ['email' => $this->testModel->email],
109 | ]);
110 |
111 | $this->assertTrue($this->whithinResponse($response));
112 | }
113 |
114 | /** @test */
115 | public function can_get_options_with_pivot_params()
116 | {
117 | $response = $this->requestResponse([
118 | 'pivotParams' => ['relation' => ['name' => $this->testModel->relation->name]],
119 | ]);
120 |
121 | $this->assertTrue($this->whithinResponse($response));
122 | }
123 |
124 | /** @test */
125 | public function can_paginate()
126 | {
127 | $paginate = 0;
128 | $response = $this->requestResponse(['paginate' => $paginate]);
129 |
130 | $this->assertCount($paginate, $response);
131 | }
132 |
133 | /** @test */
134 | public function can_use_resource()
135 | {
136 | $this->resource = SelectTestResource::class;
137 | $response = $this->requestResponse();
138 |
139 | $this->assertCount(SelectTestModel::count(), $response);
140 |
141 | $this->assertTrue(
142 | $response->first()->resolve()['resource'] === 'resource'
143 | );
144 | }
145 |
146 | /** @test */
147 | public function can_use_accessors()
148 | {
149 | $this->appends = ['custom'];
150 | $response = $this->requestResponse();
151 |
152 | $this->assertCount(SelectTestModel::count(), $response);
153 | $this->assertTrue(
154 | $response->first()->toArray()['custom'] === $this->testModel->custom
155 | );
156 | }
157 |
158 | private function whithinResponse($response)
159 | {
160 | return $response->pluck('email')
161 | ->contains($this->testModel->email);
162 | }
163 |
164 | private function requestResponse(array $params = [])
165 | {
166 | $request = new Request();
167 |
168 | Collection::wrap($params)->each(fn ($value, $key) => $request->merge([
169 | $key => is_array($value) ? json_encode($value) : $value,
170 | ]));
171 |
172 | return new Collection(
173 | $this->__invoke($request)->toResponse($request)
174 | );
175 | }
176 |
177 | public function query()
178 | {
179 | return SelectTestModel::query();
180 | }
181 |
182 | private function createTestModel()
183 | {
184 | return SelectTestModel::create([
185 | 'email' => $this->faker->email,
186 | ]);
187 | }
188 |
189 | private function createRelation()
190 | {
191 | return SelectRelation::create([
192 | 'name' => $this->faker->name,
193 | 'parent_id' => $this->testModel->id,
194 | ]);
195 | }
196 |
197 | private function createTestModelTable()
198 | {
199 | Schema::create('select_test_models', function ($table) {
200 | $table->increments('id');
201 | $table->string('email');
202 | $table->timestamps();
203 | });
204 | }
205 |
206 | private function createRelationTable()
207 | {
208 | Schema::create('select_relations', function ($table) {
209 | $table->increments('id');
210 | $table->integer('parent_id');
211 | $table->foreign('parent_id')->references('id')->on('select_test_models');
212 | $table->string('name');
213 | $table->timestamps();
214 | });
215 | }
216 | }
217 | class SelectTestModel extends Model
218 | {
219 | protected $fillable = ['email'];
220 |
221 | public function relation()
222 | {
223 | return $this->hasOne(SelectRelation::class, 'parent_id');
224 | }
225 |
226 | public function getCustomAttribute()
227 | {
228 | return 'custom';
229 | }
230 | }
231 | class SelectRelation extends Model
232 | {
233 | protected $fillable = ['name', 'parent_id'];
234 | }
235 |
236 | class SelectTestResource extends JsonResource
237 | {
238 | public function toArray($request)
239 | {
240 | return ['resource' => 'resource'];
241 | }
242 | }
243 |
--------------------------------------------------------------------------------