Laravel Excel使ってみた

#概要
今もっともメジャーと思われるLaravelのExcelプラグイン。
早さだけなら「fast-excel」もあるが情報がないので。

#インストール
phpで以下の拡張モジュールが必要なのであらかじめ使えるようにしておく。
php_zip php_xml php_gd2
composerでLaravelプロジェクトにプラグイン追加

```sh
$ composer require maatwebsite/excel
```
laravel 6.9の環境ではlaravel-excel 3.1.19がインストールされた。

次に設定を施していく。まずはlaravelのソースに追加。

```diff:[config/app.php]
'providers' => [
    /*
     * Package Service Providers...
     */
+    Maatwebsite\Excel\ExcelServiceProvider::class,
]
(略)
'aliases' => [
(略)
  'Event' => Illuminate\Support\Facades\Event::class,
+  'Excel' => Maatwebsite\Excel\Facades\Excel::class,
  'File' => Illuminate\Support\Facades\File::class,
  'Gate' => Illuminate\Support\Facades\Gate::class,
  'Hash' => Illuminate\Support\Facades\Hash::class,
(略)
```
次に以下のコマンドを叩いて専用の設定ファイルを生成する。

```
$ php artisan vendor:publish --provider="Maatwebsite\Excel\ExcelServiceProvider"
```
設定ファイル laravel/config/excel.php が作られます。
デフォルトでPDFでのエクスポート機能が有効になってました。

##サンプル
###その１　xlsxテンプレートをもとにxlsx出力
データを埋め込み、セルの書式設定、罫線設定を行って出力する場合

```php:[メインクラス]
<?php

namespace App\Sample;

use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\BeforeExport;
use Maatwebsite\Excel\Events\BeforeWriting;
use Maatwebsite\Excel\Excel;
use Maatwebsite\Excel\Files\LocalTemporaryFile;

// xlsxテンプレートのセル書式編集を行って出力するサンプルクラス
class ReportInputOutputSample implements WithEvents
{
  use Exportable;

  private $template_file = null;

  /**
   * @param string $template_file
   * @return $this
   */
  public function setTemplate(string $template_file)
  {
    if (file_exists($template_file)) {
      $this->template_file = $template_file;
    }
    return $this;
  }

  /**
   * @return array
   */
  public function registerEvents(): array
  {
    return [
      // ファイル生成直前イベントハンドラ
      BeforeExport::class => function (BeforeExport $event) {
        // テンプレート読み込み
        if (is_null($this->template_file)) {
          return;
        }
        $event->writer->reopen(new LocalTemporaryFile($this->template_file), Excel::XLSX);
        return;
      },
      // 書き込み直前イベントハンドラ
      BeforeWriting::class => function (BeforeWriting $event) {
        // テンプレート読み込みでついてくる余計な空シートを削除する
        $event->writer->removeSheetByIndex(1);

        // セルを赤色で塗りつぶし
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B5')->getStyle('B5')->getFill()->setFillType('solid')->getStartColor()->setARGB('FFFFFF00');
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B6')->getStyle('B6')->getFill()->setFillType('solid')->getStartColor()->setARGB('FFFF0000');

        // セル結合
        $event->writer->getSheetByIndex(0)->getDelegate()->mergeCells('B10:C11');

        // セル結合解除
        $event->writer->getSheetByIndex(0)->getDelegate()->unmergeCells('B13:C13');

        // 上罫線
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B16')->getStyle('B16')->getBorders()->getTop()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);

        // 下罫線
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B18')->getStyle('B18')->getBorders()->getBottom()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);

        // 左罫線
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B20')->getStyle('B20')->getBorders()->getLeft()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);

        // 右罫線
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B22')->getStyle('B22')->getBorders()->getRight()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);

        // 外枠
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B24')->getStyle('B24')->getBorders()->getOutline()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);

        // 太線
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B26')->getStyle('B26')->getBorders()->getOutline()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THICK);

        // 二重線
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B28')->getStyle('B28')->getBorders()->getOutline()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_DOUBLE);

        // 点線
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B30')->getStyle('B30')->getBorders()->getOutline()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_DOTTED);

        // 斜め右上がり罫線
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B32')->getStyle('B32')->getBorders()->setDiagonalDirection(1)->getDiagonal()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);

        // 斜め右下がり罫線
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B34')->getStyle('B34')->getBorders()->setDiagonalDirection(2)->getDiagonal()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);

        // 文字サイズ
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B5')->getStyle('B5')->getFont()->setSize(16);

        // 太字
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B6')->getStyle('B6')->getFont()->setBold(true);

        // 斜体
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B7')->getStyle('B7')->getFont()->setItalic(true);

        // 下線
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B8')->getStyle('B8')->getFont()->setUnderline(true);

        // 書体
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B9')->getStyle('B9')->getFont()->setName('HGSｺﾞｼｯｸE');

        // 色
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B10')->getStyle('B10')->getFont()->getColor()->setARGB('FFFF0000');

        // 一括設定
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B11')->getStyle('B11')->applyFromArray([
          'font' => [
            'size' => '16',
            'bold' => true,
            'italic' => true,
            'underline' => true,
            'name' => 'ヒラギノ丸ゴ Pro',
            'color' => ['argb' => 'FFFF0000']
          ]
        ]);

        // 右寄せ
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B12')->getStyle('B12')->getAlignment()->setHorizontal('right');

        // 中央寄せ
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B13')->getStyle('B13')->getAlignment()->setHorizontal('center');

        // ライブラリクラスでセルに値設定
        $event->writer->getSheetByIndex(0)->getDelegate()->getCell('B6')->setValue('これはプログラムで設定した値です。');

        // 独自ワークシートクラス
        $esh = new ExtendWorksheets($event->writer->getSheetByIndex(0)->getDelegate());

        // 独自ワークシートクラスでセルに値設定
        $esh->setValue('B9', 'これはプログラムで設定した値です。');

        // 独自ワークシートクラスでセルに値設定
        $esh->replaceValue('B12', [['target' => '{置換対象1}', 'value' => '!!プログラムで置換1!!'], ['target' => '{置換対象2}', 'value' => '!!プログラムで置換2!!']]);

        // セルの値を取得して別セルに設定することでコピー
        $cellValue = $esh->getValue('B15');
        $esh->setValue('C15', $cellValue);

        //csv用データを設範囲設定
        $data = [
          ['名前', '生年月日'],
          ['氏名1', '1999年1月1日'],
          ['氏名2', '1999年2月1日'],
          ['氏名3', '1999年3月1日'],
          ['氏名4', '1999年4月1日'],
          ['氏名5', '1999年5月1日'],
        ];
        $esh->fromArray($data, null, 'B18');



        return;
      },
    ];
  }
}
```

```php:[セル値設定カスタムクラス]
<?php

namespace App\Sample;

use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\RichText\RichText as RichText;

class ExtendWorksheets
{
  private $worksheets = null;

  // コンストラクタを追加
  public function __construct(Worksheet $worksheets)
  {
    $this->worksheets = $worksheets;
    return $this;
  }

  /**
   * セルの値を取得
   *
   * @return cell value
   */
  public function getValue($cell_label)
  {
    return $this->worksheets->getCell($cell_label)->getValue();
  }

  /**
   * セルへ値を上書き設定
   * (書式はテンプレートを継承する)
   *
   * @return $this
   */
  public function setValue($cell_label, $value)
  {
    $this->worksheets->getCell($cell_label)->setValue($value);
    return $this;
  }

  /**
   * セルへ書式指定付きで値を上書き設定
   *
   * @return $this
   */
  public function setValueWithFormat($cell_label, $values_ary)
  {
    $RichText = new RichText();
    foreach ($values_ary as $key => $value) {
      $specialFont = $RichText->createTextRun($value['value'])->getFont();
      if (array_key_exists('size', $value)) {
        $specialFont->setSize($value['size']);
      }
      if (array_key_exists('style', $value)) {
        switch ($value['size']) {
        case 'bold':
          $specialFont->setBold(true);
          break;
        case 'italic':
          $specialFont->setItalic(true);
          break;
        }
      }
      if (array_key_exists('family', $value)) {
        $specialFont->setName($value['family']);
      }
    }
    $this->worksheets->getCell($cell_label)->setValue($RichText);
    return $this;
  }

  /**
   * セルへ値を指定キーワードを置換して設定
   * (書式はテンプレートを継承する)
   *
   * @return $this
   */
  public function replaceValue($cell_label, $values_ary)
  {
    $setValue = self::getValue($cell_label);
    foreach ($values_ary as $key => $value) {
      $setValue = str_replace($value['target'], $value['value'], $setValue);
    }
    self::setValue($cell_label, $setValue);

    return $this;
  }

  /**
   * セル結合
   * @return $this
   */
  public function mergeCells($range_label)
  {
    $this->worksheets->mergeCells($range_label);
    return $this;
  }

  /**
   * セル結合解除
   * @return $this
   */
  public function unmergeCells($range_label)
  {
    $this->worksheets->unmergeCells($range_label);
    return $this;
  }

  /**
   * 複数セルへ値を範囲上書き設定
   * (書式はテンプレートを継承する)
   *
   * @return $this
   */
  public function fromArray(array $source, $nullValue = null, $startCell = 'A1', $strictNullComparison = false)
  {
    $this->worksheets->fromArray($source, $nullValue, $startCell, $strictNullComparison);
    return $this;
  }
}

```

```php:[呼出し]
        return (new ReportInputOutputSample)->setTemplate(resource_path('sample/template4.xlsx'))->download('laravel-excelサンプル４.xlsx');
```
###その２　csv出力サンプル
データを設定してcsvで出力。

```php
<?php

namespace App\Sample;

use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\BeforeExport;
use Maatwebsite\Excel\Events\BeforeWriting;
use Maatwebsite\Excel\Excel;
use Maatwebsite\Excel\Files\LocalTemporaryFile;

// CSV出力サンプルクラス
class CsvOutputSmaple implements WithEvents
{
  use Exportable;

  /**
   * @return array
   */
  public function registerEvents(): array
  {
    return [
      // 書き込み直前イベントハンドラ
      BeforeWriting::class => function (BeforeWriting $event) {
        //csv用データを設範囲設定
        $data = [
          ['名前', '生年月日'],
          ['氏名1', '1999年1月1日'],
          ['氏名2', '1999年2月1日'],
          ['氏名3', '1999年3月1日'],
          ['氏名4', '1999年4月1日'],
          ['氏名5', '1999年5月1日'],
        ];
        $event->writer->getSheetByIndex(0)->getDelegate()->fromArray($data, null, 'A1');

        return;
      },
    ];
  }
}

```

```php:[呼出し]
   return (new CsvOutputSmaple)->download('laravel-excelサンプル５.csv');
```

###その３　csvインポートサンプル
文字コードの認識がおかしい？ワイド文字が入ると区切りがおかしくなるので、今のところCSVインポートはLaravel-Excel以外の方法で検討したほうがよさそう。

```php
<?php

namespace App\Sample;

use Maatwebsite\Excel\Concerns\ToModel;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Concerns\Importable;
use App\Sample\ReportInputOutputSample006ImportModel;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\ToCollection;

// CSVインポートのサンプルクラス
class ImportSample implements ToCollection
{
  use Importable;

  // public function model(array $row)
  // {
  //   return new ReportInputOutputSample006ImportModel([
  //     'name' => $row[0],
  //     'kana' => $row[1],
  //     'sex' => $row[2],

  //   ]);
  // }

  public function collection(Collection $rows)
  {
    foreach ($rows as $row) {
      foreach ($row as $c) {
        Log::debug(mb_detect_encoding($c, 'Shift-JIS,UTF-8,EUC-JP'));
        Log::debug(mb_convert_encoding($c, 'Shift-JIS'));
      }
    }
  }
}
```

```php:[呼出し]
        (new ImportSample)->import(resource_path('sample/target6.csv'), null, \Maatwebsite\Excel\Excel::CSV);
```

###その４　xlsxインポートサンプル
その３のcsvインポートと同じ。呼出しで入力ファイルパスと種別を変えればいい。

```php:[呼出し]
        (new ImportSample)->import(resource_path('sample/target7.xlsx'), null, \Maatwebsite\Excel\Excel::XLSX);
```

###その５　pdfエクスポートサンプル
呼出しで出力ファイルの拡張子をpdfに変えればいい。ただしワイド文字が文字化けする。この対応方法が不明。
```php:[呼出し]
        return (new ReportInputOutputSample)->setTemplate(resource_path('sample/template4.xlsx'))->download('laravel-excelサンプル４.pdf');
```


##フロント側のダウンロード処理
帳票を作成してダウンロードさせるコード
「/api/sample/report-io」のAPIで上記のLaravel-Excelを使ったコントローラー処理を呼び出す想定。

```vue

  /**
   * サンプル処理
   */
  doSample(mode: number) {
    const _this = this;

    // ローディング状態に変更
    const loading = this.$loading({
      lock: true,
      text: "実行中...",
      background: SystemSetting.waitingBackgroundColor
    });

    axios({
      url: "/api/sample/report-io",
      params: { mode: mode },
      method: "GET",
      responseType: "blob" // これがないと文字化けする
    })
      .then(res => {
        this.download(res);
      })
      .catch(error => {
        var errorMsg = "不明";

        // 通知
        _this.$message({
          message: "実行に失敗しました。[" + errorMsg + "]",
          type: "error",
          duration: 0,
          showClose: true
        });
      })
      .finally(() => {
        // ローディング状態終了
        loading.close();
      });
  }

  /**
   * ファイルダウンロード
   */
  download(res) {
    const blob = new Blob([res.data], {
      type: res.data.type
    });

    //レスポンスヘッダからファイル名を取得します
    const contentDisposition = res.headers["content-disposition"];
    const fileName = this.getFileName(contentDisposition);

    //ダウンロードします
    saveAs(blob, fileName);
  }

  /**
   * responseヘッダーからファイル名取得
   */
  getFileName(contentDisposition) {
    let fileName = contentDisposition.substring(
      contentDisposition.indexOf("''") + 2,
      contentDisposition.length
    );
    //デコードするとスペースが"+"になるのでスペースへ置換します
    fileName = decodeURI(fileName).replace(/\+/g, " ");

    return fileName;
  }
```