Laravel-Excelでセル結合した複数シートを出力する


この記事は、[Fusic その2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/fusic-2) 22日目の記事です。

## はじめに

LaravelでExcel出力するなら [Laravel-Excel](https://laravel-excel.com/) を使っている方が多いかと思います。

ただこのライブラリ、[PHPSpreadSheet](https://phpspreadsheet.readthedocs.io/en/latest/) の完全なラッパーではありません。
それ故に少々詰まったので、書き残しておきます。


## 環境
PHP 7.4.1
Laravel 6.9.0
Laravel-Excel 3.1

## クラス構成
Userモデルを介して取得した全データを、
UsersExportクラスにDIし、各ユーザごとにUsersSheetクラスを作成します。

```
├── app
│   ├── Exports
│   │   ├── UsersExport.php
│   │   └── UsersSheet.php
│   └── User.php
```

最終的には各ユーザごとにレイアウトが変動する複雑なExcelファイルの出力が必要だったのですが、
今回はその前段階として、結合セルのある複数シートの出力を目指します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506521/7fcf669b-1a09-31e3-f1bf-1b25d9a42d56.png)


## セル結合

Laravel-Excel では、PHPSpreadsheet の `mergeCells()` がラッパーされていません。
即ちセル結合をする際は、ライフサイクルの中で `PHPOffice\PHPSpreadSheet\WorkSheet\WorkSheet` インスタンスを取得する必要があります。

### Laravel-Excelのライフサイクル
以下4つのイベントが順に発火します。

- BeforeExport
- BeforeSheet
- AfterSheet
- BeforeWriting

[公式ドキュメント](https://docs.laravel-excel.com/3.1/architecture/)の記述によると、 `PHPOffice\PHPSpreadSheet\WorkSheet\WorkSheet`が生成されるのはBeforeSheetイベントの直前です。
しかし、肝心の取得方法は記載されていません。

### `PHPOffice\PHPSpreadSheet\WorkSheet\WorkSheet`の取得

以下のようにして、BeforeSheetイベント内で取得できました。
あとはPHPSpreadSheetのドキュメントに従うだけです。
$event->getSheet()で取得されるのが `Maatwebsite\Excel\Sheet` なのはややこしいですね。


```php

namespace App\Exports;

use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\BeforeSheet;

class UsersExport implements WithEvents
{
    /**
     * @return array
     */
    public function registerEvents(): array
    {
        return [
            BeforeSheet::class => function (BeforeSheet $event) {
                $sheet = $event->getSheet()->getDelegate();
                // セル結合
                $sheet->mergeCells('A2:D2');
            },
        ];
    }
}
```

## 複数シートの出力

続いて、複数シートの出力を行います。

### 失敗例

[Laravel-Excel # Multiple Sheets](https://docs.laravel-excel.com/3.1/exports/multiple-sheets.html) を参考に、
先ほどのコードに `WithMultipleSheets` と `sheets()` メソッドを追加してみました。


```php

namespace App\Exports;

use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\BeforeSheet;

class UsersExport implements WithEvents, WithMultipleSheets
{
    protected $users;

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

    /**
     * @return array
     */
    public function registerEvents(): array
    {
        return [
            BeforeSheet::class => function (BeforeSheet $event) {
                $user = $event->getSheet()->user;
                $sheet = $event->getSheet()->getDelegate();
                // セル結合
                $sheet->mergeCells('A2:D2');
                $sheet->setCellValue('A2', $user->name);
            },
        ];
    }

    /**
     * @return array
     */
    public function sheets(): array
    {
        $sheets = [];
        foreach ($this->users as $user) {
            $sheets[] = new UsersSheet($user);
        }
        return $sheets;
    }
}
```
```php
<?php

namespace App\Exports;

use App\User;

class UsersSheet
{
    public $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }
}
```

これを実行すると…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506521/2fc286b2-c93e-6f50-c152-0c51bc4431e7.png)

**まっさら！！！**


### ライフサイクルの確認

各イベントが発火しているのか確認します。

```php
<?php

namespace App\Exports;

use App\User;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Maatwebsite\Excel\Events\BeforeExport;
use Maatwebsite\Excel\Events\BeforeWriting;
use Maatwebsite\Excel\Events\BeforeSheet;
use Maatwebsite\Excel\Events\AfterSheet;

class UsersExport implements WithEvents, WithMultipleSheets
{

    protected $users;

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

    /**
     * @return array
     */
    public function registerEvents(): array
    {
        return [
            BeforeExport::class => function (BeforeExport $event) {
                \Log::debug('===========================================');
                \Log::debug('BeforeExport');
                \Log::debug('===========================================');
            },
            BeforeSheet::class => function (BeforeSheet $event) {
                \Log::debug('===========================================');
                \Log::debug('BeforeSheet');
                \Log::debug('===========================================');
            },
            AfterSheet::class => function (AfterSheet $event) {
                \Log::debug('===========================================');
                \Log::debug('AfterSheet');
                \Log::debug('===========================================');
            },
            BeforeWriting::class => function (BeforeWriting $event) {
                \Log::debug('===========================================');
                \Log::debug('BeforeWriting');
                \Log::debug('===========================================');
            },
        ];
    }


    /**
     * @return array
     */
    public function sheets(): array
    {
        $sheets = [];
        foreach ($this->users as $user) {
            $sheets[] = new UsersSheet($user);
        }
        return $sheets;
    }
}
```

```
[2019-12-22 03:42:15] local.DEBUG: ===========================================  
[2019-12-22 03:42:15] local.DEBUG: BeforeExport  
[2019-12-22 03:42:15] local.DEBUG: ===========================================  
[2019-12-22 03:42:15] local.DEBUG: ===========================================  
[2019-12-22 03:42:15] local.DEBUG: BeforeWriting  
[2019-12-22 03:42:15] local.DEBUG: ===========================================  
```
`BeforeSheet`, `AfterSheet` イベントが発火していませんね。

`UsersSheet` のほうにもデバッグコードを追加してみます。

```php

class UsersSheet implements WithEvents
{
    /**
     * @return array
     */
    public function registerEvents(): array
    {
        return [
            BeforeExport::class => function (BeforeExport $event) {
                \Log::debug('===========================================');
                \Log::debug('BeforeExport -- Sheet');
                \Log::debug('===========================================');
            },
            BeforeSheet::class => function (BeforeSheet $event) {
                \Log::debug('===========================================');
                \Log::debug('BeforeSheet -- Sheet');
                \Log::debug('===========================================');
            },
            AfterSheet::class => function (AfterSheet $event) {
                \Log::debug('===========================================');
                \Log::debug('AfterSheet -- Sheet');
                \Log::debug('===========================================');
            },
            BeforeWriting::class => function (BeforeWriting $event) {
                \Log::debug('===========================================');
                \Log::debug('BeforeWriting -- Sheet');
                \Log::debug('===========================================');
            },
        ];
    }
}
```

```
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: BeforeExport  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: BeforeSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: AfterSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: BeforeSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: AfterSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: BeforeSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: AfterSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: BeforeSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: AfterSheet -- Sheet  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
[2019-12-22 04:03:45] local.DEBUG: BeforeWriting  
[2019-12-22 04:03:45] local.DEBUG: ===========================================  
```

`BeforeSheet`, `AfterSheet` はどちらも `UsersSheet` 内での発火になっています。
Sheetイベントなので、確かに各Sheetクラスに委任するのが自然ですね。


## 最終コード

以下のコードで、望んでいたExcelが出力できました。

```php
<?php

namespace App\Exports;

use App\User;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;

class UsersExport implements WithMultipleSheets
{
    protected $users;

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

    /**
     * @return array
     */
    public function sheets(): array
    {
        $sheets = [];
        foreach ($this->users as $user) {
            $sheets[] = new UsersSheet($user);
        }
        return $sheets;
    }
}
```

```php
<?php

namespace App\Exports;

use App\User;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Maatwebsite\Excel\Events\BeforeSheet;

class UsersSheet implements WithEvents, WithTitle
{
    private $user;

    /**
     * @param User $user
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     * @return array
     */
    public function registerEvents(): array
    {
        return [
            BeforeSheet::class => function (BeforeSheet $event) {
                $sheet = $event->getSheet()->getDelegate();
                $sheet->mergeCells('A2:D2');
                $sheet->setCellValue('A2', $this->user->name);
            },
        ];
    }

    public function title(): string
    {
        return $this->user->name;
    }
}
```

## まとめ

今回はセル結合のみを行いましたが、
`PHPOffice\PHPSpreadSheet\WorkSheet\WorkSheet`インスタンスの取得方法がわかったので、ほかにもいろいろできそうです。
BeforeSheet以外のイベントも上手く使っていきたいと思います。