diff --git a/README.md b/README.md index c7297de..c3278e0 100755 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Add the package to your application service providers in `config/app.php` Publish the package configuration file to your application. - $ php artisan vendor:publish --provider="Kyslik\ColumnSortable\ColumnSortableServiceProvider" --tag="columnsortable" + $ php artisan vendor:publish --provider="Kyslik\ColumnSortable\ColumnSortableServiceProvider" --tag="config" See configuration file [(`config/columnsortable.php`)](https://github.com/Kyslik/column-sortable/blob/master/src/config/columnsortable.php) yourself and make adjustments as you wish. @@ -72,22 +72,25 @@ See configuration file [(`config/columnsortable.php`)](https://github.com/Kyslik Use `Sortable` trait inside your `Eloquent` model(s). Define `$sortable` array (see example code below). ->`Scheme::hasColumn()` is run only when `$sortable` is not defined - less DB hits per request. +> **Note**: `Scheme::hasColumn()` is run only when `$sortable` is not defined - less DB hits per request. ``` use Kyslik\ColumnSortable\Sortable; -class User extends Model implements AuthenticatableContract, CanResetPasswordContract { - +class User extends Model implements AuthenticatableContract, CanResetPasswordContract +{ use Authenticatable, CanResetPassword, Sortable; ... - protected $sortable = ['id', - 'name', - 'email', - 'created_at', - 'updated_at']; + public $sortable = ['id', + 'name', + 'email', + 'created_at', + 'updated_at']; + + ... +} ``` You're set to go. @@ -110,19 +113,19 @@ Sortablelink blade extension distinguishes between "types" (numeric, amount and ``` 'columns' => [ - 'numeric' => [ - 'rows' => ['created_at', 'updated_at', 'level', 'id'], - 'class' => 'fa fa-sort-numeric' - ], - 'amount' => [ - 'rows' => ['price'], - 'class' => 'fa fa-sort-amount' - ], - 'alpha' => [ - 'rows' => ['name', 'description', 'email', 'slug'], - 'class' => 'fa fa-sort-alpha', - ], + 'numeric' => [ + 'rows' => ['created_at', 'updated_at', 'level', 'id'], + 'class' => 'fa fa-sort-numeric' ], + 'amount' => [ + 'rows' => ['price'], + 'class' => 'fa fa-sort-amount' + ], + 'alpha' => [ + 'rows' => ['name', 'description', 'email', 'slug'], + 'class' => 'fa fa-sort-alpha', + ], +], ``` Rest of the [config file](https://github.com/Kyslik/column-sortable/blob/master/src/config/columnsortable.php) should be crystal clear and I advise you to read it. @@ -145,25 +148,27 @@ Route::get('users', ['as' => 'users.index', 'uses' => 'HomeController@index']); public function index(User $user) { $users = $user->sortable()->paginate(10); - - return view('user.index')->withUsers($users); + + return view('user.index')->withUsers($users); } ``` -You can set default sort (when nothing is in (URL) query strings yet). +You can set default sort when nothing is in (URL) query strings yet. +> **For example**: page is loaded for first time, default order is [configurable](https://github.com/Kyslik/column-sortable/blob/master/src/config/columnsortable.php#L77) (asc) ``` +$users = $user->sortable('name')->paginate(10); +//generate ->orderBy('name', 'asc') + +$users = $user->sortable(['name'])->paginate(10); //generate ->orderBy('name', 'asc') -$users = $user->sortable(['name'])->paginate(10); //default order is asc -//generate ->orderBy('id', 'desc') -$users = $user->sortable(['id' => 'desc'])->paginate(10); +$users = $user->sortable(['name' => 'desc'])->paginate(10); +//generate ->orderBy('name', 'desc') ``` ### View -In Laravel 5.2 and 5.3 **\Input** facade is not aliased by default. To do so, open `config/app.php` and add `'Input' => Illuminate\Support\Facades\Input::class,` to *aliases* array. - _pagination included_ ``` @@ -173,14 +178,16 @@ _pagination included_ @foreach ($users as $user) {{ $user->name }} @endforeach -{!! $users->appends(\Input::except('page'))->render() !!} +{!! $users->appends(\Request::except('page'))->render() !!} ``` +>**Note**: Blade's ability to recognize directives depends on having space before directive itself ` @sortablelink('Name')` + # One To One Relation sorting -## Define HasOne relation +## Define hasOne relation -In order to make relation sorting work, you have to define **hasOne()** relation in your model in question. +In order to make relation sorting work, you have to define **hasOne()** relation in your model. ``` /** @@ -188,13 +195,13 @@ In order to make relation sorting work, you have to define **hasOne()** relation */ public function detail() { - return $this->hasOne('App\UserDetail'); + return $this->hasOne(App\UserDetail::class); } ``` In *User* model we define **hasOne** relation to *UserDetail* model (which holds phone number and address details). -## Define `$sortable` array +## Define `$sortable` arrays Define `$sortable` array in both models (else, package uses `Scheme::hasColumn()` which is extra database query). @@ -202,7 +209,7 @@ Define `$sortable` array in both models (else, package uses `Scheme::hasColumn() for *User* ``` -protected $sortable = ['id', 'name', 'email', 'created_at', 'updated_at']; +public $sortable = ['id', 'name', 'email', 'created_at', 'updated_at']; ``` for *UserDetail* @@ -211,8 +218,6 @@ for *UserDetail* public $sortable = ['address', 'phone_number']; ``` ->note that `$sortable` array in *UserDetail* is declared as **public** and not protected because we need to access it inside *User* model. - ## Blade and relation sorting In order to tell package to sort using relation: @@ -220,7 +225,7 @@ In order to tell package to sort using relation: ``` @sortablelink ('detail.phone_number', 'phone') ``` ->package works with relation "name" that you define in model instead of table name. +>**Note**: package works with relation "name" (method) that you define in model instead of table name. In config file you can set your own separator if `.` (dot) is not what you want. @@ -249,4 +254,4 @@ try { } ``` ->I strongly recommend to catch **ColumnSortableException** because there is a user input in question (GET parameter) and any user can modify it in such way that package throws ColumnSortableException with code 0. +>**Note**: I strongly recommend to catch **ColumnSortableException** because there is a user input in question (GET parameter) and any user can modify it in such way that package throws ColumnSortableException with code 0. diff --git a/phpunit.xml b/phpunit.xml index 5d69e79..916a770 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,12 +7,10 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false" - syntaxCheck="false" -> + stopOnFailure="false"> - ./tests/ + ./tests diff --git a/src/ColumnSortable/ColumnSortableServiceProvider.php b/src/ColumnSortable/ColumnSortableServiceProvider.php index fbe3bc9..694557d 100755 --- a/src/ColumnSortable/ColumnSortableServiceProvider.php +++ b/src/ColumnSortable/ColumnSortableServiceProvider.php @@ -1,10 +1,16 @@ -publishes([$config_path => config_path('columnsortable.php')], 'columnsortable'); - $this->registerBladeExtensions(); + $this->publishes([ + __DIR__ . '/../config/columnsortable.php' => config_path('columnsortable.php') + ], 'config'); + + Blade::directive('sortablelink', function ($expression) { + $expression = ($expression[0] === '(') ? substr($expression, 1, -1) : $expression; + return ""; + }); } /** @@ -31,24 +42,6 @@ public function boot() */ public function register() { - $config_path = __DIR__ . '/../config/columnsortable.php'; - $this->mergeConfigFrom($config_path, 'columnsortable'); - } - - /** - * Register Blade extensions. - * - * @return void - */ - protected function registerBladeExtensions() - { - $blade = $this->app['view']->getEngineResolver()->resolve('blade')->getCompiler(); - - $blade->directive('sortablelink', function ($expression) { - if ($expression[0] === '(') { - return ""; - } - return ""; - }); + $this->mergeConfigFrom(__DIR__ . '/../config/columnsortable.php', 'columnsortable'); } } diff --git a/src/ColumnSortable/Sortable.php b/src/ColumnSortable/Sortable.php index 180db4d..718ea3a 100755 --- a/src/ColumnSortable/Sortable.php +++ b/src/ColumnSortable/Sortable.php @@ -15,135 +15,52 @@ */ trait Sortable { - /** - * @param array $parameters - * - * @return string - */ - public static function link(array $parameters) - { - if (count($parameters) === 1) { - $title = self::getOneToOneSortOrNull($parameters[0]); - $title = (is_null($title)) ? $parameters[0] : $title[1]; - } else { - $title = $parameters[1]; - } - - $sort = $sortOriginal = $parameters[0]; - unset($parameters); - - $formatting_function = Config::get('columnsortable.formatting_function', null); - - if (!is_null($formatting_function) && function_exists($formatting_function)) { - $title = call_user_func($formatting_function, $title); - } - - $icon = Config::get('columnsortable.default_icon_set'); - - if ($oneToOneSort = self::getOneToOneSortOrNull($sort)) { - $sort = $oneToOneSort[1]; - } - - foreach (Config::get('columnsortable.columns') as $key => $value) { - if (in_array($sort, $value['rows'])) { - $icon = $value['class']; - } - } - - if (Request::get('sort') == $sortOriginal && in_array(Request::get('order'), ['asc', 'desc'])) { - $asc_suffix = Config::get('columnsortable.asc_suffix', '-asc'); - $desc_suffix = Config::get('columnsortable.desc_suffix', '-desc'); - $icon = $icon . (Request::get('order') === 'asc' ? $asc_suffix : $desc_suffix); - $order = Request::get('order') === 'desc' ? 'asc' : 'desc'; - } else { - $icon = Config::get('columnsortable.sortable_icon'); - $order = Config::get('columnsortable.default_order_unsorted', 'asc'); - } - - $parameters = [ - 'sort' => $sortOriginal, - 'order' => $order, - ]; - - $queryString = http_build_query(array_merge(array_filter(Request::except('sort', 'order', 'page')), - $parameters)); - $anchorClass = Config::get('columnsortable.anchor_class', null); - if ($anchorClass !== null) { - $anchorClass = 'class="' . $anchorClass . '"'; - } - - $iconAndTextSeparator = Config::get('columnsortable.icon_text_separator', ''); - - $clickableIcon = Config::get('columnsortable.clickable_icon', false); - $trailingTag = $iconAndTextSeparator . '' . ''; - if ($clickableIcon === false) { - $trailingTag = '' . $iconAndTextSeparator . ''; - } - - return '' . htmlentities($title) . $trailingTag; - } - - /** - * @param $sort - * @return array|null - * @throws ColumnSortableException - */ - private static function getOneToOneSortOrNull($sort) - { - $separator = Config::get('columnsortable.uri_relation_column_separator', '.'); - if (str_contains($sort, $separator)) { - $oneToOneSort = explode($separator, $sort); - if (count($oneToOneSort) !== 2) { - throw new ColumnSortableException(); - } - return $oneToOneSort; - } - - return null; - } - /** * @param \Illuminate\Database\Query\Builder $query * @param array|null $default * * @return \Illuminate\Database\Query\Builder */ - public function scopeSortable($query, array $default = null) + public function scopeSortable($query, $default = null) { if (Request::has('sort') && Request::has('order')) { + return $this->queryOrderBuilder($query, Request::only(['sort', 'order'])); } elseif (!is_null($default)) { - $default_array = $this->formatDefaultArray($default); - if (Config::get('columnsortable.allow_request_modification', true) && !empty($default_array)) { - Request::merge($default_array); + $defaultSortArray = $this->getDefaultSortArray($default); + if (Config::get('columnsortable.allow_request_modification', true) && !empty($defaultSortArray)) { + Request::merge($defaultSortArray); } - return $this->queryOrderBuilder($query, $default_array); + + return $this->queryOrderBuilder($query, $defaultSortArray); } else { + return $query; } } /** * @param \Illuminate\Database\Query\Builder $query - * @param array $a + * @param array $sortArray * @return \Illuminate\Database\Query\Builder * @throws ColumnSortableException */ - private function queryOrderBuilder($query, array $a) + private function queryOrderBuilder($query, array $sortArray) { $model = $this; + //dd($model); + $direction = array_get($sortArray, 'order', 'asc'); - $order = array_get($a, 'order', 'asc'); - if (!in_array($order, ['asc', 'desc'])) { - $order = Config::get('columnsortable.default_order', 'asc'); + if (!in_array($direction, ['asc', 'desc'])) { + $direction = Config::get('columnsortable.default_order', 'asc'); } - $sort = array_get($a, 'sort', null); + $sort = array_get($sortArray, 'sort', null); + if (!is_null($sort)) { - if ($oneToOneSort = $this->getOneToOneSortOrNull($sort)) { + if ($oneToOneSort = SortableLink::getOneToOneSortOrNull($sort)) { $relationName = $oneToOneSort[0]; $sort = $oneToOneSort[1]; - try { $relation = $query->getRelation($relationName); $query = $this->queryJoinBuilder($query, $relation); @@ -157,7 +74,7 @@ private function queryOrderBuilder($query, array $a) } if ($this->columnExists($model, $sort)) { - return $query->orderBy($sort, $order); + return $query->orderBy($sort, $direction); } } @@ -199,26 +116,29 @@ private function columnExists($model, $column) } /** - * @param array $a + * @param array|string $sort * * @return array */ - private function formatDefaultArray(array $a) + private function getDefaultSortArray($sort) { - $order = Config::get('columnsortable.default_order', 'asc'); - reset($a); - - if ((bool)count(array_filter(array_keys($a), 'is_string'))) { - $sort = key($a); - $order = array_get($a, $sort, $order); - } else { - $sort = current($a); + if (empty($sort)) { + return []; } - if (!$sort) { - return []; + $configDefaultOrder = Config::get('columnsortable.default_order', 'asc'); + + if (is_string($sort)) { + return ['sort' => $sort, 'order' => $configDefaultOrder]; } - return ['sort' => $sort, 'order' => $order]; + reset($sort); + $each = each($sort); + + if ($each[0] === 0) { + return ['sort' => $each[1], 'order' => $configDefaultOrder]; + } else { + return ['sort' => $each[0], 'order' => $each[1]]; + } } } diff --git a/src/ColumnSortable/SortableLink.php b/src/ColumnSortable/SortableLink.php new file mode 100644 index 0000000..7752f5a --- /dev/null +++ b/src/ColumnSortable/SortableLink.php @@ -0,0 +1,105 @@ + $value) { + if (in_array($sort, $value['rows'])) { + $icon = $value['class']; + } + } + + if (Request::get('sort') == $sortOriginal && in_array(Request::get('order'), ['asc', 'desc'])) { + $asc_suffix = Config::get('columnsortable.asc_suffix', '-asc'); + $desc_suffix = Config::get('columnsortable.desc_suffix', '-desc'); + $icon = $icon . (Request::get('order') === 'asc' ? $asc_suffix : $desc_suffix); + $order = Request::get('order') === 'desc' ? 'asc' : 'desc'; + } else { + $icon = Config::get('columnsortable.sortable_icon'); + $order = Config::get('columnsortable.default_order_unsorted', 'asc'); + } + + $parameters = [ + 'sort' => $sortOriginal, + 'order' => $order, + ]; + + $queryString = http_build_query(array_merge(array_filter(Request::except('sort', 'order', 'page')), + $parameters)); + $anchorClass = Config::get('columnsortable.anchor_class', null); + if ($anchorClass !== null) { + $anchorClass = 'class="' . $anchorClass . '"'; + } + + $iconAndTextSeparator = Config::get('columnsortable.icon_text_separator', ''); + + $clickableIcon = Config::get('columnsortable.clickable_icon', false); + $trailingTag = $iconAndTextSeparator . '' . ''; + if ($clickableIcon === false) { + $trailingTag = '' . $iconAndTextSeparator . ''; + } + + return '' . htmlentities($title) . $trailingTag; + } + + /** + * @param $sort + * @return array|null + * @throws ColumnSortableException + */ + public static function getOneToOneSortOrNull($sort) + { + $separator = Config::get('columnsortable.uri_relation_column_separator', '.'); + + if (str_contains($sort, $separator)) { + $oneToOneSort = explode($separator, $sort); + if (count($oneToOneSort) !== 2) { + throw new ColumnSortableException(); + } + + return $oneToOneSort; + } + + return null; + } +} diff --git a/src/config/columnsortable.php b/src/config/columnsortable.php index 8a29637..12f694f 100755 --- a/src/config/columnsortable.php +++ b/src/config/columnsortable.php @@ -72,7 +72,7 @@ 'allow_request_modification' => true, /* - default order for: $user->sortable(['id']) usage + default order for: $user->sortable('id') usage */ 'default_order' => 'asc',