CakePHP 4 で PhpSpreadsheet のダウンロード機能をコンポーネントに実装

はじめに

一昨日の投稿「CakePHP 4 で PhpSpreadsheet を使う方法」では、PhpSpreadsheet を CakePHP 4 で使う方法をご紹介しました。

今日はその中でも少しお話した、ダウンロード機能をコンポーネントに実装する方法をご紹介します。

今回はWindows で試していますが、Mac や Linux などでも同様のコマンドで動くと思います。その場合はパス区切り文字を「\」から「/」に変更してください。

CakePHP
4.0.3
PhpSpreadsheet
1.10.1
PHP
7.3.11
OS
Win 10 Home
目次
  1. Bake でコンポーネントのベースを作成
  2. コンポーネントにダウンロード機能を実装
  3. おわりに

1. Bake でコンポーネントのベースを作成

今日は Bake を使ってコンポーネントのベースを作成してみましょう。下記コマンドを実行します。

> cd path\to\cake
> bin\cake bake component Excel --no-test

Creating file C:\path\to\cake4\src\Controller\Component\ExcelComponent.php
Wrote `C:\path\to\cake4\src\Controller\Component\ExcelComponent.php`

「--no-test」オプションは、ユニットテスト用のファイルを作らないオプションです。このオプションをつけないと、テスト用の下記ファイルが自動生成されます。

tests\TestCase\Controller\Component\ExcelComponentTest.php

bake が完了すると、下記内容で ExcelComponent.php が生成されます。

src/Controller/Component/ExcelComponent.php
<?php
declare(strict_types=1);

namespace App\Controller\Component;

use Cake\Controller\Component;
use Cake\Controller\ComponentRegistry;

/**
 * Excel component
 */
class ExcelComponent extends Component
{
  /**
   * Default configuration.
   *
   * @var array
   */
  protected $_defaultConfig = [];
}
上記コードは便宜上、インデントサイズを2個に変換しています。

この ExcelComponent にダウンロード用の関数を追加します。

2. コンポーネントにダウンロード機能を実装

今回は下記のように実装しました。

src/Controller/Component/ExcelComponent.php
<php
...
use Cake\Controller\ComponentRegistry;
// ▼この4行を追加
use Cake\Http\CallbackStream;
use Cake\Http\Response;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
...
class ExcelComponent extends Component
{
  ...
  // ▼この関数を追加
  /**
   * エクセルファイルダウンロード
   *
   * @param PhpOffice\PhpSpreadsheet\Spreadsheet スプレッドシート
   * @param string $filename ファイル名
   * @return Cake\Http\Response
   */
  public function download(Spreadsheet $spreadsheet, string $filename)
  {
    $writer = new Xlsx($spreadsheet);
    $stream = new CallbackStream(function () use ($writer) {
      $writer->save('php://output');
    });
    $response = new Response();
    $encodedName = rawurlencode("{$filename}.xlsx");
    return $response->withType('xlsx')
      ->withHeader('Content-Disposition', "attachment;filename*=UTF-8''{$encodedName}")
      ->withBody($stream);
  }
}

$response の箇所は、Component 内では $this->request が無いため new Response() にしています。

また、ファイル名に日本語が含まれることも考慮し withHeader() の箇所も変更しました。

コントローラからは、こんな感じで呼び出します。

src/Controller/SampleController.php
<?php
declare(strict_types=1);

namespace App\Controller;

use PhpOffice\PhpSpreadsheet\Spreadsheet;

class SampleController extends AppController
{
  public function index()
  {
    $spreadsheet = new Spreadsheet();
    $sheet = $spreadsheet->getActiveSheet();
    $sheet->setCellValue('A1', 'Hello World !');

    // xlsx ファイルをダウンロード
    $this->loadComponent('Excel');
    $filename = 'サンプル_' . date('ymd_His');
    return $this->Excel->download($spreadsheet, $filename);
  }
}

3. おわりに

もし複数の箇所で PhpSpreadsheet のダウンロード機能を使う場合には、コンポーネントなどに分割するのがオススメです。このように分割しておけば、開発効率と保守性が向上します。

この機能に限らず、コードを書いていて同じような処理を繰り返し実装しているなー、と感じたら、関数化できないか検討すると良いと思います。