作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
丹尼尔·乔治的头像

丹尼尔乔戈

Daniel是Zend认证的PHP工程师,拥有超过10年的经验,在世界各地的多家公司担任首席PHP开发人员.

专业知识

工作经验

12

Share

在处理耗时、资源密集的任务时,大多数 PHP开发人员 想要选择“快速破解”路线吗.“别否认! 我们都用过 报错(max_execution_time, HUGE_INT); 以前,但没必要这样.

在今天的教程中, 我将演示如何通过将长时间运行的任务与主请求流分离来改善应用程序的用户体验(以最少的开发人员的努力) 使用Laravel开发解决方案. 通过使用PHP生成在后台运行的单独进程的能力, 主脚本将更快地响应用户操作. Thereby, 它可以更好地管理用户期望,而不是让他们等待很长时间(没有反馈)才能完成请求.

推迟长时间运行的PHP任务,不要等待.

The base concept of this tutorial is deferment; taking tasks that run for too long (by Internet standards) and instead deferring execution into a separate process that runs 独立于请求. 这种延迟允许我们实现一个通知系统,该系统向用户显示任务的状态(Y中的X行已导入), 例如),并在任务完成时提醒用户.

我们的教程是基于一个真实的场景,我相信你以前遇到过:从巨大的Excel电子表格中提取数据并将其推送到web应用程序数据库中. 完整的项目可以在我的 github.

不要让用户坐在那里等待一个长时间运行的任务. Defer.

用Laravel引导

我们将使用 “laravel /框架”:“5.2.*" and “maatwebsite / excel”:“~ 2.1.0"; a nice wrapper for the phpoffice / phpexcel package.

我选择使用Laravel来完成这个特定的任务,原因如下:

  1. Laravel附带了Artisan,这使得创建命令行任务变得轻而易举. 对于那些不了解Artisan的人, 它是Laravel中包含的命令行界面, 受权势驱使 Symfony控制台 组件
  2. Laravel有Eloquent ORM来将Excel数据映射到表格列
  3. 它维护得很好,并且有非常详尽的文档
  4. Laravel is 100 percent ready for PHP 7; in fact, the 家园 box already runs PHP 7

而我选择Laravel, 的任何框架中都可以合并本教程的概念和代码 Symfony /过程 组件(您可以使用 作曲家需要symfony /过程).

首先,启动你的流浪盒子 家园 (这是目前开发基于Laravel的应用程序的标准). 如果你没有建立家园,那 官方文档 提供全面的分步指南.

安装家园后,您需要进行修改 家园.yaml在启动你的流浪盒子之前,要做两件事: 将本地开发文件夹映射到虚拟机中的文件夹 自动配置NGINX,以便访问URL,例如 http://heavyimporter.app,将加载您的新项目.

下面是我的配置文件:

	folders:
	    - map: ~/public_html/toptal
	      : /home/vagrant/toptal

	sites:
	    -地图:进口大户.app
	      : /home/vagrant/toptal/heavyimporter /公众

	数据库:
	    ——heavyimporter

现在,保存文件并运行 流浪汉了 && 流浪的条款,启动虚拟机并对其进行相应配置. 如果一切顺利,现在可以使用 流浪的ssh,并启动一个新的Laravel项目. (如果一切都不顺利,请参考Hashicorp的报告 流浪的文档 for help.)

cd /home/vagrant/toptal && Composer - create-project -prefer-dist laravel/laravel heavy - importer

创建项目后,您将需要通过编辑 .env 您还应该通过运行 PHP工匠键:生成.

这是相关的部分 .Env文件看起来像在我的端:

APP_ENV =当地
APP_DEBUG = true
APP_KEY = * * *

DB_HOST = 127.0.0.1
DB_DATABASE = heavyimporter
DB_USERNAME =家园
DB_PASSWORD = * * * * *

现在加入 maatwebsite / excel 通过执行 作曲家要求maatwebsite / excel:~2.1.0.

您还需要添加服务提供者和门面/别名 配置/应用程序.php file.

Service providers are the core of a Laravel application; everything in Laravel is bootstrapped through a service provider, 而facade是简单的静态接口,允许更容易地访问这些服务提供者. 换句话说, 而不是访问数据库(一个服务提供商)与照亮\数据库\DatabaseManager…你可以使用DB::staticmethod().

对我们来说,我们的服务提供商是 Maatwebsite \ \ ExcelServiceProvider出类拔萃 我们的门面是 'Excel'=>'Excel Maatwebsite \\Facades\Excel'.

app.php 现在看起来应该像这样:

	//...
	'providers' => [
		//...
		Excel Maatwebsite \ \ ExcelServiceProvider::类
	],
	'aliases' => [
		//...
		'Excel'=>'Excel Maatwebsite \\Facades\Excel'
	]

设置数据库与PHP工匠

让我们为两个表设置数据库迁移. 其中一个表包含一个标记,其中包含我们将调用的导入状态 flag_table,另一个有实际的Excel数据, data.

如果您打算包含进度指示器来跟踪导入任务的状态, 中再添加两列 flag_table: rows_imported and total_rows. 这两个变量将允许我们计算并交付在我们想要向用户显示进度的情况下完成的百分比.

第一次运行 CreateFlagTable的用法和例 and 迁移:CreateDataTable 来创建这些表. 然后,打开新创建的文件 数据库/迁移 用表格结构填充up和down方法.

//...CreateFlagTable.php
类CreateFlagTable扩展迁移
{
    公共功能up()
    {
        Schema::create('flag_table', function (Blueprint $table) {
            $table->increments('id');
            $table->string('file_name')->unique();
            $table->boolean('imported');
            $table->integer('rows_imported');
            $table->integer('total_rows');
            $table->timestamps();
        });
    }

    公共函数down()
    {
        模式:下降(“flag_table”);
    }

//...CreateDataTable.php
类CreateDataTable扩展迁移
{
    公共功能up()
    {
        Schema::create('data', function (Blueprint $table) {
            $table->increments('id');
            $table->string('A', 20);
            $table->string('B', 20);
        });
    }

    公共函数down()
    {
        模式:下降(的数据);
    }

在实际编写导入代码之前,让我们先为数据库表创建空模型. 这是通过运行两个简单的命令通过Artisan实现的: php工匠制作:模型标志 and php工匠制作模型数据, 然后进入每个新创建的文件,并将表名添加为该类的受保护属性,像这样:

	/ /文件:app /国旗.php
	名称空间的应用程序;

	使用说明\雄辩的\ \数据库模型;

	类标志扩展模型
	{
	    Protected $table = 'flag_table';
	    $ Protected = []; //this will give us the ability to mass assign properties to the model
	}
	//...

	/ /文件app /数据.php
	//...
	类数据扩展模型
	{
	    Protected $table = 'data';
	    $ Protected = [];
	    protected $timestamps = false; //disable time stamps for this
	}

Routing

Routes are the eyes of a Laravel application; they observe the HTTP request and point it to the proper 控制器. 话虽如此, first, 我们需要一个POST路由来分配上传Excel文件到 import 方法。. 该文件将上传到服务器上的某个地方,以便稍后在执行命令行任务时获取它. 确保将所有路由(即使是默认路由)都放入 web 中间件路由组,以便从会话状态和CSRF保护中受益. 路由文件看起来像这样:

	Route::group(['middleware' => ['web']], function () {
		/ /主页
	    Route::get('/', ['as'=>'home', function () {
	        返回视图('欢迎');
	    }]);
		
		/ /上传路径
	    Route::post('/import', ['as'=>'import', 'uses'=>'Controller@import']);
	});

任务逻辑

现在让我们把注意力转向主控制器, 它将在一个方法中保持我们逻辑的核心,该方法负责以下内容:

  • 进行与正在上传的文件类型相关的必要验证
  • 将文件上传到服务器并在 flag_table (在任务执行后,命令行进程将更新总行数和上传的当前状态)
  • 启动导入流程(将调用Artisan任务),然后返回,让用户知道流程已经启动

这是主控制器的代码:

	名称空间的应用程序\ Http \控制器;

	//...

	使用Excel Maatwebsite \ \外墙\ Excel;
	使用Symfony\Component\Process\Process作为进程;
	组件使用Symfony \ \流程\ \ ProcessFailedException异常;
	使用说明\ Http \请求;
	使用验证器;
	使用重定向;
	使用配置;
	使用会话;
	use DB;
	使用App \国旗;

	//...

   公共函数导入(Request $ Request)
   {
       $excel_file = $request->file('excel_file');

       $validator = Validator::make($request->all(), [
           'excel_file' => 'required'
       ]);

       $validator->after(function($validator) use ($excel_file) {
           if ($excel_file->guessClientExtension()!= = xlsx) {
               $validator->errors()->add('field', 'File type is invalid - only xlsx is allowed');
           }
       });

       if ($validator->fails()) {
           返回重定向::(路线('家'))
                       ->withErrors($validator);
       }

       try {
           $fname = md5(rand) . '.xlsx';
           $full_path = Config::get('filesystems . conf '.disks.local.root');
           $excel_file->move( $full_path, $fname );
           $flag_table = Flag::firstOrNew(['file_name'=>$fname]);
           $flag_table->imported = 0; //file was not imported
           $flag_table->save();
       }捕获异常(\ $ e) {
           返回重定向::(路线('家'))
                       ->withErrors($e->getMessage()); //don't use this in production ok ?
       }

      //现在是有趣的部分
       $process = new process ('php ../艺人导入:excelfile”);
       $process->start();

       Session::flash('message', 'Hold on tight '). 您的文件正在处理中');
       返回重定向::(路线('家'));
   }

上面与流程相关的行做了一些非常酷的事情. 他们使用 symfony /过程 包在独立于请求的单独线程上生成进程. 这意味着正在运行的脚本将不会等待导入完成,而是将用消息重定向到用户,让其等待导入完成. 通过这种方式,您可以向用户显示“import pending”状态消息. 或者,您可以每隔X秒发送一次Ajax请求来更新状态.

仅使用普通PHP, 使用下面的代码可以实现相同的效果, 当然,这依赖于 exec,在许多情况下,默认情况下是禁用的.

	函数somefunction() {
		exec(“php dosomething.php > /dev/null &");
		//执行其他操作,不要等待上面的操作完成
	}

这些功能 symfony /过程 give比一个简单的执行官更广泛, 所以如果你不使用交响乐包, 之后,您可以进一步调整PHP脚本 Symphony包源代码.

使用Symfony包, 您可以在单独的线程上生成PHP进程, 独立于请求.

进口代码

现在我们来写a php工匠 处理导入的命令文件. 首先创建命令类文件: php工匠 make:console ImportManager,然后在 美元的命令 财产 /应用程序/控制台/内核.php,像这样:

   受保护美元的命令 = [
       命令\ ImportManager::类,
   ];

运行artisan命令将创建一个名为 ImportManager.php in the /应用程序/控制台/命令 folder. 的一部分来编写代码 handle() method.

我们的导入代码将首先更新 flag_table 使用要导入的行的总数, 然后它将遍历每个Excel行, 将其插入数据库中, 并更新状态.

以避免内存不足的问题,特别大的Excel文件, it’s a good idea to process bite-sized chunks of the respective data-set instead of thousands of rows at once; a proposition that would cause lots of problems, 不仅仅是记忆问题.

对于这个基于excel的示例,我们将采用 ImportManager:处理() 方法,以便在导入整个工作表之前仅获取一小部分行. This helps with keeping track of the task progress; after each chunk is processed, we update the flag_table 通过增加 imported_rows 列中使用块的大小.

注意:不需要分页,因为 Excel Maatwebsite \ 为您处理,如 Laravel的文档.

下面是最终的ImportManager类:

名称空间应用\ \控制台命令;

使用说明\ \控制台命令;

use DB;
使用验证器;
使用配置;
使用Excel Maatwebsite \ \外墙\ Excel;

使用App \国旗;

类ImportManager扩展命令
{
   $signature = 'import:excelfile';
   protected $description = '导入一个excel文件';
   $chunkSize = 100;

   公共函数handle()
   {
       $file = Flag::where('imported','=','0')
                   ->orderBy('created_at', 'DESC')
                   ->first();

       $file_path = Config::get('filesystems . path '.disks.local.root') . '/' .$file->file_name;

      //首先计算总行数
       Excel::load($file_path, function($reader) use($file) {
           $objWorksheet = $reader->getActiveSheet();
           $file->total_rows = $objWorksheet->getHighestRow() - 1; //exclude the heading
           $file->save();
       });

      //现在让我们一个接一个地导入行,同时跟踪进度
       Excel::滤光片(块)
           ->selectSheetsByIndex(0)
           ->load($file_path)
           ->chunk($this->chunkSize, function($result) use ($file) {
               $rows = $result->toArray();
              //让我们根据需要在这里做更多的处理(改变单元格中的值)
               $counter = 0;
               foreach ($rows as $k => $row) {
                   foreach ($row as $c => $cell) {
                       $rows[$k][$c] = $cell . ':)'; //altered value :)
                   }
                   DB::table('data')->insert( $rows[$k] );
                   $ counter + +;
               }
               $file = $file->fresh(); //reload from the database
               $file->rows_imported = $file->rows_imported + $counter;
               $file->save();
           }
       );

       $file->imported =1;
       $file->save();
   }
}

递归进度通知系统

让我们转到项目的前端部分,用户通知. 我们可以将Ajax请求发送到应用程序中的状态报告路由,以通知用户进度或在导入完成时提醒他们.

下面是一个简单的jQuery脚本,它将发送请求到服务器,直到它收到一个消息,说明工作已经完成:

	(函数(美元){
       使用严格的;
		statusUpdater() {
			$.ajax({
				“url”:THE_ROUTE_TO_THE_SCRIPT,
			}).(函数(r) {
				if(r.味精= = = '完成'){
				    console.导入已完成. 您的数据现在可以查看了 ... " );
				} else {
					//获取导入行的总数
					console.+ r . log("Status is: ").msg);
					console.日志(“任务尚未完成。”... 别着急,这需要一段时间。;
					statusUpdater ();
				}
			  })
			  .失败(函数(){
				  console.发生了一个错误... 我们可以问Neo到底发生了什么,但他已经吃了红色药丸,正在家里睡觉。”);
			  });
		}
		statusUpdater ();
	}) (jQuery);

回到服务器上,添加一个 GET 路线叫做 status, 哪个将调用报告导入任务当前状态的方法, 或者从X导入的行数.

	//...routes.php
	Route::get('/status', ['as'=>'status', 'uses'=>'Controller@status']);

	//...控制器.php
	...
   公共函数状态(请求$ Request)
   {
       $flag_table = DB::table('flag_table')
                       ->orderBy('created_at', 'desc')
                       ->first();
       如果(空(美元标志)){
           return response()->json(['msg' => 'done']); //nothing to do
       }
       if($flag_table->imported === 1) {
           return response()->json(['msg' => 'done']);
       } else {
           $status = $flag_table->rows_imported . excel行已从总共的 . $flag_table->total_rows;
           return response()->json(['msg' => $status]);
       }
   }
	...

向状态报告路由发送Ajax请求,以通知用户进度.

向状态报告路由发送Ajax请求,以通知用户进度.

Cron工作延期

另一种方法, 当数据检索对时间不敏感时, is to handle the import at a later time when the server is idle; say, 在午夜. 的cron作业可以实现这一点 PHP工匠导入excelfile 命令.

在Ubuntu服务器上,它就像这样简单:

crontab - e
	
#并添加这一行
@midnight CD path/to/project && /usr/bin/PHP工匠导入excelfile >> /my/log/folder/import.log

你的经历是什么??

对于类似情况下进一步提高性能和用户体验,您有其他建议吗? 我很想知道你是怎么对付他们的.

就这一主题咨询作者或专家.
预约电话
丹尼尔·乔治的头像
丹尼尔乔戈

位于 Târgoviște, Dâmbovița罗马尼亚县

成员自 2016年2月4日

作者简介

Daniel是Zend认证的PHP工程师,拥有超过10年的经验,在世界各地的多家公司担任首席PHP开发人员.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

专业知识

工作经验

12

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.