efureev/laravel-support

View on GitHub
src/Sorting/Model/Sortable.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

namespace Php\Support\Laravel\Sorting\Model;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Expression;

/**
 * Trait Sortable
 *
 * Use it in the Eloquent Model class to add sorting to it
 *
 * @method Sortable sortingPositionGreaterThen(int $value, bool $andSelf = true)
 * @method Sortable sortingPositionLessThen(int $value, bool $andSelf = true)
 * @method Sortable sortingPositionOrderByDesc()
 * @method Sortable sortingPositionOrderByAsc()
 *
 * @mixin Model
 * @mixin Builder
 */
trait Sortable
{
    /**
     * Call it in boot method of your Eloquent model
     *
     * @return void
     */
    protected static function bootSortable(): void
    {
        static::saving(
            static function (Model $model) {
                $model->onSavingSortingPosition();
            }
        );

        static::saved(
            static function (self $model) {
                $model->onSavedSortingPosition();
            }
        );
        /*
            static::addGlobalScope(new SortOrderingDesc);
            // OR
            static::addGlobalScope(
                static::getSortingScopeName(),
                fn(Builder $builder) => static::sortingOrderingFn($builder)
            );
        */
    }

    protected static function sortingOrderingFn(Builder $builder): Builder
    {
        if ($direction = static::sortingOrderingDirection()) {
            $builder->orderBy(static::getSortingColumnName(), $direction);
        }

        return $builder;
    }

    protected static function sortingOrderingDirection(): ?string
    {
        return 'desc';
    }

    public static function getSortingScopeName(): string
    {
        return 'sortingPosition';
    }

    public static function getSortingColumnName(): string
    {
        return static::$sortingColumnName ?? 'sorting_position';
    }

    public function setSortingPosition(int $value): self
    {
        if ($value <= 0) {
            $value = new Expression($this->sqlForMaxQuery());
        }
        $this->attributes[static::getSortingColumnName()] = $value;

        return $this;
    }

    private function normalizeSortingPosition(): self
    {
        $position = $this->{static::getSortingColumnName()} ?? 0;
        if ($position instanceof Expression) {
            return $this;
        }

        return $this->setSortingPosition($position);
    }

    public function sortingPosition(): int
    {
        return $this->{static::getSortingColumnName()} ?? 0;
    }

    public function setFirstForSortingPosition(): self
    {
        return $this->setSortingPosition(1);
    }

    public function onSavingSortingPosition()
    {
        $this->normalizeSortingPosition();
        $this->reorderingSortingPosition();
    }

    public function onSavedSortingPosition()
    {
        $this->refreshSortingPosition();
    }

    public function refreshSortingPosition()
    {
        if ($this->{static::getSortingColumnName()} instanceof Expression) {
            $this->{static::getSortingColumnName()} = $this->setKeysForSelectQuery(
                $this->newQueryWithoutScopes()
            )
                ->firstOrFail([static::getSortingColumnName()])
                ->{static::getSortingColumnName()};
        }
    }

    protected function sqlForMaxQuery(): string
    {
        $where = [];
        if ($this->exists) {
            $id     = $this->getKey();
            $idName = $this->getKeyName();
            switch ($this->keyType) {
                case 'int':
                case 'integer':
                    break;
                default:
                    $id = "'$id'";
            }

            $where[] = "($idName <> $id)";
        }

        if ($defaultSortingRestrictions = $this->getDefaultSortingRestrictionsSql()) {
            $where[] = $defaultSortingRestrictions;
        }
        if ($where) {
            $where = 'WHERE ' . implode(' AND ', $where);
        } else {
            $where = '';
        }
        $sortingColumnName = static::getSortingColumnName();
        return <<<SQL
(
    WITH max_s_p AS (select MAX({$sortingColumnName}) as m FROM {$this->getTable()} {$where})

    SELECT CASE
        WHEN m IS NOT NULL THEN m + 1 ELSE 1 END as v
    FROM max_s_p
    )
SQL;
    }

    private function reorderingSortingPosition(): void
    {
        if (($position = $this->{static::getSortingColumnName()}) instanceof Expression) {
            return;
        }

        if ($position > 0) {
            $column = static::getSortingColumnName();
            $new    = $this->sortingPosition();
            $old    = $this->getRawOriginal($column);

            if ($old === null) {
                $this->incrementInReorder($new, $new);
            } else {
                if ($new > $old) {
                    $this->decrementInReorder($new, $old);
                } else {
                    $this->incrementInReorder($new, $old);
                }
            }
        }
    }

    private function incrementInReorder($new, $old): void
    {
        $column = static::getSortingColumnName();
        $query  = $this->forSortingRestrictions($this->newQuery())
            ->where($column, '>=', $new);

        if ($this->exists) {
            $query->where($column, '<', $old);
        }

        $query->increment($column);
    }

    private function decrementInReorder($new, $old): void
    {
        $column = static::getSortingColumnName();
        $query  = $this->forSortingRestrictions($this->newQuery())
            ->where($column, '<=', $new);

        if ($this->exists) {
            $query->where($column, '>', $old);
        }

        $query->decrement($column);
    }

    protected function getDefaultSortingRestrictionsSql(): string
    {
        return '';
    }

    public function scopeSortingPositionGreaterThen(Builder $query, int $value, bool $andSelf = true): Builder
    {
        return $query->where(static::getSortingColumnName(), $andSelf ? '>=' : '>', $value);
    }

    public function scopeSortingPositionLessThen(Builder $query, int $value, bool $andSelf = true): Builder
    {
        return $query->where(static::getSortingColumnName(), $andSelf ? '<=' : '<', $value);
    }

    public function scopeSortingPositionOrderByDesc(Builder $query): Builder
    {
        return $query->orderByDesc(static::getSortingColumnName());
    }

    public function scopeSortingPositionOrderByAsc(Builder $query): Builder
    {
        return $query->orderBy(static::getSortingColumnName());
    }

    protected function forSortingRestrictions(Builder $query): Builder
    {
        return $query;
    }
}