├── .styleci.yml
├── .gitignore
├── tests
├── Models
│ ├── Post.php
│ ├── Comment.php
│ ├── Profile.php
│ └── User.php
├── TestCase.php
└── Feature
│ ├── LinkTest.php
│ └── TraitTest.php
├── SECURITY.md
├── .editorconfig
├── .gitattributes
├── src
├── Exceptions
│ └── SortableException.php
├── View
│ └── Components
│ │ └── SortableLink.php
├── Provider.php
├── Config
│ └── sortable.php
├── Traits
│ └── Sortable.php
└── Support
│ └── SortableLink.php
├── phpunit.xml
├── LICENSE.md
├── composer.json
├── .github
└── workflows
│ └── tests.yml
└── README.md
/.styleci.yml:
--------------------------------------------------------------------------------
1 | preset: psr2
2 |
3 | enabled:
4 | - concat_with_spaces
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /.history
3 | /.vscode
4 | /tests/databases
5 | /vendor
6 | .DS_Store
7 | .phpunit.result.cache
8 | composer.phar
9 | composer.lock
10 |
--------------------------------------------------------------------------------
/tests/Models/Post.php:
--------------------------------------------------------------------------------
1 | belongsTo(Comment::class, 'parent_id');
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/Models/Profile.php:
--------------------------------------------------------------------------------
1 | belongsTo(User::class);
21 | }
22 |
23 | public function compositeSortable($query, $direction)
24 | {
25 | return $query->orderBy('phone', $direction)->orderBy('address', $direction);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior, in case people don't have core.autocrlf set.
2 | * text eol=lf
3 |
4 | # Explicitly declare text files you want to always be normalized and converted
5 | # to native line endings on checkout.
6 | *.c text
7 | *.h text
8 |
9 | # Declare files that will always have CRLF line endings on checkout.
10 | *.sln text eol=crlf
11 |
12 | # Denote all files that are truly binary and should not be modified.
13 | *.png binary
14 | *.jpg binary
15 | *.otf binary
16 | *.eot binary
17 | *.svg binary
18 | *.ttf binary
19 | *.woff binary
20 | *.woff2 binary
21 |
22 | *.css linguist-vendored
23 | *.scss linguist-vendored
24 | *.js linguist-vendored
25 | CHANGELOG.md export-ignore
26 |
--------------------------------------------------------------------------------
/tests/Models/User.php:
--------------------------------------------------------------------------------
1 | hasOne(Profile::class, 'user_id', 'id');
25 | }
26 |
27 | public function addressSortable($query, $direction)
28 | {
29 | return $query->join('profiles', 'users.id', '=', 'profiles.user_id')->orderBy('address', $direction)->select('users.*');
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Exceptions/SortableException.php:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 | ./src
13 |
14 |
15 |
16 |
17 | tests
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2021 Akaunting
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 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "akaunting/laravel-sortable",
3 | "description": "Sortable behavior package for Laravel",
4 | "keywords": [
5 | "laravel",
6 | "sort",
7 | "sortable",
8 | "sorting",
9 | "model",
10 | "view",
11 | "blade",
12 | "column"
13 | ],
14 | "license": "MIT",
15 | "authors": [
16 | {
17 | "name": "Denis Duliçi",
18 | "email": "info@akaunting.com",
19 | "homepage": "https://akaunting.com",
20 | "role": "Developer"
21 | }
22 | ],
23 | "require": {
24 | "php": "^8.0",
25 | "laravel/framework": "^9.0|^10.0"
26 | },
27 | "require-dev": {
28 | "phpunit/phpunit": "^9.5|^10.0",
29 | "orchestra/testbench": "^7.4|^8.0"
30 | },
31 | "autoload": {
32 | "psr-4": {
33 | "Akaunting\\Sortable\\": "src"
34 | }
35 | },
36 | "autoload-dev": {
37 | "psr-4": {
38 | "Akaunting\\Sortable\\Tests\\": "tests"
39 | }
40 | },
41 | "extra": {
42 | "laravel": {
43 | "providers": [
44 | "Akaunting\\Sortable\\Provider"
45 | ]
46 | }
47 | },
48 | "minimum-stability": "dev",
49 | "prefer-stable": true
50 | }
51 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | tests:
7 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.stability }}
8 |
9 | runs-on: ubuntu-latest
10 |
11 | strategy:
12 | matrix:
13 | php: ['8.0', '8.1', '8.2']
14 | laravel: [9.*, 10.*]
15 | stability: [prefer-lowest, prefer-stable]
16 | include:
17 | - laravel: 9.*
18 | testbench: 7.*
19 | - laravel: 10.*
20 | testbench: 8.*
21 | exclude:
22 | - laravel: 10.*
23 | php: 8.0
24 |
25 | steps:
26 | - name: Checkout code
27 | uses: actions/checkout@v2
28 |
29 | - name: Setup PHP
30 | uses: shivammathur/setup-php@v2
31 | with:
32 | php-version: ${{ matrix.php }}
33 | extensions: bcmath, ctype, dom, fileinfo, intl, gd, json, mbstring, pdo, pdo_sqlite, openssl, sqlite, xml, zip
34 | coverage: none
35 |
36 | - name: Install dependencies
37 | run: |
38 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
39 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction
40 | - name: Execute tests
41 | run: vendor/bin/phpunit
42 |
--------------------------------------------------------------------------------
/src/View/Components/SortableLink.php:
--------------------------------------------------------------------------------
1 | column = $column;
50 | $this->title = $title;
51 | $this->query = $query;
52 | $this->arguments = $arguments;
53 | }
54 |
55 | /**
56 | * Get the view / contents that represent the component.
57 | *
58 | * @return \Illuminate\View\View|\Closure|string
59 | */
60 | public function render()
61 | {
62 | return Base::render([
63 | $this->column,
64 | $this->title,
65 | $this->query,
66 | $this->arguments,
67 | ]);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | setUpDatabase();
29 |
30 | $this->setUpModels();
31 | }
32 |
33 | protected function tearDown(): void
34 | {
35 | parent::tearDown();
36 | }
37 |
38 | protected function getPackageProviders($app)
39 | {
40 | return [
41 | Provider::class,
42 | ];
43 | }
44 |
45 | protected function setUpDatabase()
46 | {
47 | config(['database.default' => 'testbench']);
48 |
49 | config(['database.connections.testbench' => [
50 | 'driver' => 'sqlite',
51 | 'database' => ':memory:',
52 | 'prefix' => '',
53 | ],
54 | ]);
55 | }
56 |
57 | protected function setUpModels()
58 | {
59 | $this->user = new User();
60 | $this->profile = new Profile();
61 | $this->post = new Post();
62 | $this->comment = new Comment();
63 | }
64 |
65 | public function getNextClosure()
66 | {
67 | return function () {
68 | return 'next';
69 | };
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Provider.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom(__DIR__ . '/Config/sortable.php', 'sortable');
20 | }
21 |
22 | /**
23 | * Bootstrap the application services.
24 | *
25 | * @return void
26 | */
27 | public function boot()
28 | {
29 | $this->publishes([
30 | __DIR__ . '/Config/sortable.php' => config_path('sortable.php'),
31 | ], 'sortable');
32 |
33 | $this->registerBladeDirectives();
34 | $this->registerBladeComponents();
35 | $this->registerMacros();
36 | }
37 |
38 | public function registerBladeDirectives()
39 | {
40 | $this->callAfterResolving('blade.compiler', function (BladeCompiler $compiler) {
41 | $compiler->directive('sortablelink', function ($expression) {
42 | $expression = ($expression[0] === '(') ? substr($expression, 1, -1) : $expression;
43 |
44 | return "";
45 | });
46 | });
47 | }
48 |
49 | public function registerBladeComponents()
50 | {
51 | Blade::component('sortablelink', SortableLink::class);
52 | }
53 |
54 | public function registerMacros()
55 | {
56 | request()->macro('allFilled', function (array $keys) {
57 | foreach ($keys as $key) {
58 | if (! $this->filled($key)) {
59 | return false;
60 | }
61 | }
62 |
63 | return true;
64 | });
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Config/sortable.php:
--------------------------------------------------------------------------------
1 | [
6 | 'alpha' => [
7 | 'fields' => ['description', 'email', 'name', 'slug'],
8 | 'icon' => 'fa fa-sort-alpha',
9 | ],
10 | 'amount' => [
11 | 'fields' => ['amount', 'price'],
12 | 'icon' => 'fa fa-sort-amount',
13 | ],
14 | 'numeric' => [
15 | 'fields' => ['created_at', 'updated_at', 'level', 'id', 'phone_number'],
16 | 'icon' => 'fa fa-sort-numeric',
17 | ],
18 | ],
19 |
20 | 'icons' => [
21 | 'enabled' => true,
22 |
23 | 'wrapper' => '',
24 |
25 | 'default' => 'fa fa-sort',
26 |
27 | // Icon that shows when generating sortable link for columns not sorted by, not applied if value is null
28 | 'sortable' => 'fa fa-sort',
29 |
30 | 'clickable' => false,
31 |
32 | 'prefix' => ' ',
33 |
34 | 'suffix' => '',
35 |
36 | 'asc_suffix' => '-asc',
37 |
38 | 'desc_suffix' => '-desc',
39 | ],
40 |
41 | // Default anchor class, not applied if value is null
42 | 'anchor_class' => null,
43 |
44 | // Default active anchor class, not applied if value is null
45 | 'active_anchor_class' => null,
46 |
47 | // Default sort direction anchor class, not applied if value is null
48 | 'direction_anchor_class_prefix' => null,
49 |
50 | // Relation - column separator ex: author.name means relation "author" and column "name"
51 | 'relation_column_separator' => '.',
52 |
53 | // Formatting function applied to name of column, use null to turn formatting off
54 | 'formatting_function' => 'ucfirst',
55 |
56 | // Apply formatting function to custom titles as well as column names
57 | 'format_custom_titles' => true,
58 |
59 | // Inject title parameter in query strings, use null to turn injection off
60 | // Example: 'inject_title' => 't' will result in ..user/?t="formatted title of sorted column"
61 | 'inject_title_as' => null,
62 |
63 | // Allow request modification, when default sorting is set but is not in URI (first load)
64 | 'allow_request_modification' => true,
65 |
66 | // Default direction for: $user->sortable('id') usage
67 | 'default_direction' => 'asc',
68 |
69 | // Default direction for non-sorted columns
70 | 'default_direction_unsorted' => 'asc',
71 |
72 | // Use the first defined sortable column (Model::$sortable) as default
73 | // Also applies if sorting parameters are invalid for example: 'sort' => 'name', 'direction' => ''
74 | 'default_first_column' => false,
75 |
76 | // Join type: join vs leftJoin
77 | 'join_type' => 'leftJoin',
78 |
79 | ];
80 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sortable behavior package for Laravel
2 |
3 | 
4 | 
5 | [](https://styleci.io/repos/442271942)
6 | [](LICENSE.md)
7 |
8 | This package allows you to add sortable behavior to `models` and `views`. It ships with a trait where you can set the sortable fields and a blade directive to generate table headers automatically.
9 |
10 | ## Getting Started
11 |
12 | ### 1. Install
13 |
14 | Run the following command:
15 |
16 | ```bash
17 | composer require akaunting/laravel-sortable
18 | ```
19 |
20 | ### 2. Publish
21 |
22 | Publish configuration
23 |
24 | ```bash
25 | php artisan vendor:publish --tag=sortable
26 | ```
27 |
28 | ### 3. Configure
29 |
30 | You can change the column sorting settings of your app from `config/sortable.php` file
31 |
32 | ## Usage
33 |
34 | All you have to do is use the `Sortable` trait inside your model and define the `$sortable` fields.
35 |
36 | ```php
37 | use Akaunting\Sortable\Traits\Sortable;
38 | use Illuminate\Database\Eloquent\Model;
39 |
40 | class Post extends Model
41 | {
42 | use Sortable;
43 | ...
44 |
45 | public $sortable = [
46 | 'id',
47 | 'title',
48 | 'author',
49 | 'created_at',
50 | ];
51 | ...
52 | }
53 | ```
54 |
55 | If you don't define the `$sortable` array, the `Scheme::hasColumn()` function is used which runs an extra database query.
56 |
57 | ### Scope
58 |
59 | The trait adds a `sortable` scope to the model so you can use it just before `paginate`:
60 |
61 | ```php
62 | public function index()
63 | {
64 | $posts = Post::query()->sortable()->paginate(10);
65 |
66 | return view('posts.index')->with(['posts' => $posts]);
67 | }
68 | ```
69 |
70 | You can set also default sorting field which will be applied when URL is empty.
71 |
72 | ```php
73 | $posts = $post->sortable(['author'])->paginate(10); // $post->orderBy('posts.author', 'asc')
74 |
75 | $posts = $post->sortable(['title'])->paginate(10); // $post->orderBy('posts.title', 'asc')
76 |
77 | $posts = $post->sortable(['title' => 'desc'])->paginate(10); // $post->orderBy('posts.title', 'desc')
78 | ```
79 |
80 | ### Blade Directive
81 |
82 | There is also a `blade` directive for you to create sortable links in your views:
83 |
84 | ```blade
85 | @sortablelink('title', trans('general.title'), ['parameter' => 'smile'], ['rel' => 'nofollow'])
86 | ```
87 |
88 | The *first* parameter is the column in database. The *second* one is displayed inside the anchor tag. The *third* one is an `array()`, and it sets the default (GET) query string. The *fourth* one is also an `array()` for additional anchor-tag attributes. You can use a custom URL as 'href' attribute in the fourth parameter, which will append the query string.
89 |
90 | Only the first parameter is required.
91 |
92 | Examples:
93 |
94 | ```blade
95 | @sortablelink('title')
96 | @sortablelink('title', trans('general.title'))
97 | @sortablelink('title', trans('general.title'), ['filter' => 'active, visible'])
98 | @sortablelink('title', trans('general.title'), ['filter' => 'active, visible'], ['class' => 'btn btn-success', 'rel' => 'nofollow', 'href' => route('posts.index')])
99 | ```
100 |
101 | #### Icon Set
102 |
103 | You can use any icon set you want. Just change the `icons.wrapper` from the config file accordingly. By default, it uses Font Awesome.
104 |
105 | ### Blade Component
106 |
107 | Same as the directive, there is also a `blade` component for you to create sortable links in your views:
108 |
109 | ```html
110 |
111 | ```
112 |
113 | ### Sorting Relationships
114 |
115 | The package supports `HasOne` and `BelongsTo` relational sorting:
116 |
117 | ```php
118 | class Post extends Model
119 | {
120 | use Sortable;
121 | ...
122 |
123 | protected $fillable = [
124 | 'title',
125 | 'author_id',
126 | 'body',
127 | ];
128 |
129 | public $sortable = [
130 | 'id',
131 | 'title',
132 | 'author',
133 | 'created_at',
134 | 'updated_at',
135 | ];
136 |
137 | /**
138 | * Get the author associated with the post.
139 | */
140 | public function author()
141 | {
142 | return $this->hasOne(\App\Models\Author::class);
143 | }
144 | ...
145 | }
146 | ```
147 |
148 | And you can use the relation in views:
149 |
150 | ```blade
151 | // resources/views/posts/index.blade.php
152 |
153 | @sortablelink('title', trans('general.title'))
154 | @sortablelink('author.name', trans('general.author'))
155 | ```
156 |
157 | > **Note**: In case there is a self-referencing model (like comments, categories etc.); parent table will be aliased with `parent_` string.
158 |
159 | ### Advanced Relation
160 |
161 | You can also extend the relation sorting feature by creating a function with `Sortable` suffix. There you're free to write your own queries and apply `orderBy()` manually:
162 |
163 | ```php
164 | class User extends Model
165 | {
166 | use Sortable;
167 | ...
168 |
169 | public $sortable = [
170 | 'name',
171 | 'address',
172 | ];
173 |
174 | public function addressSortable($query, $direction)
175 | {
176 | return $query->join('user_details', 'users.id', '=', 'user_details.user_id')
177 | ->orderBy('address', $direction)
178 | ->select('users.*');
179 | }
180 | ...
181 | ```
182 |
183 | The usage in `controller` and `view` remains the same.
184 |
185 | ### Aliasing
186 |
187 | You can declare the `$sortableAs` array in your model and use it to alias (bypass column exists check), and ignore prefixing with table:
188 |
189 | ```php
190 | public $sortableAs = [
191 | 'nick_name',
192 | ];
193 | ```
194 |
195 | In controller
196 |
197 | ```php
198 | $users = $user->select(['name as nick_name'])->sortable(['nick_name'])->paginate(10);
199 | ```
200 |
201 | In view
202 |
203 | ```blade
204 | @sortablelink('nick_name', 'nick')
205 | ```
206 |
207 | It's very useful when you want to sort results using [`withCount()`](https://laravel.com/docs/eloquent-relationships#counting-related-models).
208 |
209 | ## Changelog
210 |
211 | Please see [Releases](../../releases) for more information what has changed recently.
212 |
213 | ## Contributing
214 |
215 | Pull requests are more than welcome. You must follow the PSR coding standards.
216 |
217 | ## Security
218 |
219 | Please review [our security policy](https://github.com/akaunting/laravel-sortable/security/policy) on how to report security vulnerabilities.
220 |
221 | ## Credits
222 |
223 | - [Denis Duliçi](https://github.com/denisdulici)
224 | - [Martin Kiesel](https://github.com/Kyslik)
225 | - [All Contributors](../../contributors)
226 |
227 | ## License
228 |
229 | The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.
230 |
--------------------------------------------------------------------------------
/src/Traits/Sortable.php:
--------------------------------------------------------------------------------
1 | allFilled(['sort', 'direction']) && $this->columnExists($this, request()->get('sort'))) { // allFilled() is macro
26 | return $this->queryOrderBuilder($query, request()->only(['sort', 'direction']));
27 | }
28 |
29 | if (is_null($defaultParameters)) {
30 | $defaultParameters = $this->getDefaultSortable();
31 | }
32 |
33 | if (! is_null($defaultParameters)) {
34 | $defaultSortArray = $this->formatToParameters($defaultParameters);
35 |
36 | if (config('sortable.allow_request_modification', true) && ! empty($defaultSortArray)) {
37 | request()->merge($defaultSortArray);
38 | }
39 |
40 | return $this->queryOrderBuilder($query, $defaultSortArray);
41 | }
42 |
43 | return $query;
44 | }
45 |
46 | /**
47 | * Returns the first element of defined sortable columns from the Model
48 | *
49 | * @return array|null
50 | */
51 | private function getDefaultSortable()
52 | {
53 | if (config('sortable.default_first_column', false)) {
54 | $sortBy = Arr::first($this->sortable);
55 |
56 | if (! is_null($sortBy)) {
57 | return [$sortBy => config('sortable.default_direction', 'asc')];
58 | }
59 | }
60 |
61 | return null;
62 | }
63 |
64 | /**
65 | * @throws SortableException
66 | */
67 | private function queryOrderBuilder(Builder $query, array $sortParameters): Builder
68 | {
69 | $model = $this;
70 |
71 | list($column, $direction) = $this->parseParameters($sortParameters);
72 |
73 | if (is_null($column)) {
74 | return $query;
75 | }
76 |
77 | $explodeResult = SortableLink::explodeSortParameter($column);
78 | if (! empty($explodeResult)) {
79 | $relationName = $explodeResult[0];
80 | $column = $explodeResult[1];
81 |
82 | try {
83 | $relation = $query->getRelation($relationName);
84 | $query = $this->queryJoinBuilder($query, $relation);
85 | } catch (BadMethodCallException $e) {
86 | throw new SortableException($relationName, 1, $e);
87 | } catch (\Exception $e) {
88 | throw new SortableException($relationName, 2, $e);
89 | }
90 |
91 | $model = $relation->getRelated();
92 | }
93 |
94 | if (method_exists($model, Str::camel($column) . 'Sortable')) {
95 | return call_user_func_array([$model, Str::camel($column) . 'Sortable'], [$query, $direction]);
96 | }
97 |
98 | if (isset($model->sortableAs) && in_array($column, $model->sortableAs)) {
99 | $query = $query->orderBy($column, $direction);
100 | } elseif ($this->columnExists($model, $column)) {
101 | $column = $model->getTable() . '.' . $column;
102 | $query = $query->orderBy($column, $direction);
103 | }
104 |
105 | return $query;
106 | }
107 |
108 | private function parseParameters(array $parameters): array
109 | {
110 | $column = Arr::get($parameters, 'sort');
111 | if (empty($column)) {
112 | return [null, null];
113 | }
114 |
115 | $direction = Arr::get($parameters, 'direction', []);
116 | if (! in_array(strtolower($direction), ['asc', 'desc'])) {
117 | $direction = config('sortable.default_direction', 'asc');
118 | }
119 |
120 | return [$column, $direction];
121 | }
122 |
123 | /**
124 | * @param BelongsTo|HasOne $relation
125 | *
126 | * @throws \Exception
127 | */
128 | private function queryJoinBuilder(Builder $query, $relation): Builder
129 | {
130 | $relatedTable = $relation->getRelated()->getTable();
131 | $parentTable = $relation->getParent()->getTable();
132 |
133 | if ($parentTable === $relatedTable) {
134 | $query = $query->from($parentTable . ' as parent_' . $parentTable);
135 | $parentTable = 'parent_' . $parentTable;
136 | $relation->getParent()->setTable($parentTable);
137 | }
138 |
139 | if ($relation instanceof HasOne || $relation instanceof MorphOne) {
140 | $relatedPrimaryKey = $relation->getQualifiedForeignKeyName();
141 | $parentPrimaryKey = $relation->getQualifiedParentKeyName();
142 | } elseif ($relation instanceof BelongsTo) {
143 | $relatedPrimaryKey = $relation->getQualifiedOwnerKeyName();
144 | $parentPrimaryKey = $relation->getQualifiedForeignKeyName();
145 | } else {
146 | throw new \Exception();
147 | }
148 |
149 | return $this->formJoin($query, $parentTable, $relatedTable, $parentPrimaryKey, $relatedPrimaryKey);
150 | }
151 |
152 | private function columnExists($model, $column): bool
153 | {
154 | return isset($model->sortable)
155 | ? in_array($column, $model->sortable)
156 | : Schema::connection($model->getConnectionName())->hasColumn($model->getTable(), $column);
157 | }
158 |
159 | /**
160 | * @param array|string $array
161 | *
162 | * @return array
163 | */
164 | private function formatToParameters($array): array
165 | {
166 | if (empty($array)) {
167 | return [];
168 | }
169 |
170 | $defaultDirection = config('sortable.default_direction', 'asc');
171 |
172 | if (is_string($array)) {
173 | return ['sort' => $array, 'direction' => $defaultDirection];
174 | }
175 |
176 | return (key($array) === 0)
177 | ? ['sort' => $array[0], 'direction' => $defaultDirection]
178 | : ['sort' => key($array), 'direction' => reset($array)];
179 | }
180 |
181 | /**
182 | * @param $query
183 | * @param $parentTable
184 | * @param $relatedTable
185 | * @param $parentPrimaryKey
186 | * @param $relatedPrimaryKey
187 | *
188 | * @return mixed
189 | */
190 | private function formJoin($query, $parentTable, $relatedTable, $parentPrimaryKey, $relatedPrimaryKey)
191 | {
192 | $joinType = config('sortable.join_type', 'leftJoin');
193 |
194 | return $query->select($parentTable . '.*')->{$joinType}($relatedTable, $parentPrimaryKey, '=', $relatedPrimaryKey);
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/tests/Feature/LinkTest.php:
--------------------------------------------------------------------------------
1 | 0, 'another-key' => null, 'another-one' => 1];
16 | Request::replace($parameters);
17 |
18 | $link = SortableLink::render(['column']);
19 | $expected = http_build_query($parameters);
20 |
21 | $this->assertStringContainsString($expected, $link);
22 | }
23 |
24 | public function testQueryStringCanHoldArray(): void
25 | {
26 | $parameters = ['key' => ['part1', 'part2'], 'another-one' => 1];
27 | Request::replace($parameters);
28 |
29 | $link = SortableLink::render(['column']);
30 | $expected = http_build_query($parameters);
31 |
32 | $this->assertStringContainsString($expected, $link);
33 | }
34 |
35 | public function testInjectTitleInQueryStrings(): void
36 | {
37 | Config::set('sortable.inject_title_as', 'title');
38 | SortableLink::render(['column', 'ColumnTitle']);
39 |
40 | $expected = ['title' => 'ColumnTitle'];
41 |
42 | $this->assertEquals($expected, Request::all());
43 | }
44 |
45 | public function testInjectTitleInQueryStringsIsOff(): void
46 | {
47 | Config::set('sortable.inject_title_as', null);
48 | SortableLink::render(['column', 'ColumnTitle']);
49 |
50 | $this->assertEquals([], Request::all());
51 | }
52 |
53 | public function testGeneratingAnchorAttributes(): void
54 | {
55 | $link = SortableLink::render(['column', 'ColumnTitle', ['a' => 'b'], ['c' => 'd']]);
56 |
57 | $icon = config('sortable.icons.default');
58 |
59 | $expected = 'ColumnTitle' . SortableLink::getIconHtml($icon);
60 |
61 | $this->assertSame($expected, $link);
62 | }
63 |
64 | public function testGeneratingTitleWithoutFormattingFunction(): void
65 | {
66 | Config::set('sortable.formatting_function', null);
67 | $link = SortableLink::render(['column']);
68 |
69 | $icon = config('sortable.icons.default');
70 |
71 | $expected = 'column' . SortableLink::getIconHtml($icon);
72 |
73 | $this->assertSame($expected, $link);
74 | }
75 |
76 | public function testGeneratingTitle(): void
77 | {
78 | Config::set('sortable.formatting_function', 'ucfirst');
79 | Config::set('sortable.format_custom_titles', true);
80 | $link = SortableLink::render(['column']);
81 |
82 | $icon = config('sortable.icons.default');
83 |
84 | $expected = 'Column' . SortableLink::getIconHtml($icon);
85 |
86 | $this->assertSame($expected, $link);
87 | }
88 |
89 | public function testCustomTitle(): void
90 | {
91 | Config::set('sortable.formatting_function', 'ucfirst');
92 | Config::set('sortable.format_custom_titles', true);
93 | $link = SortableLink::render(['column', 'ColumnTitle']);
94 |
95 | $icon = config('sortable.icons.default');
96 |
97 | $expected = 'ColumnTitle' . SortableLink::getIconHtml($icon);
98 |
99 | $this->assertSame($expected, $link);
100 | }
101 |
102 | public function testCustomTitleWithoutFormatting(): void
103 | {
104 | Config::set('sortable.formatting_function', 'ucfirst');
105 | Config::set('sortable.format_custom_titles', false);
106 | $link = SortableLink::render(['column', 'ColumnTitle']);
107 |
108 | $icon = config('sortable.icons.default');
109 |
110 | $expected = 'ColumnTitle' . SortableLink::getIconHtml($icon);
111 |
112 | $this->assertSame($expected, $link);
113 | }
114 |
115 | public function testCustomTitleWithHTML(): void
116 | {
117 | Config::set('sortable.formatting_function', 'ucfirst');
118 | Config::set('sortable.format_custom_titles', true);
119 | $link = SortableLink::render(['column', new HtmlString('ColumnTitle')]);
120 |
121 | $icon = config('sortable.icons.default');
122 |
123 | $expected = 'ColumnTitle' . SortableLink::getIconHtml($icon);
124 |
125 | $this->assertSame($expected, $link);
126 | }
127 |
128 | public function testCustomHrefAttribute(): void
129 | {
130 | $link = SortableLink::render(['column', 'ColumnTitle', ['a' => 'b'], ['c' => 'd', 'href' => 'http://localhost/custom-path']]);
131 |
132 | $icon = config('sortable.icons.default');
133 |
134 | $expected = 'ColumnTitle' . SortableLink::getIconHtml($icon);
135 |
136 | $this->assertSame($expected, $link);
137 | }
138 |
139 | public function testParseParameters(): void
140 | {
141 | $parameters = ['column'];
142 | $resultArray = SortableLink::parseParameters($parameters);
143 | $expected = ['column', 'column', null, [], []];
144 | $this->assertEquals($expected, $resultArray);
145 |
146 | $parameters = ['column', 'ColumnTitle'];
147 | $resultArray = SortableLink::parseParameters($parameters);
148 | $expected = ['column', 'column', 'ColumnTitle', [], []];
149 | $this->assertEquals($expected, $resultArray);
150 |
151 | $parameters = ['column', 'ColumnTitle', ['world' => 'matrix']];
152 | $resultArray = SortableLink::parseParameters($parameters);
153 | $expected = ['column', 'column', 'ColumnTitle', ['world' => 'matrix'], []];
154 | $this->assertEquals($expected, $resultArray);
155 |
156 | $parameters = ['column', 'ColumnTitle', ['world' => 'matrix'], ['white' => 'rabbit']];
157 | $resultArray = SortableLink::parseParameters($parameters);
158 | $expected = ['column', 'column', 'ColumnTitle', ['world' => 'matrix'], ['white' => 'rabbit']];
159 | $this->assertEquals($expected, $resultArray);
160 |
161 | $parameters = ['relation.column'];
162 | $resultArray = SortableLink::parseParameters($parameters);
163 | $expected = ['column', 'relation.column', null, [], []];
164 | $this->assertEquals($expected, $resultArray);
165 |
166 | $parameters = ['relation.column', 'ColumnTitle'];
167 | $resultArray = SortableLink::parseParameters($parameters);
168 | $expected = ['column', 'relation.column', 'ColumnTitle', [], []];
169 | $this->assertEquals($expected, $resultArray);
170 |
171 | $parameters = ['relation.column', 'ColumnTitle', ['world' => 'matrix']];
172 | $resultArray = SortableLink::parseParameters($parameters);
173 | $expected = ['column', 'relation.column', 'ColumnTitle', ['world' => 'matrix'], []];
174 | $this->assertEquals($expected, $resultArray);
175 |
176 | $parameters = ['relation.column', 'ColumnTitle', ['world' => 'matrix'], ['red' => 'pill']];
177 | $resultArray = SortableLink::parseParameters($parameters);
178 | $expected = ['column', 'relation.column', 'ColumnTitle', ['world' => 'matrix'], ['red' => 'pill']];
179 | $this->assertEquals($expected, $resultArray);
180 | }
181 |
182 | public function testGetOneToOneSort(): void
183 | {
184 | $sortParameter = 'relation-name.column';
185 | $resultArray = SortableLink::explodeSortParameter($sortParameter);
186 | $expected = ['relation-name', 'column'];
187 | $this->assertEquals($expected, $resultArray);
188 |
189 | $sortParameter = 'column';
190 | $resultArray = SortableLink::explodeSortParameter($sortParameter);
191 | $expected = [];
192 | $this->assertEquals($expected, $resultArray);
193 | }
194 |
195 | public function testGetOneToOneSortThrowsException(): void
196 | {
197 | $this->expectException('\Exception');
198 | $this->expectExceptionCode(0);
199 | $sortParameter = 'relation-name..column';
200 | SortableLink::explodeSortParameter($sortParameter);
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/src/Support/SortableLink.php:
--------------------------------------------------------------------------------
1 | merge([$mergeTitleAs => $title]);
22 | }
23 |
24 | list($icon, $direction) = self::getDirectionAndIcon($sortColumn, $sortParameter);
25 |
26 | $trailingTag = self::getTrailingTag($icon);
27 |
28 | $anchorClass = self::getAnchorClass($sortParameter, $anchorAttributes);
29 |
30 | $anchorAttributesString = self::buildAnchorAttributesString($anchorAttributes);
31 |
32 | $queryString = self::buildQueryString($queryParameters, $sortParameter, $direction);
33 |
34 | $url = self::buildUrl($queryString, $anchorAttributes);
35 |
36 | return '' . e($title) . $trailingTag;
37 | }
38 |
39 | /**
40 | * @throws SortableException
41 | */
42 | public static function parseParameters(array $parameters): array
43 | {
44 | //TODO: let 2nd parameter be both title, or default query parameters
45 | //TODO: needs some checks before determining $title
46 | $explodeResult = self::explodeSortParameter($parameters[0]);
47 | $sortColumn = (empty($explodeResult)) ? $parameters[0] : $explodeResult[1];
48 | $title = (count($parameters) === 1) ? null : $parameters[1];
49 | $queryParameters = (isset($parameters[2]) && is_array($parameters[2])) ? $parameters[2] : [];
50 | $anchorAttributes = (isset($parameters[3]) && is_array($parameters[3])) ? $parameters[3] : [];
51 |
52 | return [$sortColumn, $parameters[0], $title, $queryParameters, $anchorAttributes];
53 | }
54 |
55 | /**
56 | * Explodes parameter if possible and returns array [column, relation]
57 | * Empty array is returned if explode could not run eg: separator was not found.
58 | *
59 | * @throws SortableException
60 | */
61 | public static function explodeSortParameter(string $parameter): array
62 | {
63 | $separator = config('sortable.relation_column_separator');
64 |
65 | if (Str::contains($parameter, $separator)) {
66 | $oneToOneSort = explode($separator, $parameter);
67 |
68 | if (count($oneToOneSort) !== 2) {
69 | throw new SortableException();
70 | }
71 |
72 | return $oneToOneSort;
73 | }
74 |
75 | return [];
76 | }
77 |
78 | /**
79 | * @param string|Htmlable|null $title
80 | *
81 | * @return string|Htmlable
82 | */
83 | private static function applyFormatting($title, string $sortColumn)
84 | {
85 | if ($title instanceof Htmlable) {
86 | return $title;
87 | }
88 |
89 | if ($title === null) {
90 | $title = $sortColumn;
91 | } elseif (! config('sortable.format_custom_titles')) {
92 | return $title;
93 | }
94 |
95 | $formatting_function = config('sortable.formatting_function');
96 | if (! is_null($formatting_function) && function_exists($formatting_function)) {
97 | $title = call_user_func($formatting_function, $title);
98 | }
99 |
100 | // clear special chars
101 | $title = htmlspecialchars_decode($title, ENT_QUOTES);
102 |
103 | return $title;
104 | }
105 |
106 | private static function getDirectionAndIcon($sortColumn, $sortParameter): array
107 | {
108 | $icon = self::selectIcon($sortColumn);
109 |
110 | $sort = request()->get('sort');
111 | $dir = request()->get('direction');
112 |
113 | if (($sort == $sortParameter) && in_array($dir, ['asc', 'desc'])) {
114 | $icon .= ($dir === 'asc')
115 | ? config('sortable.icons.asc_suffix')
116 | : config('sortable.icons.desc_suffix');
117 |
118 | $direction = ($dir === 'desc') ? 'asc' : 'desc';
119 | } else {
120 | $icon = config('sortable.icons.sortable');
121 | $direction = config('sortable.default_direction_unsorted');
122 | }
123 |
124 | $icon = static::getIconHtml($icon);
125 |
126 | return [$icon, $direction];
127 | }
128 |
129 | private static function selectIcon($sortColumn): string
130 | {
131 | $icon = config('sortable.icons.default');
132 |
133 | foreach (config('sortable.types') as $value) {
134 | if (in_array($sortColumn, $value['fields'])) {
135 | $icon = $value['icon'];
136 | }
137 | }
138 |
139 | return $icon;
140 | }
141 |
142 | /**
143 | * @param string|null $icon
144 | */
145 | private static function getTrailingTag($icon): string
146 | {
147 | if (! config('sortable.icons.enabled')) {
148 | return '';
149 | }
150 |
151 | if (config('sortable.icons.clickable') === true) {
152 | return $icon . '';
153 | }
154 |
155 | return '' . $icon;
156 | }
157 |
158 | /**
159 | * Take care of special case, when `class` is passed to the sortablelink.
160 | */
161 | private static function getAnchorClass(string $sortColumn, array &$anchorAttributes = []): string
162 | {
163 | $class = [];
164 |
165 | $anchorClass = config('sortable.anchor_class');
166 | if ($anchorClass !== null) {
167 | $class[] = $anchorClass;
168 | }
169 |
170 | $activeClass = config('sortable.active_anchor_class');
171 | if (($activeClass !== null) && self::shouldShowActive($sortColumn)) {
172 | $class[] = $activeClass;
173 | }
174 |
175 | $directionClassPrefix = config('sortable.direction_anchor_class_prefix');
176 | if (($directionClassPrefix !== null) && self::shouldShowActive($sortColumn)) {
177 | $class[] = $directionClassPrefix . (request()->get('direction') === 'asc')
178 | ? config('sortable.asc_suffix', '-asc')
179 | : config('sortable.desc_suffix', '-desc');
180 | }
181 |
182 | if (isset($anchorAttributes['class'])) {
183 | $class = array_merge($class, explode(' ', $anchorAttributes['class']));
184 |
185 | unset($anchorAttributes['class']);
186 | }
187 |
188 | return (empty($class)) ? '' : ' class="' . implode(' ', $class) . '"';
189 | }
190 |
191 | private static function shouldShowActive(string $sortColumn): bool
192 | {
193 | return request()->has('sort') && (request()->get('sort') == $sortColumn);
194 | }
195 |
196 | private static function buildQueryString(array $queryParameters, string $sortParameter, string $direction): string
197 | {
198 | $checkStrlenOrArray = function ($element) {
199 | return is_array($element) ? $element : strlen($element);
200 | };
201 |
202 | $persistParameters = array_filter(request()->except('sort', 'direction', 'page'), $checkStrlenOrArray);
203 | $queryString = http_build_query(array_merge($queryParameters, $persistParameters, [
204 | 'sort' => $sortParameter,
205 | 'direction' => $direction,
206 | ]));
207 |
208 | return $queryString;
209 | }
210 |
211 | private static function buildAnchorAttributesString(array $anchorAttributes): string
212 | {
213 | if (empty($anchorAttributes)) {
214 | return '';
215 | }
216 |
217 | unset($anchorAttributes['href']);
218 |
219 | $attributes = [];
220 | foreach ($anchorAttributes as $k => $v) {
221 | $attributes[] = $k . ('' != $v ? '="' . $v . '"' : '');
222 | }
223 |
224 | return ' ' . implode(' ', $attributes);
225 | }
226 |
227 | private static function buildUrl(string $queryString, array $anchorAttributes): string
228 | {
229 | $path = isset($anchorAttributes['href']) ? $anchorAttributes['href'] : request()->path();
230 |
231 | return url($path . "?" . $queryString);
232 | }
233 |
234 | /**
235 | * @param string|null $icon
236 | */
237 | public static function getIconHtml($icon): string
238 | {
239 | $prefix = config('sortable.icons.prefix');
240 | $suffix = config('sortable.icons.suffix');
241 | $wrapper = config('sortable.icons.wrapper');
242 |
243 | return $prefix . str_replace('{icon}', $icon, $wrapper) . $suffix;
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/tests/Feature/TraitTest.php:
--------------------------------------------------------------------------------
1 | set('sortable.default_first_column', false);
15 | $query = $this->user->scopeSortable($this->user->newQuery());
16 | $this->assertEmpty($query->getQuery()->orders);
17 |
18 | config()->set('sortable.default_first_column', true);
19 | $this->user->sortable = ['name', 'id'];
20 |
21 | $query = $this->user->scopeSortable($this->user->newQuery());
22 | $this->assertEquals([
23 | [
24 | 'column' => 'users.' . Arr::first($this->user->sortable),
25 | 'direction' => $this->direction,
26 | ],
27 | ], $query->getQuery()->orders);
28 |
29 | $query = $this->post->scopeSortable($this->post->newQuery());
30 | $this->assertEmpty($query->getQuery()->orders);
31 | }
32 |
33 | public function testSortableWithRequestParameters(): void
34 | {
35 | config()->set('sortable.default_first_column', false);
36 |
37 | $usersTable = $this->user->getTable();
38 | Request::replace(['sort' => 'name', 'direction' => 'asc']);
39 | $resultArray = $this->user->scopeSortable($this->user->newQuery())->getQuery()->orders;
40 | $expected = ['column' => $usersTable . '.name', 'direction' => 'asc'];
41 | $this->assertEquals($expected, head($resultArray));
42 |
43 | Request::replace(['sort' => 'name', 'direction' => 'desc']);
44 | $resultArray = $this->user->scopeSortable($this->user->newQuery())->getQuery()->orders;
45 | $expected = ['column' => $usersTable . '.name', 'direction' => 'desc'];
46 | $this->assertEquals($expected, head($resultArray));
47 |
48 | Request::replace(['sort' => 'name', 'direction' => '']);
49 | $result = $this->user->scopeSortable($this->user->newQuery())->getQuery()->orders;
50 | $this->assertNull($result);
51 |
52 | Request::replace(['sort' => '', 'direction' => 'asc']);
53 | $result = $this->user->scopeSortable($this->user->newQuery())->getQuery()->orders;
54 | $this->assertNull($result);
55 |
56 | Request::replace(['sort' => '', 'direction' => '']);
57 | $result = $this->user->scopeSortable($this->user->newQuery())->getQuery()->orders;
58 | $this->assertNull($result);
59 |
60 | Request::replace(['sort' => 'name']);
61 | $result = $this->user->scopeSortable($this->user->newQuery())->getQuery()->orders;
62 | $this->assertNull($result);
63 |
64 | Request::replace(['sort' => '']);
65 | $result = $this->user->scopeSortable($this->user->newQuery())->getQuery()->orders;
66 | $this->assertNull($result);
67 | }
68 |
69 | public function testSortableWithDefaultAndWithoutRequestParameters(): void
70 | {
71 | $usersTable = $this->user->getTable();
72 | $default = [
73 | 'name' => 'desc',
74 | ];
75 |
76 | $resultArray = $this->user->scopeSortable($this->user->newQuery(), $default)->getQuery()->orders;
77 | $expected = ['column' => $usersTable . '.name', 'direction' => 'desc'];
78 | $this->assertEquals($expected, head($resultArray));
79 | }
80 |
81 | public function testSortableQueryJoinBuilder(): void
82 | {
83 | $query = $this->user->newQuery()->with(['profile']);
84 | $relation = $query->getRelation('profile');
85 | $resultQuery = $this->invokeMethod($this->user, 'queryJoinBuilder', [$query, $relation]);
86 | $expectedQuery = $this->user->newQuery()->select('users.*')->leftJoin('profiles', 'users.id', '=', 'profiles.user_id');
87 | $this->assertEquals($expectedQuery->toSql(), $resultQuery->toSql());
88 |
89 | $query = $this->profile->newQuery()->with(['user']);
90 | $relation = $query->getRelation('user');
91 | $resultQuery = $this->invokeMethod($this->user, 'queryJoinBuilder', [$query, $relation]);
92 | $expectedQuery = $this->profile->newQuery()->select('profiles.*')->leftJoin('users', 'profiles.user_id', '=', 'users.id');
93 | $this->assertEquals($expectedQuery->toSql(), $resultQuery->toSql());
94 |
95 | $query = $this->comment->newQuery()->with(['parent']);
96 | $relation = $query->getRelation('parent');
97 | $resultQuery = $this->invokeMethod($this->comment, 'queryJoinBuilder', [$query, $relation]);
98 | $expectedQuery = $this->comment->newQuery()->from('comments as parent_comments')->select('parent_comments.*')
99 | ->leftJoin('comments', 'parent_comments.parent_id', '=', 'comments.id');
100 | $this->assertEquals($expectedQuery->toSql(), $resultQuery->toSql());
101 | }
102 |
103 | public function testSortableOverridingQueryOrderBuilder(): void
104 | {
105 | $sortParameters = ['sort' => 'address', 'direction' => 'desc'];
106 | $query = $this->user->newQuery();
107 | $resultQuery = $this->invokeMethod($this->user, 'queryOrderBuilder', [$query, $sortParameters]);
108 | $expectedQuery = $this->user->newQuery()
109 | ->join('profiles', 'users.id', '=', 'profiles.user_id')
110 | ->orderBy('address', 'desc')
111 | ->select('users.*');
112 |
113 | $this->assertEquals($expectedQuery, $resultQuery);
114 | }
115 |
116 | public function testSortableOverridingQueryOrderBuilderOnRelation(): void
117 | {
118 | $sortParameters = ['sort' => 'profile.composite', 'direction' => 'desc'];
119 | $query = $this->user->newQuery();
120 |
121 | $resultQuery = $this->invokeMethod($this->user, 'queryOrderBuilder', [$query, $sortParameters]);
122 |
123 | $expectedQuery = $this->user->newQuery()
124 | ->leftJoin('profiles', 'users.id', '=', 'profiles.user_id')
125 | ->orderBy('phone', 'desc')
126 | ->orderBy('address', 'desc')
127 | ->select('users.*');
128 |
129 | $this->assertEquals($expectedQuery, $resultQuery);
130 | }
131 |
132 | public function testSortableAs(): void
133 | {
134 | $sortParameters = ['sort' => 'nick_name', 'direction' => 'asc'];
135 | $query = $this->user->newQuery()->select('name as nick_name');
136 | $resultQuery = $this->invokeMethod($this->user, 'queryOrderBuilder', [$query, $sortParameters]);
137 | $expectedQuery = $this->user->newQuery()->select('name as nick_name')->orderBy('nick_name', 'asc');
138 |
139 | $this->assertEquals($expectedQuery, $resultQuery);
140 | }
141 |
142 | public function testSortableQueryJoinBuilderThrowsException(): void
143 | {
144 | $this->expectException('\Exception');
145 | $this->expectExceptionCode(0);
146 |
147 | $query = $this->user->hasMany(Profile::class)->newQuery();
148 | $relation = $query->getRelation('profile');
149 |
150 | $this->invokeMethod($this->user, 'queryJoinBuilder', [$query, $relation]);
151 | }
152 |
153 | public function testSortableWithDefaultUsesConfig(): void
154 | {
155 | $usersTable = $this->user->getTable();
156 | $default = 'name';
157 |
158 | $resultArray = $this->user->scopeSortable($this->user->newQuery(), $default)->getQuery()->orders;
159 | $expected = ['column' => $usersTable . '.name', 'direction' => $this->direction];
160 | $this->assertEquals($expected, head($resultArray));
161 |
162 | $default = ['name'];
163 |
164 | $resultArray = $this->user->scopeSortable($this->user->newQuery(), $default)->getQuery()->orders;
165 | $expected = ['column' => $usersTable . '.name', 'direction' => $this->direction];
166 | $this->assertEquals($expected, head($resultArray));
167 | }
168 |
169 | public function testParseParameters(): void
170 | {
171 | $array = [];
172 | $resultArray = $this->invokeMethod($this->user, 'parseParameters', [$array]);
173 | $expected = [null, null];
174 | $this->assertEquals($expected, $resultArray);
175 |
176 | $array = ['sort' => ''];
177 | $resultArray = $this->invokeMethod($this->user, 'parseParameters', [$array]);
178 | $expected = [null, null];
179 | $this->assertEquals($expected, $resultArray);
180 |
181 | $array = ['direction' => ''];
182 | $resultArray = $this->invokeMethod($this->user, 'parseParameters', [$array]);
183 | $expected = [null, null];
184 | $this->assertEquals($expected, $resultArray);
185 |
186 | $array = ['direction' => 'foo'];
187 | $resultArray = $this->invokeMethod($this->user, 'parseParameters', [$array]);
188 | $expected = [null, null];
189 | $this->assertEquals($expected, $resultArray);
190 |
191 | $array = ['sort' => 'foo', 'direction' => ''];
192 | $resultArray = $this->invokeMethod($this->user, 'parseParameters', [$array]);
193 | $expected = ['foo', $this->direction];
194 | $this->assertEquals($expected, $resultArray);
195 |
196 | $array = ['sort' => 'foo', 'direction' => 'desc'];
197 | $resultArray = $this->invokeMethod($this->user, 'parseParameters', [$array]);
198 | $expected = ['foo', 'desc'];
199 | $this->assertEquals($expected, $resultArray);
200 |
201 | $array = ['sort' => 'foo', 'direction' => 'asc'];
202 | $resultArray = $this->invokeMethod($this->user, 'parseParameters', [$array]);
203 | $expected = ['foo', 'asc'];
204 | $this->assertEquals($expected, $resultArray);
205 |
206 | $array = ['sort' => 'foo', 'direction' => 'bar'];
207 | $resultArray = $this->invokeMethod($this->user, 'parseParameters', [$array]);
208 | $expected = ['foo', $this->direction];
209 | $this->assertEquals($expected, $resultArray);
210 | }
211 |
212 | public function testFormatToParameters(): void
213 | {
214 | $array = [];
215 | $resultArray = $this->invokeMethod($this->user, 'formatToParameters', [$array]);
216 | $expected = [];
217 | $this->assertEquals($expected, $resultArray);
218 |
219 | $array = null;
220 | $resultArray = $this->invokeMethod($this->user, 'formatToParameters', [$array]);
221 | $expected = [];
222 | $this->assertEquals($expected, $resultArray);
223 |
224 | $array = 'foo';
225 | $resultArray = $this->invokeMethod($this->user, 'formatToParameters', [$array]);
226 | $expected = ['sort' => 'foo', 'direction' => $this->direction];
227 | $this->assertEquals($expected, $resultArray);
228 |
229 | $array = ['foo'];
230 | $resultArray = $this->invokeMethod($this->user, 'formatToParameters', [$array]);
231 | $expected = ['sort' => 'foo', 'direction' => $this->direction];
232 | $this->assertEquals($expected, $resultArray);
233 |
234 | $array = ['foo' => 'desc'];
235 | $resultArray = $this->invokeMethod($this->user, 'formatToParameters', [$array]);
236 | $expected = ['sort' => 'foo', 'direction' => 'desc'];
237 | $this->assertEquals($expected, $resultArray);
238 |
239 | $array = ['foo' => 'desc', 'bar' => 'asc'];
240 | $resultArray = $this->invokeMethod($this->user, 'formatToParameters', [$array]);
241 | $expected = ['sort' => 'foo', 'direction' => 'desc'];
242 | $this->assertEquals($expected, $resultArray);
243 | }
244 |
245 | /**
246 | * Call protected/private method of a class.
247 | *
248 | * @param object &$object Instantiated object that we will run method on.
249 | * @param string $methodName Method name to call
250 | * @param array $parameters Array of parameters to pass into method.
251 | *
252 | * @return mixed Method return.
253 | * @throws \ReflectionException
254 | */
255 | protected function invokeMethod(object &$object, string $methodName, array $parameters = [])
256 | {
257 | $reflection = new \ReflectionClass(get_class($object));
258 | $method = $reflection->getMethod($methodName);
259 | $method->setAccessible(true);
260 |
261 | return $method->invokeArgs($object, $parameters);
262 | }
263 | }
264 |
--------------------------------------------------------------------------------