この記事は、Fusic その2 Advent Calendar 2019 22日目の記事です。
はじめに
LaravelでExcel出力するなら Laravel-Excel を使っている方が多いかと思います。
ただこのライブラリ、PHPSpreadSheet の完全なラッパーではありません。
それ故に少々詰まったので、書き残しておきます。
環境
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ファイルの出力が必要だったのですが、
今回はその前段階として、結合セルのある複数シートの出力を目指します。
セル結合
Laravel-Excel では、PHPSpreadsheet の mergeCells()
がラッパーされていません。
即ちセル結合をする際は、ライフサイクルの中で PHPOffice\PHPSpreadSheet\WorkSheet\WorkSheet
インスタンスを取得する必要があります。
Laravel-Excelのライフサイクル
以下4つのイベントが順に発火します。
- BeforeExport
- BeforeSheet
- AfterSheet
- BeforeWriting
公式ドキュメントの記述によると、 PHPOffice\PHPSpreadSheet\WorkSheet\WorkSheet
が生成されるのはBeforeSheetイベントの直前です。
しかし、肝心の取得方法は記載されていません。
PHPOffice\PHPSpreadSheet\WorkSheet\WorkSheet
の取得
以下のようにして、BeforeSheetイベント内で取得できました。
あとはPHPSpreadSheetのドキュメントに従うだけです。
$event->getSheet()で取得されるのが Maatwebsite\Excel\Sheet
なのはややこしいですね。
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 を参考に、
先ほどのコードに WithMultipleSheets
と sheets()
メソッドを追加してみました。
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
namespace App\Exports;
use App\User;
class UsersSheet
{
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
}
これを実行すると…
まっさら!!!
ライフサイクルの確認
各イベントが発火しているのか確認します。
<?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
のほうにもデバッグコードを追加してみます。
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
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
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以外のイベントも上手く使っていきたいと思います。