【Laravel-Excel】Laravelでエクセルファイルを操作する
背景
Laravelにはエクセルファイルを簡単に操作できるLaravel-Excelというライブラリがあります。
このライブラリの記事はたくさんありますが、Githubのリポジトリやテストなどのサンプルコードがあるものが少なかったのでメモ
すべてのコードはGithubのリポジトリにpushしてあります。
諸注意
サンプルコードは簡便のため、層の分離等は行っておらず若干ファットコントローラーになっています。
インストール
環境
- Laravel 8.x
- PHP 8
Laravelインストール
docker-laravelというLaravelアプリを簡単に用意できる環境があるのでこちらをclone
する。
READMEにしたがって環境を立ち上げる。
ライブラリの準備
以下のようにinfra/docker/php/Dockerfile
を書き換える。
FROM php:8.0-fpm-buster
LABEL maintainer="ucan-lab <yes@u-can.pro>"
SHELL ["/bin/bash", "-oeux", "pipefail", "-c"]
# timezone environment
ENV TZ=UTC \
# locale
LANG=en_US.UTF-8 \
LANGUAGE=en_US:en \
LC_ALL=en_US.UTF-8 \
# composer environment
COMPOSER_ALLOW_SUPERUSER=1 \
COMPOSER_HOME=/composer
COPY /usr/bin/composer /usr/bin/composer
RUN apt-get update && \
apt-get -y install git libicu-dev libonig-dev libfreetype6-dev libpng-dev libjpeg-dev libzip-dev unzip locales && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
locale-gen en_US.UTF-8 && \
localedef -f UTF-8 -i en_US en_US.UTF-8 && \
mkdir /var/run/php-fpm && \
docker-php-ext-install intl pdo_mysql zip bcmath && \
docker-php-ext-configure gd --with-freetype --with-jpeg && \
docker-php-ext-install -j$(nproc) gd && \
composer config -g process-timeout 3600 && \
composer config -g repos.packagist composer https://packagist.org
COPY ./infra/docker/php/php-fpm.d/zzz-www.conf /usr/local/etc/php-fpm.d/zzz-www.conf
COPY ./infra/docker/php/php.ini /usr/local/etc/php/php.ini
WORKDIR /work/backend
追記した処理としては
- 画像処理系のライブラリをコンテナのOSにインストール
- gdにフォントとjpegを使用できるように設定
- PHPの画像処理ライブラリ「gd」をインストール
phpコンテナのビルドし直し
docker compose build app
php spread sheetのインストール
composer require phpoffice/phpspreadsheet
laravel excelのインストール
composer require maatwebsite/excel
念の為 service provider にライブラリを登録
'providers' => [
/*
* Package Service Providers...
*/
Maatwebsite\Excel\ExcelServiceProvider::class,
]
念の為、Facadeも登録
'aliases' => [
...
'Excel' => Maatwebsite\Excel\Facades\Excel::class,
]
configファイルをpublish
php artisan vendor:publish --provider="Maatwebsite\Excel\ExcelServiceProvider" --tag=config
エクスポート
ここではUserモデルのデータをxlsx形式で書き出し、画面上でダウンロードできるようにします。
実装
Exportクラスを定義
php artisan make:export UsersExport --model=User
<?php
namespace App\Exports;
use App\Models\User;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromCollection;
class UsersExport implements FromCollection
{
/**
* @return Collection
*/
public function collection(): Collection
{
return User::all();
}
}
UserController.php
にAPIを追加
public function download(): BinaryFileResponse
{
return Excel::download(new UsersExport, UsersExport::FILE_NAME);
}
APIを叩く画面を用意。
@extends('layouts.app')
@section('content')
<section>
<h1>Laravel/Excel Sample app</h1>
<div>
<a href="{{ route('users.excel.export.download') }}">Export user models</a>
</div>
</section>
@endsection
Export user models
のリンクをクリックすると、usersテーブルの全レコードがエクスポートされる。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290859/2a1a918e-99d2-a1b7-2081-522da42c1a2d.png" width=100%>
大容量データエクスポート時の対策
テーブルが大量のレコードを持つ場合、エクスポートに時間がかかりユーザーの離脱やクレームに繋がりかねません。
そこでLaravelのQueues(キュー)を用いて、エクスポートを非同期で実行させます。
エクスポート完了後、S3にファイルをアップロードし、ダウンロードリンクをメール送信します。
実装
S3にアップロードするためのライブラリを追加
composer require --with-all-dependencies league/flysystem-aws-s3-v3 "^1.0"
以下のartisanコマンドを実行し、DBにキューを管理するテーブルを追加
php artisan queue:table
php artisan migrate
キューに入れるジョブを定義
php artisan make:job NotifyUserOfCompletedExport
エクスポート完了時に送信する通知(Notification)を定義
php artisan make:notification ExportCompleted
通知が送信されるようにジョブを編集する。
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\User;
use App\Notifications\ExportCompleted;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
/**
* Class NotifyUserOfCompletedExport
* @package App\Jobs
*/
class NotifyUserOfCompletedExport implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* @var User
*/
private User $user;
/**
* @var string
*/
private string $fileName;
/**
* Create a new job instance.
*
* @param User $user
* @param string $fileName
*/
public function __construct(User $user, string $fileName)
{
$this->user = $user;
$this->fileName = $fileName;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$this->user->notify(new ExportCompleted($this->fileName));
}
}
通知にS3のリンクを含めるように修正する。
<?php
declare(strict_types=1);
namespace App\Notifications;
use App\Http\Controllers\UserController;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Storage;
/**
* Class ExportCompleted
* @package App\Notifications
*/
class ExportCompleted extends Notification
{
use Queueable;
/**
* @var string
*/
private string $fileName;
/**
* Create a new notification instance.
*
* @param string $fileName
* @return void
*/
public function __construct(string $fileName)
{
$this->fileName = $fileName;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable): array
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return MailMessage
*/
public function toMail($notifiable): MailMessage
{
$s3 = Storage::disk(UserController::STORAGE_S3);
return (new MailMessage)
->line('Export has been completed.')
->line('Please click link to download a exported file.')
->action('Download ' . $this->fileName, $s3->url($this->fileName));
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable): array
{
return [
//
];
}
}
動作確認
mailtrapに登録し、.env
に必要な情報を入力する。
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=********
MAIL_PASSWORD=********
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=test@test.com
MAIL_FROM_NAME="${APP_NAME}"
S3のバケットにアップロードするためAWSのクレデンシャル等を.env
に追加
AWS_ACCESS_KEY_ID=********
AWS_SECRET_ACCESS_KEY=********
AWS_DEFAULT_REGION=********
AWS_BUCKET=********
AWS_USE_PATH_STYLE_ENDPOINT=false
backend/resources/views/excel/index.blade.php
にキューエクスポート用のリンクを追加
@extends('layouts.app')
@section('content')
<section>
<h1>Laravel/Excel Sample app</h1>
<div>
<a href="{{route('users.excel.download')}}">Export user models</a>
</div>
<div>
<a href="{{route('users.excel.queue')}}">Add a job of export user models in queue</a>
</div>
</section>
@endsection
localhost/users/excel
にアクセスし「Add a job of export user models in queue」リンクをクリック
画面上部に成功メッセージが表示されることを確認。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290859/59a7f34b-9837-2682-d3c5-7db55f50d81b.png" width=100%>
mailtrapでファイルダウンロードリンク付きのメールが送信されることを確認する。
※ファイルダウンロードのためにはバケットの公開範囲を設定してください
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/290859/1dc89cfe-a14e-26a6-e115-d29df95c9b6f.png" width=100%>
テスト
ExcelファサードはExcel::fake()
でモックしテストコードを書くことができます。
出力されたファイルの中身等も検証することが可能です。
public function test_download()
{
Excel::fake();
User::factory()->count(10)->create();
$response = $this->get(route('users.excel.download'));
$response->assertOk();
Excel::assertDownloaded(UsersExport::FILE_NAME, function (UsersExport $export) {
//出力されたExcelファイルが10件のModelを持つか検証
return $export->collection()->count() === 10;
});
}
すべてのテストケースはUserControllerTest.phpに実装してあります。
インポート
エクセルに記載されたデータを取り込むことも可能。
実装
UserのImportクラスを追加
php artisan make:import UsersImport --model=User
UsersImportを下記のように編集する。
<?php
declare(strict_types=1);
namespace App\Imports;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Maatwebsite\Excel\Concerns\ToModel;
class UsersImport implements ToModel
{
/**
* @param array $row
*
* @return Model|User|null
*/
public function model(array $row): Model|User|null
{
return new User([
'name' => $row[0],
'email' => $row[1],
'd_o_b' => $row[2],
'sex' => $row[3],
'password' => Hash::make($row[4]),
]);
}
}
UserController.php
にAPIを追加
/**
* @param UploadPost $request
* @return View|RedirectResponse
*/
public function upload(UploadPost $request): View|RedirectResponse
{
try {
Excel::import(new UsersImport, $request->file('users'));
$message = 'Successfully imported an excel file';
return \view('excel.import', compact('message'));
} catch (Exception $e) {
logger()->error($e);
return redirect(route('users.excel.import.import-form'))
->withErrors($e->getMessage());
}
}
APIを叩く画面を用意
@extends('layouts.app')
@section('content')
<section>
<h1>Laravel/Excel Sample app</h1>
<div>
<p>Import</p>
<form
action="{{ route('users.excel.import.upload') }}"
method="POST"
enctype="multipart/form-data"
>
@csrf
<div>
<input type="file" id="users" name="users">
</div>
<div>
<input type="submit" value="Import">
</div>
</form>
</div>
</section>
@endsection
ファイルを選択し、Import
ボタンを押すとインポートが実行される
大容量データインポート時の対策
エクスポートと同様に大容量エクセルファイルがインポートされた際は、インポートの終了をメール通知することができます。
実装
UsersExport.php
にWithChunkReading
, ShouldQueue
インターフェースをimplementsさせる。
<?php
declare(strict_types=1);
namespace App\Imports;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithChunkReading;
class UsersImport implements ToModel, WithChunkReading, ShouldQueue
{
/**
* @param array $row
*
* @return Model|User|null
*/
public function model(array $row): Model|User|null
{
return new User([
'name' => $row[0],
'email' => $row[1],
'sex' => $row[2],
'password' => Hash::make($row[3]),
]);
}
public function chunkSize(): int
{
return 1000;
}
}
キューに入れるジョブを定義
php artisan make:job NotifyUserOfCompletedImport
エクスポート完了時に送信する通知(Notification)を定義
php artisan make:notification ImportCompleted
<?php
declare(strict_types=1);
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ImportCompleted extends Notification
{
use Queueable;
/**
* @var string
*/
private string $fileName;
/**
* Create a new job instance.
*
* @param string $fileName
*/
public function __construct(string $fileName)
{
$this->fileName = $fileName;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable): array
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable): MailMessage
{
return (new MailMessage)
->line('Import has been completed.')
->action('Return to the app', route('users.excel.import.import-form'));
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable): array
{
return [
//
];
}
}
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\User;
use App\Notifications\ImportCompleted;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class NotifyUserOfCompletedImport implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* @var User
*/
private User $user;
/**
* @var string
*/
private string $fileName;
/**
* Create a new job instance.
*
* @param User $user
* @param string $fileName
*/
public function __construct(User $user, string $fileName)
{
$this->user = $user;
$this->fileName = $fileName;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$this->user->notify(new ImportCompleted($this->fileName));
}
}
UserController.php
にAPIを用意
/**
* @param QueueImportPost $request
* @return View|RedirectResponse
*/
public function queueImport(QueueImportPost $request): View|RedirectResponse
{
try {
Excel::queueImport(
new UsersImport,
$file = $request->file('users'),
self::STORAGE_S3
)->chain([
new NotifyUserOfCompletedImport(
request()->user() ?? User::factory()->create(),
$request->file('users')->getClientOriginalName()
)
]);
$message = 'Successfully queued job of import an excel file';
return \view('excel.import', compact('message'));
} catch (Exception $e) {
logger()->error($e->getMessage());
return redirect(route('users.excel.import.import-form'))
->withErrors($e->getMessage());
}
}
APIを叩く画面を用意
@extends('layouts.app')
@section('content')
<section>
<h1>Laravel/Excel Sample app</h1>
<div>
<p>Import</p>
<form
action="{{ route('users.excel.import.upload') }}"
method="POST"
enctype="multipart/form-data"
>
@csrf
<div>
<input type="file" id="users" name="users">
</div>
<div>
<input type="submit" value="Import">
</div>
</form>
</div>
<div>
<p>Import by queue</p>
<form
action="{{ route('users.excel.import.queue') }}"
method="POST"
enctype="multipart/form-data"
>
@csrf
<div>
<input type="file" id="users" name="users">
</div>
<div>
<input type="submit" value="Import by queue">
</div>
</form>
</div>
</section>
@endsection
ファイルを選択し、Import by queue
ボタンを押すとインポートがQueueに入る
インポート終了すると、メールが送信される。
テスト
エクスポートと同様にアサーションが用意されているのでテストをかける。
※インポートされた後にDBテストを行う際はExcel::fake()
は使えないので注意
public function test_import_success()
{
$response = $this->post(route('users.excel.import.upload'), [
'users' => new UploadedFile(
'./tests/Feature/data/import_success.xlsx',
'import_success.xlsx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
null,
true
)
]);
$response
->assertOk()
->assertViewHas('message');
$this
->assertDatabaseHas('users', [
'name' => 'Dr. Keaton Beahan DVM',
'email' => 'myrtice.langosh@example.com',
])
->assertDatabaseHas('users', [
'name' => 'Amelia Auer DDS',
'email' => 'maximillian76@example.com',
])
->assertDatabaseHas('users', [
'name' => 'Mariane Satterfield',
'email' => 'cassin.brendon@example.net',
]);
}
すべてのテストケースはUserControllerTest.phpに実装してあります。
所感
これであなたもエクセルマスター
補足
ドキュメントに設計思想書いてあるので、読んでみたら勉強になるぜよ。
※記事へのご指摘歓迎します。
Discussion
php spread sheetのインストールを行っていますが、今回は使用されていないように見受けられます。
ご指摘ありがとうございますm
Laravel Excel本家サイトを参照すると、依存関係としてインストールに必要でしたので記載いたしましたmm