今回は前回紹介した画像挿入を応用して顔写真付き名簿を作成してみましょう。

顔写真付き名簿を作る

例えば、以下のように行ごとにA列に顔写真とB列に名前を出力したいとします。

今回はNameListExportというクラスで実装していきますので予め以下のコマンドで生成して下さい。

php artisan make:export NameListExport

また、エクセルに出力する画像データはpublic/img配下にあるとします。

データに紐付いた画像の挿入

前回の画像挿入と異なり、今回はB列に出力する名前に応じてA列の顔写真が決まります。つまり、名前情報と顔写真(正確にはその画像ファイルパス)が紐付いています。実際の状況ではそれらはDBなどから取得するかと思いますが、今回はcollectionで$membersという変数を用意します。

まず、NameListExportを以下のように実装しました。$membersはコンストラクタにて外部から渡し、プロパティにセットしています。collection()内で取得すれば良いのでは?と思うかもしれませんが、そうするとdrawings()内で参照することができません。なぜなら、drawings()collection()より先に処理されるからです。(詳しくは vendor/maatwebsite/excel/src/Sheet.php のexport()をご確認下さい。)

<?php

namespace App\Exports;

use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithDrawings;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use \PhpOffice\PhpSpreadsheet\Worksheet\Drawing;

class NameListExport implements FromCollection, WithHeadings, WithDrawings, WithMapping
{
    use Exportable;

    protected $members;

    public function __construct($members)
    {
        $this->members = $members;
    }

    public function headings():array
    {
        return [
            '顔写真', '名前',
        ];
    }

    public function map($member):array
    {
        return ['', $member['name']];   // A列は画像を出力するので空文字にしておく
    }

    public function collection()
    {
        return $this->members;
    }

    public function drawings()
    {
        $drawings = [];

        $rowNum = 2;    // 1行目はヘッダ行なので画像出力は2行目から

        foreach ($this->members as $member) {
            $drawing = new Drawing;
            $drawing->setPath($member['file']);
            $drawing->setCoordinates('A'.$rowNum);
            $drawing->setWidthAndHeight(60, 60);   // 画像サイズ調整
            $drawings[] = $drawing;

            $rowNum++;
        }
        
        return $drawings;
    }
}

それではtinkerで出力してみましょう。

$members = collect([
    ['name' => 'ミケ', 'file' => public_path('img/mike.jpg')],
    ['name' => '小虎', 'file' => public_path('img/kotora.jpg')],
    ['name' => 'おこげ', 'file' => public_path('img/okoge.jpg')],
    ['name' => 'くろ', 'file' => public_path('img/kuro.jpg')],
]);
use App\Exports\NameListExport;
(new NameListExport($members))->store('name_list.xlsx');

出力結果が以下になりました。(セルの幅と高さを調整してます)

あれ、名前の行がズレてしまいました。

どうやら$drawing->setCoordinates()で2〜5行目に画像をセットしたことで、データを出力する際の開始行がズレてしまったようです。

この件についてGithub上でDiscussionされていましたが、Laravel Excelの開発側としてはそこはPhpSpreadsheet側のロジックなのでどうにもできないとのこと。。。。

AfterSheetイベントで画像挿入

最終的に力技ですが、Laravel Excelのdrawings()は使用せず、AfterSheetイベント(シート作成プロセスの最後に呼ばれるイベント)にて画像をセットするようにしました。これならデータ→画像の順に挿入されるのでデータの開始行がズレません。

NameListExportを以下のように編集しました。WithDrawingの代わりにWithEventsをimplementしています。drawings()は中身はそのままですがgetDrawings()に変更し、registerEvents()内から呼び出すようにしました。そして、取得した$drawingsをAfterSheet内でWorkSheetにセットしています。

<?php

namespace App\Exports;

use Maatwebsite\Excel\Events\AfterSheet;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\FromCollection;
use \PhpOffice\PhpSpreadsheet\Worksheet\Drawing;

class NameListExport implements FromCollection, WithHeadings, WithMapping, WithEvents
{
    use Exportable;

    protected $members;
 
    public function __construct($members)
    {
        $this->members = $members;
    }

    public function headings():array
    {
        return [
            '顔写真', '名前',
        ];
    }

    public function map($member):array
    {
        return ['', $member['name']];   // A列は画像を出力するので空文字にしておく
    }

    public function collection()
    {
        return $this->members;
    }

    public function getDrawings()
    {
        $drawings = [];

        $rowNum = 2;    // 1行目はヘッダ行なので画像出力は2行目から

        foreach ($this->members as $member) {
            $drawing = new Drawing;
            $drawing->setPath($member['file']);
            $drawing->setCoordinates('A'.$rowNum);
            $drawing->setWidthAndHeight(60, 60);
            $drawings[] = $drawing;

            $rowNum++;
        }
        
        return $drawings;
    }

    public function registerEvents(): array
    {
        $drawings = $this->getDrawings();

        return [
            AfterSheet::class => function (AfterSheet $event) use ($drawings) {
                $workSheet = $event->sheet->getDelegate();

                // 画像挿入処理
                foreach ($drawings as $drawing) {
                    $drawing->setWorkSheet($workSheet);
                }
            }
        ];
    }
}

再度tinkerで出力すると行ズレが無い期待通りの出力が得られるはずです。

まとめ

画像挿入に関しては正直Laravel ExcelのWithDrawingsを使うメリットがあまり感じられませんでした。Laravel Excel側でやってくれるのはWorkSheetへのセットだけですし、それもデータの挿入と併用すると行ズレ問題が発生してしまいます。

今回色々調べていて実感したのはLaravel ExcelはあくまでPhpShreadsheetのwrapperなので、Laravel Excel側でうまく処理できないケースについてはWithEventsなどを使って直接PhpSpreadsheet側で処理してしまうのが良さそうです。

By hikaru