在开发过程中经常会用到 excel导入导出数据,以下是几个场景以及对应的解决方案,操作之前先安装依赖: composer require maatwebsite/excel (最好3.1+版本): 1.excel批量导入
在控制器里添加如下方法:
public function import(Request $request, ProductImport $productImport)
{
try {
$productImport->import($request->file('excel'));
//Excel::import(new ProductImport(), $request->file('excel')); //也可以这样使用
} catch (\Throwable $throwable) {
return $this->_error($throwable->getMessage());
}
return $this->_success("导入成功");
}
在 namespace App\Imports; 下定义 ProductImport:
<?php
namespace App\Imports;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithChunkReading;
use Maatwebsite\Excel\Concerns\Importable;
class ProductImport implements ToCollection, WithChunkReading
{
use Importable;
public function chunkSize(): int
{
//控制数据读取大小,防止一次导入太多数据
return 500;
}
public function collection(Collection $rows)
{
//如果需要去除表头
unset($rows[0]);
foreach ($rows as $key => $row) {
$this->_saveData($row->toArray());
}
}
private function _saveData($data)
{
try {
DB::transaction(function () use ($data) {
$product['name'] = $data[0] ?? '';
$product['cover_image'] = $data[1] ?? '';
$product['price'] = $data[2] ?? 0;
$product['description'] = $data[3] ?? '';
$product['detail'] = $data[4] ?? '';
$exist = Product::query()->where(['name' => $data[0]])->first();
if (!$exist) {
Product::query()->create($product);
}
});
} catch (\Throwable $exception) {
Log::info("create error is:" . $exception->getMessage());
}
}
}
2.批量导出 ①数据量不大的时候可以有下面几种方法
1.基于数组的导出
在控制器下添加如下方法
public function exports(Request $request)
{
$data = ProductService::index($request->all());
//可以传递 $request->input('page', 1) 分页参数
return (new ProductExport($data))->download();
}
在 namespace App\Imports;下添加如下 ProductExport 类
use Illuminate\Contracts\Support\Responsable;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Events\AfterSheet;
class ProductExport implements FromArray, Responsable, WithMapping, WithHeadings, ShouldAutoSize, WithEvents
{
use Exportable;
protected $data;
protected $fileName = '.xlsx';
public function __construct(array $data)
{
$this->data = $data;
$title = '产品数据-' . date('Y-m-d');
$this->fileName = $title . '.xlsx';
return $this;
}
public function array(): array
{
return $this->data;
}
public function map($product): array
{
return [
$product['name'],
$product['cover_image'],
$product['price'],
$product['description'],
$product['detail']
];
}
public function headings(): array
{
return [
'名称',
'产品图片',
'产品价格',
'产品描述',
'产品详情'
];
}
public function registerEvents(): array
{
return [
AfterSheet::class => function (AfterSheet $event) {
$event->sheet->getColumnDimension('A')->setAutoSize(false)->setWidth(10);
$event->sheet->getColumnDimension('B')->setAutoSize(false)->setWidth(50);
$event->sheet->getColumnDimension('C')->setAutoSize(false)->setWidth(10);
$event->sheet->getColumnDimension('D')->setAutoSize(false)->setWidth(10);
$event->sheet->getColumnDimension('E')->setAutoSize(false)->setWidth(10);
}
];
}
}
//当需要导出图片的时候可以按下面的方法操作
<?php
/**
* Created by PhpStorm.
* User: guoshipeng
* Date: 2022/9/20
* Time: 15:25
*/
namespace App\Imports;
use Illuminate\Contracts\Support\Responsable;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
class ProductExport implements FromArray, Responsable, WithMapping, WithHeadings, ShouldAutoSize, WithEvents
{
use Exportable;
protected $data;
protected $fileName = '.xlsx';
public function __construct(array $data, $page = 1)
{
$this->data = $data;
$title = '用户数据-' . '第' . $page . '页-' . date('Y-m-d');//这个是导出当前页的写法
$this->fileName = $title . '.xlsx';
return $this;
}
public function array(): array
{
return $this->data;
}
public function map($user): array
{
return [
$user->id,
$user->name,
$user->description,
//$user->cover_image, //这个图片链接可以不用了 ,因为后面会通过 Drawing 将链接渲染为图片
];
}
public function headings(): array
{
return [
'产品ID',
'产品名称',
'产品详情',
'产品图片',
];
}
public function registerEvents(): array
{
return [
AfterSheet::class => function (AfterSheet $event) {
$event->sheet->getColumnDimension('A')->setAutoSize(false)->setWidth(10);
$event->sheet->getColumnDimension('B')->setAutoSize(false)->setWidth(50);
$event->sheet->getColumnDimension('C')->setAutoSize(false)->setWidth(30);
$event->sheet->getColumnDimension('D')->setAutoSize(false)->setWidth(10);
//导出图片部分,官网:https://docs.laravel-excel.com/3.1/exports/drawings.html 中通过实现 implements WithDrawings 里的方法实现也可以,但是具体到逻辑,不用 implements WithDrawings,直接使用Drawing类实例化对象即可,我认为下面的写法更合理一些
$data = $this->array();
foreach ($data as $key => $val) {
$temp = $val->toArray();
$event->sheet->getRowDimension($key + 2)->setRowHeight(30);
//直接用var_dump($temp['id']),没有任何效果,但是通过Log打印日志,就可以取到值,说明是有值的
//Log::info("id is:".$temp['id']);
//Log::info("cover_image is:".$temp['cover_image']);
if ($temp['cover_image']) {
$base_name = pathinfo($temp['cover_image'])['basename'];
if (!file_exists('/img/' . $base_name)) {//防止重复存储图片
$get_file = @file_get_contents($temp['cover_image']);//需要通过 file_get_contents 而不是 file_put_contents
if ($get_file) {
$fp = @fopen(storage_path('/img/' . $base_name), "w");
@fwrite($fp, $get_file);
@fclose($fp);
}
}
$drawing = new Drawing(); // 给每个图片链接一个 Drawing 对象处理
$drawing->setResizeProportional(true);// 这个还没搞懂怎么使用,感觉是要重置属性
//在导出图片的时候不能设置width,一设置就会有展示问题,比如这里设置为10,实际上展示的根本就不是10,而是很大的一个值
//$drawing->setWidth(10);
$drawing->setPath(storage_path('/img/' . $base_name)); // 这个必须为本地图片,需要先将外网图片下载存储到本地,我这里存储到storage/img下,将来这个文件夹肯定会很大,尽管我做了不重复存储的判断
$drawing->setHeight(30);// 这里要设置 一个宽度,不然显示也会有问题,但是 尽管这里设置了30
$drawing->setCoordinates('D' . ($key + 2));// 这个主要用来设置 图片展示在哪行哪列
$drawing->setWorksheet($event->sheet->getDelegate());// 当前 $drawing 作用在哪里
}
}
}
];
}
}
2.普通查询导出
在控制器下添加如下方法:
public function export()
{
return (new ProductExport())->download();
}
在 namespace App\Imports; 下添加如下代码:
<?php
/**
* Created by PhpStorm.
* User: guoshipeng
* Date: 2022/9/30
* Time: 10:09
*/
namespace App\Imports;
use App\Models\Product\Product;
use Illuminate\Contracts\Queue\ShouldQueue;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithCustomQuerySize;
use Maatwebsite\Excel\Concerns\WithMapping;
/**
*
* Class ProductExport
* @package App\Imports
*/
class ProductExport implements FromQuery, WithMapping
{
use Exportable;
protected $where;
protected $fileName = '产品数据测试.xlsx';
public function query()
{
return Product::query()->where('id', '<', 100); // 使用 where 查询,这里只用查询,不用get;这个where 条件可以在控制器里实例化 ProductExport 的时候通过自定义set/get方法传递,或者在构造方法里传递
}
public function map($product): array
{
return [
$product->id,
$product->name,
$product->description,
];
}
}
3.分多个excel sheet的导出
在控制器下添加如下方法:
public function export(Request $request)
{
$sheetData = [];
Product::query()->chunkById(1000, function ($items) use (&$sheetData) {
$data = [];
$sheetName = '产品列表数据';//每一个sheet的名称,多个的话 excel 会自动在后面追加阿拉伯数字
$columnFormats = [];//根据需求添加
$temp = [];
if ($items->isNotEmpty()) {
$items->each(function ($item) use (&$temp) {
$temp[] = ['id' => $item->id, 'name' => $item->name, 'cover_image' => $item->cover_image, 'price' => $item->price, 'description' => $item->description, 'created_at' => $item->created_at];
});
}
//对列进行特殊处理,use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
$columnFormats['D'] = NumberFormat::FORMAT_NUMBER_0; //格式化数字
$columnFormats['F'] = NumberFormat::FORMAT_DATE_YYYYMMDD2; //格式化时间
$data['sheet'] = $sheetName;
$data['data'] = $temp;
$data['columnFormats'] = $columnFormats;
$sheetData[] = $data;
});
// 标题
$headings = ['ID', '名称', '封面图', '价格', '描述', '创建时间'];
$sheet = [];
foreach ($sheetData as &$item) {
$sheet[] = new ProductMultiSheet($item['data'], $headings, $item['sheet'], $item['columnFormats']);
}
return (new MultiTest())->download($sheet);
}
在 namespace App\Imports;下添加如下代码,由于是分多个sheet导出,需要两个类来操作,一个做导出操作,还有一个做单个excel表数据处理:
<?php
/**
* Created by PhpStorm.
* User: guoshipeng
* Date: 2022/9/30
* Time: 11:48
*/
namespace App\Imports;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
use Maatwebsite\Excel\Events\AfterSheet;
class ProductMultiSheet implements FromArray, WithTitle, WithHeadings, WithColumnFormatting, WithEvents
{
private $data;
private $headings;
private $title;
private $columnFormats;
/**
* @param array $data 数据
* @param array $headings 头部
* @param string $title sheet名称
* @param array $columnFormats 格式化列
*/
public function __construct(array $data, array $headings, string $title, array $columnFormats = []) {
$this->data = $data;
$this->headings = $headings;
$this->title = $title;
$this->columnFormats = $columnFormats;
}
/**
* 数据
* @return array
*/
public function array() : array {
return $this->data ? : [];
}
/**
* 表头
* @return array
*/
public function headings() : array {
return $this->headings ? : [];
}
/**
* sheet名称
* @return string
*/
public function title() : string {
return $this->title ? : '';
}
/**
* 列格式列
* @return array
*/
public function columnFormats() : array {
return $this->columnFormats ? : [];
}
public function registerEvents(): array
{
return [
AfterSheet::class => function (AfterSheet $event) {
$event->sheet->getColumnDimension('A')->setAutoSize(false)->setWidth(10);
$event->sheet->getColumnDimension('B')->setAutoSize(false)->setWidth(50);
$event->sheet->getColumnDimension('C')->setAutoSize(false)->setWidth(10);
$event->sheet->getColumnDimension('D')->setAutoSize(false)->setWidth(10);
$event->sheet->getColumnDimension('E')->setAutoSize(false)->setWidth(10);
$event->sheet->getColumnDimension('F')->setAutoSize(false)->setWidth(10);
}
];
}
}
<?php
/**
* Created by PhpStorm.
* User: guoshipeng
* Date: 2022/9/30
* Time: 13:55
*/
namespace App\Imports;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
class MultiTest implements WithMultipleSheets
{
private $sheets;
/**
* @param array $data
* @param array $registerEvents
* @param string $filename
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function download(array $sheets, string $filename = '产品数据.xlsx')
{
$this->sheets = $sheets;
return \Maatwebsite\Excel\Facades\Excel::download($this, $filename);
}
/**
* @return array
*/
public function sheets(): array
{
return $this->sheets ?: [];
}
}
本来还想在多个sheet导出,通过异步队列实现,但是没有成功,这个以后再弄
②数据量很大的时候需要用到异步队列(redis)操作
在控制器下添加如下方法:
public function export()
{
//注意,通过队列导出 需要先创建 store 里的 filePath 文件地址(完整地址)
//注意,这个异步执行可能需要很久,所以 打开时间太快的话 产品数据测试2.xlsx 可能没有数据
//注意,这个 chain 里的job 必须要通过队列执行(implements ShouldQueue),不能同步执行,我感觉是 store 走了队列,然后 job 也要走队列
(new ProductMultiExport())->store(storage_path('logs').'/产品数据测试2.xlsx')->chain([new SendEmail()]);
return $this->_success("操作完成");
}
在 namespace App\Imports; 下添加如下代码:
<?php
/**
* Created by PhpStorm.
* User: guoshipeng
* Date: 2022/9/30
* Time: 10:09
*/
namespace App\Imports;
use App\Models\Product\Product;
use Illuminate\Contracts\Queue\ShouldQueue;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithCustomQuerySize;
use Maatwebsite\Excel\Concerns\WithMapping;
/**
* 可以通过 implements ShouldQueue 实现通过队列导出
*
* Class CompanyMultiExport
* @package App\Imports
*/
class ProductMultiExport implements FromQuery, WithMapping, WithCustomQuerySize, ShouldQueue
{
use Exportable;
protected $where;
protected $fileName = '产品数据测试.xlsx';
//注意:当使用队列异步导出的时候,好像只能用query()获取数据,array()一直获取不到数据
public function query()
{
return Product::query()->where('id', '<', 10000); // 使用 where 查询,这里只用查询,不用get
}
public function map($product): array
{
return [
$product->id,
$product->name,
$product->description,
];
}
//必须要有这个,默认队列只会导出 1000 条数据,需要指定导出总数,不然数据不完整
public function querySize(): int
{
return Product::query()->where('id', '<', 10000)->count();//和query的条件要一样
}
}
在 namespace App\Jobs; 下添加下面代码:
<?php
/**
* Created by PhpStorm.
* User: guoshipeng
* Date: 2022/9/30
* Time: 16:10
*/
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Log;
class SendEmail implements ShouldQueue
{
use Queueable, Dispatchable, InteractsWithQueue, SerializesModels;
public function handle()
{
Log::info("下载完成,可以发邮件了通知下载了");
}
}
