325 lines
8.2 KiB
PHP
Executable File
325 lines
8.2 KiB
PHP
Executable File
<?php namespace Common\Database;
|
|
|
|
use Cache;
|
|
use Carbon\Carbon;
|
|
use Closure;
|
|
use Illuminate\Container\Container;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Str;
|
|
|
|
class Paginator
|
|
{
|
|
/**
|
|
* @var Builder
|
|
*/
|
|
private $query;
|
|
|
|
/**
|
|
* @var Model
|
|
*/
|
|
private $model;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
private $defaultOrderColumn = 'updated_at';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
private $defaultOrderDirection = 'desc';
|
|
|
|
/**
|
|
* @var Closure
|
|
*/
|
|
public $secondaryOrderCallback;
|
|
|
|
/**
|
|
* @var int
|
|
*/
|
|
public $defaultPerPage = 15;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
public $searchColumn = 'name';
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
public $filterColumns = [];
|
|
|
|
/**
|
|
* @var Closure
|
|
*/
|
|
public $searchCallback;
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
private $params;
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
public $dontSort;
|
|
|
|
public function __construct($model, array $params)
|
|
{
|
|
$this->model = $model;
|
|
$this->params = $this->toCamelCase($params);
|
|
$this->query = $model->newQuery();
|
|
}
|
|
|
|
public function paginate(): LengthAwarePaginator
|
|
{
|
|
$with = array_filter(explode(',', $this->param('with', '')));
|
|
$withCount = array_filter(explode(',', $this->param('withCount', '')));
|
|
$searchTerm = $this->param('query');
|
|
$order = $this->getOrder();
|
|
$perPage = $this->param('perPage', $this->defaultPerPage);
|
|
$page = (int) $this->param('page', 1);
|
|
|
|
// load specified relations and counts
|
|
if (!empty($with)) {
|
|
$this->query->with($with);
|
|
}
|
|
if (!empty($withCount)) {
|
|
$this->query->withCount($withCount);
|
|
}
|
|
|
|
// search
|
|
if ($searchTerm) {
|
|
if ($this->searchCallback) {
|
|
call_user_func(
|
|
$this->searchCallback,
|
|
$this->query,
|
|
$searchTerm,
|
|
);
|
|
} else {
|
|
$this->query->where(
|
|
$this->searchColumn,
|
|
'like',
|
|
"$searchTerm%",
|
|
);
|
|
}
|
|
}
|
|
|
|
$this->applyFilters();
|
|
|
|
// order
|
|
if (!$this->dontSort) {
|
|
if ($this->isRawOrder($order['col'])) {
|
|
$this->query->orderByRaw($order['col']);
|
|
} else {
|
|
$this->query->orderBy($order['col'], $order['dir']);
|
|
}
|
|
|
|
if ($this->secondaryOrderCallback) {
|
|
call_user_func(
|
|
$this->secondaryOrderCallback,
|
|
$this->query,
|
|
$order['col'],
|
|
$order['dir'],
|
|
);
|
|
}
|
|
}
|
|
|
|
$countCacheKeyQuery = $this->query
|
|
->toBase()
|
|
->cloneWithout(['columns', 'orders', 'limit', 'offset']);
|
|
$countCacheKye = base64_encode(
|
|
Str::replaceArray(
|
|
'?',
|
|
$countCacheKeyQuery->getBindings(),
|
|
$countCacheKeyQuery->toSql(),
|
|
),
|
|
);
|
|
$total = null;
|
|
if ($countCacheKye) {
|
|
$total = Cache::get($countCacheKye);
|
|
}
|
|
if (is_null($total)) {
|
|
$total = $this->query->toBase()->getCountForPagination();
|
|
if ($total > 500000) {
|
|
Cache::put($countCacheKye, $total, Carbon::now()->addDay());
|
|
}
|
|
}
|
|
|
|
$items = $total
|
|
? $this->query->forPage($page, $perPage)->get()
|
|
: new Collection();
|
|
|
|
return Container::getInstance()->makeWith(LengthAwarePaginator::class, [
|
|
'items' => $items,
|
|
'total' => $total,
|
|
'perPage' => $perPage,
|
|
'page' => $page,
|
|
]);
|
|
}
|
|
|
|
public function param(string $name, $default = null)
|
|
{
|
|
return Arr::get($this->params, Str::camel($name)) ?: $default;
|
|
}
|
|
|
|
public function setParam(string $name, $value): self
|
|
{
|
|
$this->params[$name] = $value;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return Builder
|
|
*/
|
|
public function query()
|
|
{
|
|
return $this->query;
|
|
}
|
|
|
|
/**
|
|
* Load specified relation counts with paginator items.
|
|
*
|
|
* @param mixed $relations
|
|
* @return $this
|
|
*/
|
|
public function withCount($relations)
|
|
{
|
|
$this->query->withCount($relations);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Load specified relations of paginated items.
|
|
*
|
|
* @param mixed $relations
|
|
* @return $this
|
|
*/
|
|
public function with($relations)
|
|
{
|
|
$this->query->with($relations);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param $column
|
|
* @param null $operator
|
|
* @param null $value
|
|
* @param string $boolean
|
|
* @return $this
|
|
*/
|
|
public function where(
|
|
$column,
|
|
$operator = null,
|
|
$value = null,
|
|
$boolean = 'and'
|
|
) {
|
|
$this->query->where($column, $operator, $value, $boolean);
|
|
return $this;
|
|
}
|
|
|
|
public function setDefaultOrderColumns($column, $direction = 'desc'): self
|
|
{
|
|
$this->defaultOrderColumn = $column;
|
|
$this->defaultOrderDirection = $direction;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function getOrder()
|
|
{
|
|
// order provided as single string: "column|direction"
|
|
if ($specifiedOrder = $this->param('order')) {
|
|
$parts = preg_split('(\||:)', $specifiedOrder);
|
|
$orderCol = Arr::get($parts, 0, $this->defaultOrderColumn);
|
|
$orderDir = Arr::get($parts, 1, $this->defaultOrderDirection);
|
|
} else {
|
|
$orderCol = $this->param('orderBy', $this->defaultOrderColumn);
|
|
$orderDir = $this->param('orderDir', $this->defaultOrderDirection);
|
|
}
|
|
|
|
return [
|
|
'dir' => Str::snake($orderDir),
|
|
'col' => !$this->isRawOrder($orderCol)
|
|
? Str::snake($orderCol)
|
|
: $orderCol,
|
|
];
|
|
}
|
|
|
|
private function toCamelCase($params)
|
|
{
|
|
return collect($params)
|
|
->keyBy(function ($value, $key) {
|
|
return Str::camel($key);
|
|
})
|
|
->toArray();
|
|
}
|
|
|
|
private function applyFilters()
|
|
{
|
|
foreach ($this->filterColumns as $column => $callback) {
|
|
$column = is_int($column) ? $callback : $column;
|
|
$column = Str::camel($column);
|
|
if (isset($this->params[$column])) {
|
|
$value = $this->params[$column];
|
|
$column = Str::snake($column);
|
|
|
|
// user specified callback
|
|
if (is_callable($callback)) {
|
|
$callback($this->query, $value);
|
|
|
|
// boolean filter
|
|
} elseif ($value === 'false' || $value === 'true') {
|
|
$this->applyBooleanFilter($column, $value);
|
|
|
|
// filter by between date
|
|
} elseif (
|
|
\Str::contains($column, '_at') &&
|
|
\Str::contains($value, ':')
|
|
) {
|
|
$this->query()->whereBetween($column, explode(':', $value));
|
|
|
|
// filter by specified column value
|
|
} else {
|
|
$this->query()->where($column, $value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $column
|
|
* @param string $value
|
|
*/
|
|
private function applyBooleanFilter($column, $value)
|
|
{
|
|
// cast "true" or "false" to boolean
|
|
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
|
$casts = $this->model->getCasts();
|
|
|
|
// column is a simple boolean type
|
|
if (Arr::get($casts, $column) === 'boolean') {
|
|
$this->query()->where($column, $value);
|
|
|
|
// column has actual value, test whether it's null or not by default
|
|
} else {
|
|
if ($value) {
|
|
$this->query()->whereNotNull($column);
|
|
} else {
|
|
$this->query()->whereNull($column);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function isRawOrder(string $order): bool
|
|
{
|
|
return Str::contains($order, ['(', ')']);
|
|
}
|
|
}
|