25. Yii2使用PHPExcel第三方扩展

解释:以下代码非我所写,我只是添加部分注释和说明,不要用来说事,若能帮助到,自己实现所需的功能就行了。

1. 说明

PHPExcel是一个由纯PHP写的函数库,从不同的电子表格中读取和写回数据。PHPExcel这个项目围绕微软的OpenXML和PHP标准创建,GitHub的源码托管地址

可以去听听慕课的免费视频

网上有详细的设置教程,并且源代码中提供了很多的使用样例,几乎能用到的场景都有现成的例子。汉老师的yiifans也列有好几篇,我大体看过,都是很好的教程,对初学者都是不错的选择。

这里只提供一种简单粗暴的解决方案,目前也是我们正在使用的,没有对文件迭代一类的思路给予处理。

不推荐动不动就要把这些第三方扩展集成到框架中,直接使用最基本的引用就足够。网上看了一些成年旧帖,没准还让你怎么修改框架的配置文件,设置各种参数才达到简单的目的,浪费时间不说,还破坏了原本干净的框架,维护交接都是问题。

我们使用的是yii2高级模板,目录类似:

zoulu - 
      - backend
      - common
      ----extend 第三方扩展存放文件夹
      --------PHPExcel PHPExcel扩展文件夹
      ------------ PHPExcel 扩展内部代码,非必要时不推荐修改
      ------------ .readme 简单的说明文件
      ------------ PHPExcel.php 扩展入口文件 非必要不推荐修改
      ------------ Excel.php 自己封装的针对该扩展的调用类,以后对该扩展的引用全部通过该类来完成
      - console
      - frontend
      - tests
      - vendor
      ...

2. 封装

// 项目本身需要的命名空间
namespace common\extend\phpexcel;

// 在这里引入扩展入口文件
// @todo 不记得什么时候,网上还有教程说,为了防止扩展和框架的依赖注入冲突,使用了各种“骇客”手段
// 现在看来,技术的东西,会就简单,不会就稀里糊涂。
require 'PHPExcel.php';

class Excel extends \PHPExcel
{
    // 单例
    public static $instance;
    public static function getInstance()
    {
        if (empty(self::$instance)) self::$instance = new self();
        return self::$instance;
    }

    /**
     * 获取Excel表单数据
     * 说明:通常来说,数据量不是很大(多大算大?)按照这种方式一次性读入内存中是没什么关系的,
     * 扩展提供了文件迭代器协议的方式循环读取,可参见开头给出的慕课网免费视频,几乎常用的都提到了
     * @param int $inFile 读取文件路径
     * @param bool $index 读取表格索引,默认读取所有数据 合并后返回
     * @return array
     */
    public function readSheet($inFile, $index = false)
    {
        // 得到文件类型
        $type = \PHPExcel_IOFactory::identify($inFile);
        $reader = \PHPExcel_IOFactory::createReader($type);
        // 导入文件
        $sheet = $reader->load($inFile);
        // 获取文件中表格sheet的数量
        $sCount = $sheet->getSheetCount();
        // 如果指定了获取某一个子表格(表格中,可能有多个子表格)的数据,转化为数组返回
        if (is_int($index) && $index < $sCount && $index >= 0) {
            return $sheet->getSheet($index)->toArray();
        }
        // 只有一个子表格
        if ($sCount == 1) {
            return $sheet->getSheet(0)->toArray();
        }
        $data = [];
        // 所有表格数据全部以数组格式返回
        for ($i=0; $i < $sCount; $i++) {
            $data[] = $sheet->getSheet($i)->toArray();
        }
        unset($sheet,$reader, $type);

        return $data;
    }

    /**
     * 将数据保存至Excel表格
     * 说明:只是一个子表时请自行处理,道理类似
     * @param string $outFile 要保存的文件路径
     * @param array $data 需要保存的数据 二维数组
     * @return bool
     */
    public function saveSheet($outFile, array $data)
    {
        $path = explode('/',$outFile);
        unset($path[count($path)-1]);
        // DIRECTORY_SEPARATOR 常量是框架定义的目录分隔符,(服务器环境自知,可以不用这么麻烦)
        $path = implode('/',$path) . DIRECTORY_SEPARATOR;
        //目录不存在 则创建目录 需要父目录有写权限才可以创建子目录,
        // Linux基础现在几乎都多多少少会了,不再是什么高深的知识
        if (!file_exists($path)) { 
            @mkdir($path, 0777, TRUE); 
            @chmod($path, 0777);
        }

        // 实例化一个PHPExcel对象
        $newExcel = new \PHPExcel();
        // 得到一个默认的激活表格,预备写入数据
        $newSheet = $newExcel->getActiveSheet();
        $newSheet->fromArray($data);
        // 格式按自己需要,源码文件样例中有写,(下面这个其实是excel2003的标准)
        $objWriter = \PHPExcel_IOFactory::createWriter($newExcel, 'Excel5');
        // 保存数据到表格中
        $objWriter->save($outFile);
        unset($objWriter,$newSheet, $newExcel);
        return true;
    }

    /**
     * @param array $data 需要过滤处理的数据 二维数组
     * @param int $cols  取N列
     * @param int $offset  排除 N 行,比如读取一个表格数据时,标题这一行可能是不希望读出来的,
     *                     毕竟这部分和存入数据库中没什么关系,就排除这一行
     * @param bool|int $must 某列不可为空  0 - index
     * @return array
     */
    public function handleSheetArray(array $data, $cols = 10, $offset = 1, $must = false)
    {
        $final = [];
        if ($must && $must >= $cols) {
            $must = false;
         }

        foreach($data as $key => $row) {
            if ($key < $offset) {
                continue;
            }
            $t = [];
            for ($i = 0; $i < $cols; $i++) {
                if (isset($row[$i])) {
                    $t[$i] = trim(strval($row[$i]));
                } else {
                    $t[$i] = '';
                }
            }
            if (is_array($row) && implode('', $t) && ($must===false || $t[$must])) {
                $final[] = $t; 
                continue;
            }
        }

        return $final;
    }
}

3. 导出数据

有这么一个场景,数据库中存有帖子信息,根据搜索条件搜索出一部分帖子,想把它导出到Excel表格中,来帅选…

namespace backend\modules\feed\controllers;

use yii\web\Controller;

class FeedController extends Controller
{
    public function actionExport()
    {
        // 各种搜索条件过滤后得到一个相对较大的数组,数据太大不合适,上千条也能导出,但是如果
        // 数据偏大,可能用浏览器触发来操作就不合适了,可以考虑写好后用控制台或者命令行导出

        // 得到搜索结果集$list
        // 这一行定义导出后的表格头部,相当于table 的 th ,标题需要设置,这里没有标题显得简单
        $final = [['小区','帖子ID','用户昵称', '用户类型','用户ID','标签','内容','赞','评论','分享','收藏','发帖时间']];
        foreach ($list as $feed) {
            // 把需要处理的数据都处理一下
            $communityName = '';
            $nickname = '';
            $userType = '';
            $tagName = '';
            $final[] = [
                $communityName, $feed['feedid'], $nickname, $userType, $feed['userid'], $tagName,
                $feed['content'], $feed['diggcount'], $feed['commentcount'], $feed['sharecount'],
                $feed['favcount'], $feed['ctime'],
            ];

        }
        unset($list);

        // 使用我们写好的saveSheet()方法导出数据
        $outFile = \Yii::$app->getRuntimePath().'/fileIO/feed/' . date("YmdHis") . '.xls';
        $ret = \common\extend\phpexcel\Excel::getInstance()->saveSheet($outFile, $final);
        if ($ret) {
            // 导出成功了,其它操作
        }
        // 看了代码其实都知道,这个是恒返回真,一般来说只要不是权限问题,即是你的数据不对,都会导出成功
    }
}

4. 读入数据

有这么一个场景,客户需求提供一个文件上传功能,可以上传Excel表格,上传完成后希望看到上传的结果列表再执行其它操作。上传文件对服务器来说本身是很危险的,包括图片,因此可能需要做很多限制,以防发生危险。我也没什么好的处理方式,自己

namespace backend\modules\feed\controllers;

use yii\web\Controller;

class Article extends Controller
{
    public function actionLoad($communityid)
    {
        // 其它逻辑

        //保存sheet表记录数据
        $data = [];

        if (\Yii::$app->getRequest()->getIsPost() && $model->load(\Yii::$app->request->post())) {

            $redis = \common\core\Redis::getInstance();
            $lock = '';
            if ($redis && $redis->get($lock)) {
                \Yii::$app->getSession()->setFlash('error', '请不要频繁导入文件');
                return $this->render();
            } else {
                $redis->set($lock, 1, 30);
            }

            //保存临时文件,建议保存一份文件下来,以免出错时手足无措,保存后查问题时还有补救的可能
            $inFile = \yii\web\UploadedFile::getInstance($model, 'inFile');
            if (!$inFile->name || (strpos($inFile->type,'excel') === false && strpos($inFile->type, 'sheet') === false)) {
                \Yii::$app->getSession()->setFlash('error', '无法获取上传文件,请重试或者检查文件格式是否正确');
                return $this->render('');
            }
            $fileName = date("Hi-") . $inFile->name;
            // 自定义方法保存文件
            $tempFile = \common\core\File::getInstance()->saveTempFile($fileName, $inFile);

            //排除标题行数
            $number = $_POST['num'];

            //限制文件大小在2M以内,以小值为界
            $size = 2 * 1000 * 1000;
            if (!$inFile->size || $inFile->size > $size) {
                \Yii::$app->getSession()->setFlash('error', '上传文件请控制在2M以内.');
                return $this->render('');
            }

            // 从excel中读取所有信息
            // 这里说明一下,我们踩过的坑,对时间格式,正常的2016-03-03是几乎没有问题的,可就是有人要输入03/03/16等一系列时间格式
            // 请当心读入会出错,如果你也遇到,再说。毕竟总有那么一些人,就是不按常理出牌,谁让我们是程序员,在别人眼里,我们就是来解决
            // 变态问题的,^~^
            $result = \common\extend\phpexcel\Excel::getInstance()->readSheet($tempFile, 0);
            // 针对读完的数组$result 取前8列,排除$number行
            $newResult = \common\extend\phpexcel\Excel::getInstance()->handleSheetArray($result, 8, $number);
            $now = time();
            if (!empty($newResult)) {
                $count = count($newResult);
                $t_mobiles = [];
                foreach ($newResult as $key => $value) {
                    if (empty($value)) {
                        continue;
                    }
                    $index = 0;
                    //顺序为姓名-手机号-类别-有效期-区-楼-单元-门牌号-小区-状态为临时数据-创建时间-更新时间-备注-来源物业-操作员
                    foreach ($value as $k => $val) {
                        // 各种变态判断,注意这里读到的数据是按表格顺序,索引数组保存的就好
                        // 同样保存成索引数组,按数据库表字段保存好,这里推荐使用框架的批量插入batchInsert()来操作,速度会好一些。
                        $data = []; // 
                }
                // 哈哈我们自定义的批量操作,一次处理500条,非常快,就是批量操作不好记录日志,一旦出错,几乎无法查找原因
                $total = $this->generate($data);
            }

            // 其它逻辑
        }

        return $this->render('');
    }
}

5. 总结

简单的导入导出,确实没什么操作,复杂的和大量的导出,需要使用任务日志,命令行等来操作,否则浏览器可能存在超时等原因,影响导出。

2016-03-03 朝阳区定福庄西街