こんにちは。
フレームワーク機能として実装してほしい。
只野です。
検索系画面を作っているとこんな要件よく出ませんか?
“検索結果を任意の項目でソートしたい。”
これはよくある要件です。
だがしかし!
Web系のフレームワークだと検索結果のソート機能がないものが多々あります!
私が最近利用している「Laravel 11」と「Livewire」もソート機能は標準でありませんでした。なぜなのだ。
フレームワーク標準で実装してくれてよい気がする機能ですが、何か問題でもあるんですかね?
調べてみるとプラグインではありそうでしたが、ここはせっかくなので、独自で実装してみようかと思いました。
実装構成としては、色々な画面で利用したいので共通化する必要があります。
「Livewire」コンポートネントでの共通化は「trait」が使えます。
「blade」テンプレートでの共通化はテンプレートを部品化する仕組みが使えます。
こんな感じで実装するファイルはこれら。
- {ルートディレクトリ}\app\Traits\SortTrait.php
 - {ルートディレクトリ}\resources\views\components\a-sort.blade.php
 
2ファイルで収まるのはよい感じ。
1:実装!
Livewire用Traitの実装
- {ルートディレクトリ}\app\Traits\SortTrait.php
 
namespace App\Traits;
/**
 * ソート
 * 検索結果のソートメソッドを提供します。
 */
trait SortTrait
{
    // ソート項目
    public $sortColumn = '';
  // 現在ソート順序
    public $orderBy = [];
    // 基礎ソート順序
    public $baseOrderBy = [];
    /**
     * ソート項目取得処理
     */
    private function getOrderBy() 
    {
        $base = $this->baseOrderBy;
        
        if (empty($base)) 
        {
            return $base;
        }
        // ソート項目判定
        $orderBy = [];
        if ($this->sortColumn != '') 
        {
            // ソート項目の場合、ソート順序を先頭に変更
            $orderBy[$this->sortColumn] = isset($this->orderBy) ? $this->orderBy[$this->sortColumn] : $base[$this->sortColumn];
            unset($base[$this->sortColumn]);
        }
        foreach ($base as $key => $value) 
        {
            $orderBy[$key] = empty($this->orderBy) ? $base[$key] : $this->orderBy[$key];
        }
        $this->orderBy = $orderBy;
        return $this->orderBy;
    }
    /**
     * ソート項目変更処理
     * 
     * @param $key ソート項目名
     */
    public function changeSort($key) 
    {
        $this->sortColumn = $key;
        if (is_array($this->orderBy[$key])) 
        {
            $order = $this->orderBy[$key]['order'];
            $this->orderBy[$key]['order'] = !$order;
        } else 
        {
            $this->orderBy[$key] = !$this->orderBy[$key];
        }
    }
    /**
     * ソート項目を追加します。
     * 
     * @param $key ソートキー
     * @param $columns ソートカラムリスト
     * @param $order ソート方向(初期値=昇順)
     */
    private function addOrderBy($key, $columns, $order = true) 
    {
        if (!is_array($columns)) 
        {
            $columns = (array)$columns;
        }
        $this->baseOrderBy[$key] = [
            'columns' => $columns,
            'order' => $order
        ];
    }
    /**
     * ソート項目設定処理
     * 
     * @param $q クエリー
     */
    private function setOrderBy($q) 
    {
        foreach ($this->getOrderBy() as $key => $value) 
        {
            if (is_array($value)) 
            {                
                $order = $value['order'];
                foreach ($value['columns'] as $childValue) 
                {
                    $this->queryOrder($q, $childValue, $order);
                }
            }
            else 
            {
                $this->queryOrder($q, $key, $value);
            }
        }
    }
    /**
     * クエリーに並び順を追加します。
     * 
     * @param $q クエリー
     * @param $column カラム名
     * @param $order 並び方向(true = 昇順, false = 降順)
     */
    private function queryOrder($q, $column, $order) 
    {
        if ($order) 
        {
            $q->orderBy($column);
        } else 
        {
            $q->orderByDesc($column);
        }
    }
    /**
     * ソートクリア処理
     */
    private function resetOrderBy() 
    {
        $this->sortColumn = '';
        $this->orderBy = $this->baseOrderBy;
    }
}
blade用コンポーネントの実装
- {ルートディレクトリ}\resources\views\components\a-sort.blade.php
 
@props(['orderBy', 'column'])
@php
$order = true;
if (is_array($orderBy[$column])) 
{
    $order = $orderBy[$column]['order'];
}
else 
{
    $order = $orderBy[$column];
}
@endphp
<a href="#" wire:click="changeSort('{{ $column }}')" class="flex items-center text-gray-600 hover:text-gray-800">
    <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor">
        @if ($order)
        <path d="M10 3l-4 4h8l-4-4z" />
        @else
        <path d="M10 17l-4-4h8l-4 4z" />
        @endif
    </svg>
    <span>{{ $slot }}</span>
</a>
2:使い方!
Livewireの実装
<?php
namespace App\Livewire\User;
use Livewire\WithPagination;
use App\Models\User;
// 1:ここから
use App\Traits\SortTrait;
// 1:ここまで
/**
 * ユーザー検索コンポーネント(Livewire)
 */
class SearchUser extends Component
{
// 2:ここから
    // ソート共通部品
    use SortTrait;
// 2:ここまで
    //ページネーション
    use WithPagination;
    // ユーザーID
    public $userId = '';
    // ユーザー名
    public $userName = '';
    /**
     * 検索します。
     */
    public function search() 
    {
        $this->resetPage();
// 3:ここから
        $this->resetOrderBy();
// 3:ここまで
    }
    /**
     * 初期化処理を行います。
     */
    public function mount() 
// 4:ここから
        // ソートデフォルト設定
        $this->addOrderBy('order_1', 'users.id');
        $this->addOrderBy('order_2', 'users.name');
// 4:ここまで
    }
    /**
     * ビューを返却します。
     * 
     * @return view
     */
    public function render()
    {
        $q = User::query();
        $q->select(
            'users.id',
            'users.user_id',
            'users.name');
        if ($this->userId != '') {
            $q->where('users.user_id', '=' , $this->userId);
        }
        if ($this->userName != '') {
            $q->where('users.name', 'like' , '%'.$this->userName.'%');
        }
// 5:ここから
        $this->setOrderBy($q);
// 5:ここまで
        return view('livewire.user.search', [
            'users' => $q->paginate(config('app.number_searches.user')),
        ]);
    }
}
まず下記で「trait」を有効にします。
// 1:ここから
use App\Traits\SortTrait;
// 1:ここまで
// 2:ここから
    // ソート共通部品
    use SortTrait;
// 2:ここまで
画面の検索処理を実行した際にソート順序を初期値に戻す場合、下記メソッドを実行します。
// 3:ここから
        $this->resetOrderBy();
// 3:ここまで
並び順の初期値は「Livewire」の初期化処理に、検索SQLで利用したい並び順を設定します。
この例だと、「users」テーブルの「id」、「name」項目を並び順で指定しています。
ソート順序の順番で項目を追加していきます。
// 4:ここから
        // ソートデフォルト設定
        $this->addOrderBy('order_1', 'users.id');
        $this->addOrderBy('order_2', 'users.name');
// 4:ここまで
※メソッド引数ルール
addOrderBy($key, $columns, $order = true) 
$key = ソート項目管理キー(画面内で一意の名称であれば何でもよい)
$columns = SQLで並び順をしていする項目名(ネイティブSQL文での項目指定)
$order = 初期順序方向(true=昇順、false=降順)
ちなみに一つのソート項目管理キーで複数の並び順を指定したい場合は下記で可能としています。
        $this->addOrderBy('order_1', 'users.id');
        $this->addOrderBy('order_2', ['users.name', 'users.furigana']);
※この例だと「order_2」は1キーで「name」、「furigana」と複合で並び順を管理します。
さらにキーとソート項目名を同じ名称で管理する方式の指定できます。
        $this->baseOrderBy = [
            'users.id' => true,
            'users.name' => true
        ];
※設定書式='{SQL項目名}' => 順序方向(true=昇順、false=降順)
 この指定方法は複合ソートには対応していません。
 単一指定はぱっと見ソート項目が分かりやすい以外のメリットはありません。
検索SQLを並び順を反映するには下記のメソッドを検索用クエリーの最後に実行します。
この辺りはクエリビルダを拡張とかできればもっと簡単に指定できそうな気もします。今後の課題ですね。(遠い目)
// 5:ここから
        $this->setOrderBy($q);
// 5:ここまで
※引数にはlaraveのクエリビルダーで取得したクエリーを設定します。
baldeテンプレートの実装
<div>
    <form wire:submit="search">
        <div>
            <label>
                ユーザーID
            </label>
            <div>
                <input type="text" model="id">
            </div>
        </div>
        <div>
            <label>
                ユーザー名
            </label>
            <div>
                <input type="text" model="name">
            </div>
        </div>
        <button type="submit">検索</button>
    </form>
    <div class="">
        <table>
            <thead>
                <tr>
                    <th>
<!-- 1:ここから -->
                        <x-a-sort :orderBy="$orderBy" column="order_1">
                            ユーザーID
                        </x-a-sort>
<!-- 1:ここまで -->
                    </th>
                    <th>
<!-- 2:ここから -->
                        <x-a-sortt :orderBy="$orderBy" column="order_2">
                            ユーザー名
                        </x-a-sort>
<!-- 2:ここまで -->
                    </th>
                </tr>
            </thead>
            <tbody>
                @foreach ($users as $index => $user)
                <tr>
                    <td>{{$user->user_id}}</td>
                    <td>{{$user->name}}</td>
                </tr>
                @endforeach
            </tbody>
        </table>
        {{ $users->links() }}
    </div>
</div>
この例では検索結果は「table」で表示しています。
指定方法は「table」の場合はヘッダ項目に共通化した「a-sort.blade.php」コンポーネントを指定して、必要なプロパティを設定します。
<!-- 1:ここから -->
                        <x-a-sort :orderBy="$orderBy" column="order_1">
                            ユーザーID
                        </x-a-sort>
<!-- 1:ここまで -->
※引数の「:orderBy="$orderBy"」は全て共通
 「column」に「Livewire」の初期化で定義したソート項目管理キーを指定します。
「blade」のコンポーネントの指定は少し特殊でコンポーネント名の先頭に必ず「x-」が必要となります。今回作成したコンポーネント名は「a-sort.blade.php」ですが、利用時は「x-a-sort」のように指定します。
画面での利用はソート対応の項目名をクリックするだけです。
今のところ順調にソート処理が動いていますが、まだまだ改善点は色々ありそうなので、引き続き「Laravel 11」と「Livewire」の理解を深めて改善していきたいところです。
本日はこの辺で
	


