This repository has been archived by the owner on Jun 9, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
392 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
{ | ||
"name": "zbiller/laravel-sort", | ||
"description": "Sort Eloquent model records by their attributes or relationships", | ||
"license": "MIT", | ||
"keywords": [ | ||
"laravel", | ||
"sort", | ||
"order", | ||
"eloquent", | ||
"model", | ||
"relations" | ||
], | ||
"minimum-stability": "dev", | ||
"authors": [ | ||
{ | ||
"name": "Andrei Badea", | ||
"email": "zbiller@gmail.com", | ||
"role": "Developer" | ||
} | ||
], | ||
"require": { | ||
"php": "^7.1.3", | ||
"illuminate/contracts": "~5.7.0", | ||
"illuminate/support": "~5.7.0", | ||
"illuminate/database": "~5.7.0" | ||
}, | ||
"require-dev": { | ||
"orchestra/testbench": "~3.7.0", | ||
"phpunit/phpunit": "~7.0" | ||
}, | ||
"autoload": { | ||
"psr-4": { | ||
"Zbiller\\Sort\\": "src/" | ||
} | ||
}, | ||
"autoload-dev": { | ||
"psr-4": { | ||
"Zbiller\\Sort\\Tests\\": "tests/" | ||
}, | ||
"classmap": [ | ||
"tests/TestCase.php" | ||
] | ||
}, | ||
"scripts": { | ||
"test": "phpunit" | ||
}, | ||
"config": { | ||
"sort-packages": true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<?php | ||
|
||
namespace Zbiller\Sort\Exceptions; | ||
|
||
use Zbiller\Sort\Objects\Sort; | ||
|
||
class SortException extends \Exception | ||
{ | ||
/** | ||
* The exception to be thrown when an invalid direction is supplied as the argument. | ||
* | ||
* @param string $direction | ||
* @return static | ||
*/ | ||
public static function invalidDirectionSupplied($direction) | ||
{ | ||
return new static( | ||
'Invalid sorting direction.' . PHP_EOL . | ||
'You provided the direction: "' . $direction . '".' . PHP_EOL . | ||
'Please provide one of these directions: ' . implode('|', Sort::$directions) . '.' | ||
); | ||
} | ||
|
||
/** | ||
* The exception to be thrown when trying to sort by an invalid relation type. | ||
* | ||
* @param string$relation | ||
* @param string $type | ||
* @return static | ||
* @internal param string $direction | ||
*/ | ||
public static function wrongRelationToSort($relation, $type) | ||
{ | ||
return new static( | ||
'You can only sort records by the following relations: HasOne, BelongsTo.' . PHP_EOL . | ||
'The relation "' . $relation . '" is of type ' . $type . ' and cannot be sorted by.' | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<?php | ||
|
||
namespace Zbiller\Sort\Objects; | ||
|
||
abstract class Sort | ||
{ | ||
/** | ||
* In case you apply the sorted scope on a model without an Zbiller\Sort\Objects\Sort instance as it's parameter. | ||
* This constant will act as the default sort field. | ||
* Meaning that the IsSortable trait will look for this request name when deciding what to sort by. | ||
* | ||
* @const | ||
*/ | ||
const DEFAULT_SORT_FIELD = 'sort'; | ||
|
||
/** | ||
* In case you apply the sorted scope on a model without an Zbiller\Sort\Objects\Sort instance as it's parameter. | ||
* This constant will act as the default sorting direction field. | ||
* Meaning that the IsSortable trait will look for this request name when deciding the sorting direction. | ||
* | ||
* @const | ||
*/ | ||
const DEFAULT_DIRECTION_FIELD = 'direction'; | ||
|
||
/** | ||
* The sorting directions available. | ||
* | ||
* @const | ||
*/ | ||
const DIRECTION_ASC = 'asc'; | ||
const DIRECTION_DESC = 'desc'; | ||
const DIRECTION_RANDOM = 'random'; | ||
|
||
/** | ||
* List of valid sorting directions. | ||
* | ||
* @var array | ||
*/ | ||
public static $directions = [ | ||
self::DIRECTION_ASC, | ||
self::DIRECTION_DESC, | ||
self::DIRECTION_RANDOM, | ||
]; | ||
|
||
/** | ||
* Get the request field name to sort by. | ||
* | ||
* @return string | ||
*/ | ||
abstract public function field(); | ||
|
||
/** | ||
* Get the direction to sort by. | ||
* | ||
* @return string | ||
*/ | ||
abstract public function direction(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
<?php | ||
|
||
namespace Zbiller\Sort\Traits; | ||
|
||
use Illuminate\Database\Eloquent\Builder; | ||
use Illuminate\Database\Eloquent\Model; | ||
use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||
use Illuminate\Database\Eloquent\Relations\HasOne; | ||
use Zbiller\Sort\Exceptions\SortException; | ||
use Zbiller\Sort\Objects\Sort; | ||
|
||
trait IsSortable | ||
{ | ||
/** | ||
* @var array | ||
*/ | ||
protected $sort = [ | ||
/** | ||
* The query builder instance from the Sorted scope. | ||
* | ||
* @var Builder | ||
*/ | ||
'query' => null, | ||
|
||
/** | ||
* The data applying the "sorted" scope on a model. | ||
* | ||
* @var array | ||
*/ | ||
'data' => null, | ||
|
||
/** | ||
* The Zbiller\Sort\Objects\Sort instance. | ||
* This is used to get the sorting rules, just like a request. | ||
* | ||
* @var Sort | ||
*/ | ||
'instance' => null, | ||
|
||
/** | ||
* The field to sort by. | ||
* | ||
* @var string | ||
*/ | ||
'field' => Sort::DEFAULT_SORT_FIELD, | ||
|
||
/** | ||
* The direction to sort in. | ||
* | ||
* @var string | ||
*/ | ||
'direction' => Sort::DEFAULT_DIRECTION_FIELD, | ||
]; | ||
|
||
/** | ||
* The filter scope. | ||
* Should be called on the model when building the query. | ||
* | ||
* @param Builder $query | ||
* @param array $data | ||
* @param Sort $sort | ||
*/ | ||
public function scopeSorted($query, array $data, Sort $sort = null) | ||
{ | ||
$this->sort['query'] = $query; | ||
$this->sort['data'] = $data; | ||
$this->sort['instance'] = $sort; | ||
|
||
$this->setFieldToSortBy(); | ||
$this->setDirectionToSortIn(); | ||
|
||
if ($this->isValidSort()) { | ||
$this->checkSortingDirection(); | ||
|
||
switch ($this->sort['data'][$this->sort['direction']]) { | ||
case Sort::DIRECTION_RANDOM: | ||
$this->sort['query']->inRandomOrder(); | ||
break; | ||
default: | ||
if ($this->shouldSortByRelation()) { | ||
$this->sortByRelation(); | ||
} else { | ||
$this->sortNormally(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Verify if all sorting conditions are met. | ||
* | ||
* @return bool | ||
*/ | ||
protected function isValidSort() | ||
{ | ||
return | ||
isset($this->sort['data'][$this->sort['field']]) && | ||
isset($this->sort['data'][$this->sort['direction']]); | ||
} | ||
|
||
/** | ||
* Set the sort field if an Zbiller\Sort\Objects\Sort instance has been provided as a parameter for the sorted scope. | ||
* | ||
* @return void | ||
*/ | ||
protected function setFieldToSortBy() | ||
{ | ||
if ($this->sort['instance'] instanceof Sort) { | ||
$this->sort['field'] = $this->sort['instance']->field(); | ||
} | ||
} | ||
|
||
/** | ||
* Set the sort direction if an Zbiller\Sort\Objects\Sort instance has been provided as a parameter for the sorted scope. | ||
* | ||
* @return void | ||
*/ | ||
protected function setDirectionToSortIn() | ||
{ | ||
if ($this->sort['instance'] instanceof Sort) { | ||
$this->sort['direction'] = $this->sort['instance']->direction(); | ||
} | ||
} | ||
|
||
/** | ||
* Sort model records using columns from the model's table itself. | ||
* | ||
* @return void | ||
*/ | ||
protected function sortNormally() | ||
{ | ||
$this->sort['query']->orderBy( | ||
$this->sort['data'][$this->sort['field']], | ||
$this->sort['data'][$this->sort['direction']] | ||
); | ||
} | ||
|
||
/** | ||
* Sort model records using columns from the model relation's table. | ||
* | ||
* @return void | ||
*/ | ||
protected function sortByRelation() | ||
{ | ||
$parts = explode('.', $this->sort['data'][$this->sort['field']]); | ||
$models = []; | ||
|
||
if (count($parts) > 2) { | ||
$field = array_pop($parts); | ||
$relations = $parts; | ||
} else { | ||
$field = array_last($parts); | ||
$relations = (array)array_first($parts); | ||
} | ||
|
||
foreach ($relations as $index => $relation) { | ||
$previousModel = $this; | ||
|
||
if (isset($models[$index - 1])) { | ||
$previousModel = $models[$index - 1]; | ||
} | ||
|
||
$this->checkRelationToSortBy($previousModel, $relation); | ||
|
||
$models[] = $previousModel->{$relation}()->getModel(); | ||
|
||
$modelTable = $previousModel->getTable(); | ||
$relationTable = $previousModel->{$relation}()->getModel()->getTable(); | ||
$foreignKey = $previousModel->{$relation}() instanceof HasOne ? | ||
$previousModel->{$relation}()->getForeignKeyName() : | ||
$previousModel->{$relation}()->getForeignKey(); | ||
|
||
if (!$this->alreadyJoinedForSorting($relationTable)) { | ||
switch (get_class($previousModel->{$relation}())) { | ||
case BelongsTo::class: | ||
$this->sort['query']->join($relationTable, $modelTable . '.' . $foreignKey, '=', $relationTable . '.id'); | ||
break; | ||
case HasOne::class: | ||
$this->sort['query']->join($relationTable, $modelTable . '.id', '=', $relationTable . '.' . $foreignKey); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
$alias = implode('_', $relations) . '_' . $field; | ||
|
||
if (isset($relationTable)) { | ||
$this->sort['query']->addSelect([ | ||
$this->getTable() . '.*', | ||
$relationTable . '.' . $field . ' AS ' . $alias | ||
]); | ||
} | ||
|
||
$this->sort['query']->orderBy( | ||
$alias, $this->sort['data'][$this->sort['direction']] | ||
); | ||
} | ||
|
||
/** | ||
* @return bool | ||
*/ | ||
protected function shouldSortByRelation() | ||
{ | ||
return str_contains($this->sort['data'][$this->sort['field']], '.'); | ||
} | ||
|
||
/** | ||
* Verify if the desired join exists already, possibly included by a global scope. | ||
* | ||
* @param string $table | ||
* | ||
* @return bool | ||
*/ | ||
protected function alreadyJoinedForSorting($table) | ||
{ | ||
return str_contains(strtolower($this->sort['query']->toSql()), 'join `' . $table . '`'); | ||
} | ||
|
||
/** | ||
* Verify if the direction provided matches one of the directions from: | ||
* Zbiller\Sort\Objects\Sort::$directions. | ||
* | ||
* @return void | ||
*/ | ||
protected function checkSortingDirection() | ||
{ | ||
if (!in_array(strtolower($this->sort['data'][$this->sort['direction']]), array_map('strtolower', Sort::$directions))) { | ||
throw SortException::invalidDirectionSupplied($this->sort['data'][$this->sort['direction']]); | ||
} | ||
} | ||
|
||
/** | ||
* Verify if the desired relation to sort by is one of: HasOne or BelongsTo. | ||
* Sorting by "many" relations or "morph" ones is not possible. | ||
* | ||
* @param Model $model | ||
* @param string $relation | ||
*/ | ||
protected function checkRelationToSortBy(Model $model, $relation) | ||
{ | ||
if (!($model->{$relation}() instanceof HasOne) && !($model->{$relation}() instanceof BelongsTo)) { | ||
throw SortException::wrongRelationToSort($relation, get_class($model->{$relation}())); | ||
} | ||
} | ||
} |