クリスマスや年越しの話題が次々に賑わう師走の今日この頃。こんな季節は気持ちまでつい走ってしまいそうですが、開発作業はミスなく、着実にこなしていきたいものです。

Hackerの皆様はいかかお過ごしでおられますでしょうか。

概要

本連載は、Laravel+Filamentというフレームワーク&ライブラリの組み合わせを通じて、インストールから入りひとまず簡単に 開発環境で管理画面開発を短時間で始められることを目的にしています。


過去にNext.jsのCMS「Strapi」の記事も書いてありますのでよろしければご参考になさってください。


⇩第1回の記事は以下からご覧いただけます。

参考記事:

モルドスプーンアイコン

⇩第2回の記事は以下からご覧いただけます。

参考記事:

モルドスプーンアイコン

今回はCSVダウンロード機能をFilament上で作成し、利用できるようになるまでを解説します。

それではどうぞ最後までお付き合いください。

本連載のゴール

Laravel+Filamentを使い、DockerでMySQLデータベースを立て、ローカルで開発環境が利用できる&CSVダウンロード機能を利用できるようになるまで整える。

対象読者

前回のおさらい

第2回の記事では、Posts(ブログのポスト) のリソースを作成し、最低限表示・編集・削除の基本動作ができる機能を作成したと思います。

filament301.png

などをもし把握されていない場合は、お手数ですが第1回・第2回をご覧ください。

それでは始めていきます。

作業の推移

バルク処理について

この画面の左上に「Bulk Actions」というボタンがついているのがわかると思いますが、このボタンから1件以上のレコードをまとめて処理できます。

前回の処理で、以下のようにPostResource.phpに一斉削除機能はすでに実装しています。

app/Filament/Resources/PostResource.php
<?php

namespace App\Filament\Resources;

// 略

class PostResource extends Resource
{
    // 前略

    public static function table(Table $table): Table
    {
        return $table
            ..->bulkActions([
                // 一斉削除など、まとめてアクションを行える
                Tables\Actions\BulkActionGroup::make([
                    Tables\Actions\DeleteBulkAction::make(),
                ]),
            ]);
    }

    // 後略
}

DeleteBulkAction というのが、一斉削除ボタンを表示する部分です。これが書かれていると、

ページの左上に一斉削除ボタンが表示されます。

消したいレコードを入れて、削除ボタンを押すと...

filament302.png

確認ダイアログが出て

filament303.png

実際に削除が行われ「Deleted」がティッカー表示されます。

filament304.png

このような一斉処理=バルク機能 をCSVダウンロードでも使っていきます。一から実装するとこの機能は案外面倒くさい ので、助かりますね...!

実装方針を調査

公式ドキュメントのactionsのページによると すでに事前に作られているバルク機能は

などあるようですが、CSVダウンロードはまだありません。よって、自分で作らねばなりません

一括削除機能のソースコードvendor/filament/table/src/Actions/DeleteBulkAction.phpを参考にしたところ、メソッドをチェーンのようにしてUIを作っていく形のようです。

しかし、同じようにBulkActionを継承したカスタムクラスを作る例を探したところすぐには出てこない...

そこでGoogleでも検索したところ、2023/12現在最新ではないものの、version2のFilamentの公式ドキュメントで 似たような処理をしているページを見つけました。やはりBulkActionを使う形で、しかもPostResource.phpに実装する形で実現できるようなので、よりかんたんです。

Tables\Actions\BulkAction::make('アクション名')
// 前提を書く
->action(function(Collection $records) {
  // ここで$recordsを処理して、結果をレスポンスとして返す。})

action()をフィルター的に使えるようです。これはわかりやすい。こちらを大いに参考にさせていただき、CSVダウンロード処理を作ります。

具体的な実装内容

結果的に以下のようにPostResource.phpの実装を行いました。上に書いたように、BulkAction::make('アクション名') からメソッドチェーン にする形で 必要な定義を追加していきます。

app/Filament/Resources/PostResource.php
<?php

namespace App\Filament\Resources;

use App\Models\Post;
// 略、変更なし
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use Symfony\Component\HttpFoundation\StreamedResponse;

class PostResource extends Resource
{
    protected static ?string $model = Post::class;

    // この部分は変更なし、省略
    
    public static function table(Table $table): Table
    {
        return $table
            // ここも一切変更なし、省略。            ->bulkActions([
                // 一括削除ボタンのUIを作る=BulkActionGroup
                Tables\Actions\BulkActionGroup::make([
                    Tables\Actions\DeleteBulkAction::make(),
                    Tables\Actions\BulkAction::make('csvDownload')
                        ->icon('heroicon-s-arrow-down-on-square-stack') // s...はsolid download部分が、heroicons.comで調べて出た部分
                        ->color('success')
                        ->requiresConfirmation()
                        ->modalHeading('CSVダウンロード')
                        ->modalDescription('CSVをダウンロードします。よろしいですか?')
                        ->modalSubmitActionLabel('はい')
                        ->modalCancelActionLabel('いいえ')
                        ->action(function (Collection $posts) {

                            // ヘッダー行
                            $csvHeader = ['id', 'title', 'body', 'type', 'published'];

                            // 不要なキーを削除して$csvDataにつめる
                            $csvData = $posts->map(function ($post) {
                                return [
                                    'id' => $post->id,
                                    'title' => $post->title,
                                    'body' => $post->body,
                                    'type' => $post->type,
                                    'published' => $post->published,
                                ];
                            })->toArray();

                            // CSV出力処理
                            $response = new StreamedResponse(function () use ($csvHeader, $csvData) {
                                $handle = fopen('php://output', 'w');
                                fputcsv($handle, $csvHeader); // ヘッダー行を出力
                                foreach ($csvData as $row) {
                                    fputcsv($handle, $row);
                                }
                                fclose($handle);
                            }, 200, [
                                'Content-Type' => 'text/csv',
                                'Content-Disposition' => 'attachment; filename="posts.csv"',
                            ]);
                            return $response;
                        })
                        ->deselectRecordsAfterCompletion() // CSVダウンロード後、すべてのチェックをリセット
                ]),
            ]);
    }
    // 後略、この部分は変更なし
}

ポイントをかいつまんで説明します。

ダウンロードボタンへのアイコンの指定

Filamentには、heroiconが組み込まれています。ダウンロードボタンのLavelは->color('success')で、アイコンは下記の部分で指定しています。

->icon('heroicon-s-arrow-down-on-square-stack') // s...はsolid download部分が、heroicons.comで調べて出た部分

sの部分は「solid」を意味するのは分かったのですが、そのあとの部分は、なんか使えるアイコンと使えないアイコンがあるような...? この辺りはもう少し深掘りしないとわからないですが、使えるものを使ってみました。

モーダルのUIを描く

下記の部分で確認モーダルのUI を出力しています。私がググって出てきたソースはfilament v2の古い書き方だったようで、その点はv3のものに合わせて書く必要があると思います。

->requiresConfirmation()
->modalHeading('CSVダウンロード')
->modalDescription('CSVをダウンロードします。よろしいですか?')
->modalSubmitActionLabel('はい')
->modalCancelActionLabel('いいえ')

CSVダウンロードを実際に実行

下記の部分で実際にCSVダウンロードを行っています。Collectionではすべてのキーが取れてしまうので、必要なものだけにカットして出力するようにしましょう。

 ->action(function (Collection $posts) {

    // ヘッダー行
    $csvHeader = ['id', 'title', 'body', 'type', 'published'];

    // 不要なキーを削除して$csvDataにつめる
    $csvData = $posts->map(function ($post) {
        return [
            'id' => $post->id,
            'title' => $post->title,
            'body' => $post->body,
            'type' => $post->type,
            'published' => $post->published,
        ];
    })->toArray();

    // CSV出力処理
    $response = new StreamedResponse(function () use ($csvHeader, $csvData) {
        $handle = fopen('php://output', 'w');
        fputcsv($handle, $csvHeader); // ヘッダー行を出力
        foreach ($csvData as $row) {
            fputcsv($handle, $row);
        }
        fclose($handle);
    }, 200, [
        'Content-Type' => 'text/csv',
        'Content-Disposition' => 'attachment; filename="posts.csv"',
    ]);
    return $response;
})
->deselectRecordsAfterCompletion() // CSVダウンロード後、すべてのチェックをリセット

ところで、fputcsv()の利用はダブルクオーテーション付きでCSVを出力するにあたり問題がありそうで、商用のサービスには使わない方がいいかもしれません。


この点は本題から逸れるため深掘りせず、参考URLを貼らせていただくのみとしたいと思います。

参考資料: 【PHP】csvを自前で作った話 | ダブルコーテーション問題でfputcsvは使わず実装 参考資料: PHPで高速・省メモリ・確実に日本語CSVを扱う方法

動作確認

実装が終わったので、実行してみるとこのようになります。

サクッとダウンロードできている

サクッとダウンロードできている

filament306.png

ダウンロードしたCSVをLibreOffice で開いたところ想定通り出力できています。ただし、上記ソースコードでは文字コードをUTF8 で出力しているので、Microsoft Excelで開けるようにしたい場合は、SJISへの変換処理を入れてみてください。

参考URL: LibreOffice

まとめ

無事CSV一括ダウンロード処理を実装できました。

ただ、各ResourceクラスがこのままではActionが増えていき、ファット(ソースコードの量が多いファイル)になってしまいそう なので どこかでカスタムクラスを作成する方法を調べ、具体的な実装方法もわかれば本記事に追記していきたいと思います。どうぞ、よろしくお願いいたします。


連載記事を最後までお読みいただきありがとうございました!

追記: 新しい連載を書き始めました。

ご興味がございましたら下記もお読みいただけますと嬉しいです😀 本連載と同じく、Laravelに関する記事となります。

参考記事:

モルドスプーンアイコン

引き続き当ブログをよろしくお願いいたします。