Top View


Author Naoya Miyagawa

CakePHPでCSV/TSVダウンロード機能を実装する🥮

2022/11/27

🌱 はじめに

こんにちは。自社プロダクト「360(さんろくまる)」を手掛けている技術開発第一部門の みやがわ です。

今日は、CakePHPでCSV/TSVダウンロード機能の実装方法についてご紹介します。

環境はCakePHP4ですが、CakePHP3でもほとんど同じ実装で動作しますのでご参考くださいませ。


🌱 環境

  • CakePHP: 4.4
  • satthi/csv-combine: 1.0.2

🌱 ダウンロードサンプル

CSV

Sample CSV

TSV

Sample TSV


🌱 実装方法

こちらからサンプルコードを確認できます。


Controller (エンドポイント)

エンドポイントとなるコントローラ側の実装です。

CSV/TSVファイルの作成は DemoExporter.php::exportCsv()/exportTsv() に委譲し、コントローラではダウンロードの形式のみ取り扱っています。

ファイルのダウンロードは response->withFile() を利用しています。(withDownloadでも可)
Laravelでいう response()→file() です。

ここで、Laravelの場合は file()->deleteFileAfterSend(true) という便利なメソッドが用意されており、ダウンロード後に不要になる一時ファイルを削除してくれます。
ただしCakePHPには同等のメソッドが無いので、代替案を後述します。


class DemosController extends AppController
{


    public function downloadCsv(): Response
    {
        $exporter = new DemoExporter();
        $tmpFilepath = $exporter->setProperties()->exportCsv();

        $now = FrozenTime::now()->i18nFormat('yyyyMMdd_HHmmss');
        $exportName = "{$now}_demo.csv";

        $response = $this->response->withFile($tmpFilepath, ['download' => true, 'name' => $exportName]);

        return $response;
    }

    public function downloadTsv(): Response
    {
        // ︙ csvと同じ
        $tmpFilepath = $exporter->setProperties()->exportTsv();

        // ︙ csvと同じ
        $exportName = "{$now}_demo.tsv";

        // ︙ csvと同じ
    }
}

DemoExporter(データ整形用)

ファイル内に書き込みたいデータを定義するためのファイルです。

今回はサンプルデータなのでデータはベタ書きしていますが、実際のアプリケーションでは、DBからデータを抽出した上で、配列形式に整形するように記述します。

見ての通りここにCSV/TSVに関するものは記載されていません。
ファイル操作に関わるところは、親クラスの AbstractCsvExporter に実装しています。


class DemoExporter extends AbstractCsvExporter
{
    private array $articles;

    public function setProperties(): self
    {
        // add more
        $this->articles = [
            [1, 'How to download CSV in CakePHP', 1, '2022-01-01 12:00'],
            [2, 'How to download TSV in CakePHP', 2, '2022-01-02 08:00'],
        ];

        return $this;
    }

    protected function getHeaders(): array
    {
        return [
            'ID',
            'Title',
            'Status',
            'Created'
        ];
    }

    protected function getDataList(): array
    {
        $dataList = [];

        foreach ($this->articles as $article) {
            // process data if needed
            $dataList[] = $article;
        }

        return $dataList;
    }
}

AbstractCsvExporter (ファイル出力用)

ファイル操作を行うファイルです。ここで CsvExport というクラスを利用しています。
これは弊社のエンジニアの中で最もCakePHPに強い萩原氏が作成した、PHPで利用できるCSV/TSVファイル出力用プラグイン satthi/csv-combine のクラスです。

ポイントとなる点は、CsvExport で生成される一時ファイルをこの抽象クラスで定義したデストラクタ内で削除している点です。

CSV出力は色々なデータテーブルがある画面に要件として追加されやすい機能ではありますが、開発者はDemoExporterのようなデータ定義用クラスを増やし、いかにデータを抽出して整形するかにのみ集中すればよくなる構成となっています。


abstract class AbstractCsvExporter
{
    const EXPORT_DIR = TMP;

    private array $defaultExportConfig = [
        'export_encoding' => 'UTF-8',
    ];

    private CsvExport $exporter;

    private string $tmpFilePath;

    public function __construct(array $exportConfig = [])
    {
        $this->exporter = new CsvExport();
        $this->tmpFilePath = self::EXPORT_DIR . FrozenTime::now()->i18nFormat('yyyyMMdd_HHmmss') . '_' . rand(0, 10000);
        $this->defaultExportConfig = array_merge($this->defaultExportConfig, $exportConfig);
    }

    public function __destruct()
    {
        $this->purgeTmpFile();
    }

    abstract protected function getHeaders(): array;

    abstract protected function getDataList(): array;

    public function exportCsv(): string
    {
        $this->exporter->make($this->makeExportData(), $this->tmpFilePath, $this->defaultExportConfig);

        return $this->tmpFilePath;
    }

    public function exportTsv(): string
    {
        $exportConfig = array_merge($this->defaultExportConfig, ['delimiter' => "\t"]);
        $this->exporter->make($this->makeExportData(), $this->tmpFilePath, $exportConfig);

        return $this->tmpFilePath;
    }

    private function purgeTmpFile(): void
    {
        unlink($this->tmpFilePath);
    }

    private function makeExportData(): array
    {
        return array_merge([$this->getHeaders()], $this->getDataList());
    }
}

🌱 View

最後に画面にダウンロード用のボタンを置いたら完成です。

<?= $this->Html->link(
    'Download',
    ['prefix' => 'File', 'controller' => 'Demos', 'action' => 'downloadCsv'],
    ['class' => 'button']
) ?>

<?= $this->Html->link(
    'Download',
    ['prefix' => 'File', 'controller' => 'Demos', 'action' => 'downloadTsv'],
    ['class' => 'button']
) ?>

Demo Page


🌱 おわりに

親クラスに共通処理をまとめ、ダウンロード対象のファイルの追加時に最小工数となるようにしてみました。

一時ファイルの削除に関しては、cronを用いて定期的に掃除をするような方法もありますが、PHPの機能を利用してもっとシンプルに出来るこの方法は一つアリだと思います。

なにか参考になればうれしいです!


🌱 参考


Naoya Miyagawa

Naoya Miyagawa

Twitter X

Hi🙋🏻‍♂️ Web Developer in Fukuoka, Japan🇯🇵 ❄️ Fusic Co., Ltd.|360 (さんろくまる) ❄️ PHP: CakePHP ❄️ JS: Vue.js ❄️ AWS: SAP ❄️ FAV: nm7/🍣/SG🇸🇬