个性化阅读
专注于IT技术分析

使用Laravel处理密集任务

本文概述

在处理耗时的资源密集型任务时, 大多数PHP开发人员都倾向于选择”快速破解途径”。不要否认!我们都使用过ini_set(‘max_execution_time’, HUGE_INT);之前, 但不一定非要这样。

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

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

本教程的基本概念是延迟。承担运行时间过长的任务(根据Internet标准), 而将执行推迟到独立于请求的独立进程中执行。此延迟使我们可以实施一个通知系统, 该系统向用户显示任务的状态(例如, 已导入Y中的X行), 并在作业完成时提醒用户。

我们的教程基于一个我肯定已经遇到过的现实生活场景:从庞大的Excel电子表格中获取数据并将其推送到Web应用程序数据库中。完整的项目可以在我的github上找到。

使用Laravel推迟长时间运行的PHP任务。

不要让用户坐下来等待长时间运行的任务。推迟。

用Laravel引导

我们将使用” laravel / framework”:” 5.2。*”和” maatwebsite / excel”:”〜2.1.0″; phpoffice / phpexcel软件包的不错的包装。

我出于以下原因选择将Laravel用于此特定任务:

  1. Laravel随附有Artisan, 这使创建命令行任务变得轻而易举。对于那些不了解Artisan的人来说, 它是Laravel中包含的命令行界面, 由强大的Symfony Console组件驱动
  2. Laravel具有Eloquent ORM, 用于将我们的Excel数据映射到表列
  3. 它维护得很好并且有非常详尽的文档
  4. Laravel已为PHP 7做好了100%的准备;实际上, Homestead盒已经运行了PHP 7

当我选择使用Laravel时, 本教程的概念和代码可以合并到任何使用Symfony / Process组件的框架中(你可以使用composer require symfony / process通过composer安装该组件)。

相关:为什么我决定拥抱Laravel

首先, 基于Homestead(这是近来开发基于Laravel的应用程序的标准)启动你的无所事事的盒子。如果你没有设置Homestead, 则官方文档会提供详尽的分步指南。

安装了Homestead后, 你需要在启动游民游箱之前修改Homestead.yaml, 以执行以下两项操作:将本地开发文件夹映射到虚拟机内部的文件夹自动配置NGINX, 以便访问URL, 例如http:// heavyimporter.app, 将加载你的新项目。

我的配置文件如下所示:

	folders:
	    - map: ~/public_html/srcmini
	      to: /home/vagrant/srcmini

	sites:
	    - map: heavyimporter.app
	      to: /home/vagrant/srcmini/heavyimporter/public

	databases:
	    - heavyimporter

现在, 保存文件并运行” vagrant up && vagrant设置”, 这将启动VM并进行相应的配置。如果一切顺利, 你现在可以使用vagrant ssh登录到虚拟机, 并启动一个新的Laravel项目。 (如果一切都不顺利, 请参阅Hashicorp的Vagrant文​​档以获取帮助。)

cd /home/vagrant/srcmini && composer create-project --prefer-dist laravel/laravel heavyimporter

创建项目后, 你将需要通过编辑主文件夹中的.env文件来设置一些配置变量。你还应该通过运行php artisan key:generate保护安装。

这是我末端的.env文件的相关部分:

APP_ENV=local
APP_DEBUG=true
APP_KEY=***

DB_HOST=127.0.0.1
DB_DATABASE=heavyimporter
DB_USERNAME=homestead
DB_PASSWORD=*****

现在通过执行composer require maatwebsite / excel:〜2.1.0添加maatwebsite / excel软件包。

你还需要在config / app.php文件中添加服务提供商和Facade / alias。

服务提供者是Laravel应用程序的核心。 Laravel中的所有内容都通过服务提供商进行引导, 而Facades是简单的静态接口, 可以更轻松地访问那些服务提供商。换句话说, 除了使用Illuminate \ Database \ DatabaseManager来访问数据库(服务提供商)外, 你还可以使用DB :: staticmethod()。

对于我们来说, 我们的服务提供商是Maatwebsite \ Excel \ ExcelServiceProvider, 而我们的门面是’Excel’=>’Maatwebsite \ Excel \ Facades \ Excel’。

app.php现在应如下所示:

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

使用PHP Artisan设置数据库

让我们为两个表设置数据库迁移。一个表包含一个带有导入状态的标志, 我们将其称为flag_table, 另一个表具有实际的Excel数据data。

如果打算包括进度指示器以跟踪导入任务的状态, 则需要在flag_table中再添加两列:rows_imported和total_rows。这两个变量将使我们能够计算并向用户显示进度时完成的百分比。

首先运行php artisan make:migration CreateFlagTable和php artisan make:migration CreateDataTable来实际创建这些表。然后, 从数据库/迁移中打开新创建的文件, 并使用表结构填充上下方法。

//...CreateFlagTable.php
class CreateFlagTable extends Migration
{
    public function 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();
        });
    }

    public function down()
    {
        Schema::drop('flag_table');
    }

//...CreateDataTable.php
class CreateDataTable extends Migration
{
    public function up()
    {
        Schema::create('data', function (Blueprint $table) {
            $table->increments('id');
            $table->string('A', 20);
            $table->string('B', 20);
        });
    }

    public function down()
    {
        Schema::drop('data');
    }

在实际编写导入代码之前, 让我们为数据库表创建空模型。这可以通过Artisan通过运行两个简单命令来实现:php artisan make:model Flag和php artisan make:model Data, 然后进入每个新创建的文件, 并将表名添加为该类的受保护属性, 如下所示:

	//file: app/Flag.php
	namespace App;

	use Illuminate\Database\Eloquent\Model;

	class Flag extends Model
	{
	    protected $table = 'flag_table';
	    protected $guarded = []; //this will give us the ability to mass assign properties to the model
	}
	//...

	//file app/Data.php
	//...
	class Data extends Model
	{
	    protected $table = 'data';
	    protected $guarded = [];
	    protected $timestamps = false; //disable time stamps for this
	}

路由

路线是Laravel应用程序的关注点;他们观察HTTP请求并将其指向适当的控制器。话虽如此, 首先, 我们需要一个POST路由, 该路由将将Excel文件上传到控制器中的import方法的任务分配了。该文件将被上传到服务器上的某个位置, 以便我们稍后在执行命令行任务时可以将其获取。确保将所有路由(甚至是默认路由)放入Web中间件路由组中, 以便从会话状态和CSRF保护中受益。路由文件将如下所示:

	Route::group(['middleware' => ['web']], function () {
		//homepage
	    Route::get('/', ['as'=>'home', function () {
	        return view('welcome');
	    }]);
		
		//upload route
	    Route::post('/import', ['as'=>'import', 'uses'=>'[email protected]']);
	});

任务逻辑

现在, 我们将注意力转移到主控制器上, 该主控制器将采用一种负责以下工作的方法来控制逻辑的核心:

  • 对要上传的文件类型进行必要的验证
  • 将文件上载到服务器, 并在flag_table中添加一个条目(一旦任务执行并显示行总数和上载的当前状态, 命令行进程将更新该条目)
  • 开始导入过程(将调用Artisan任务), 然后返回以告知用户该过程已启动。

这是主控制器的代码:

	namespace App\Http\Controllers;

	//...

	use Maatwebsite\Excel\Facades\Excel;
	use Symfony\Component\Process\Process as Process;
	use Symfony\Component\Process\Exception\ProcessFailedException;
	use Illuminate\Http\Request;
	use Validator;
	use Redirect;
	use Config;
	use Session;
	use DB;
	use App\Flag;

	//...

   public function import(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()) {
           return Redirect::to(route('home'))
                       ->withErrors($validator);
       }

       try {
           $fname = md5(rand()) . '.xlsx';
           $full_path = Config::get('filesystems.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();
       }catch(\Exception $e){
           return Redirect::to(route('home'))
                       ->withErrors($e->getMessage()); //don't use this in production ok ?
       }

      //and now the interesting part
       $process = new Process('php ../artisan import:excelfile');
       $process->start();

       Session::flash('message', 'Hold on tight. Your file is being processed');
       return Redirect::to(route('home'));
   }

上面与过程相关的行确实很酷。他们使用symfony / process包在独立于请求的线程上生成进程。这意味着正在运行的脚本不会等待导入完成, 而是会重定向并向用户显示一条消息, 以等待导入完成。这样, 你可以向用户显示”导入挂起”状态消息。或者, 你可以每X秒发送一次Ajax请求以更新状态。

仅使用原始PHP, 可以通过以下代码实现相同的效果, 但是当然, 这取决于exec, 在许多情况下, 默认情况下禁用了exec。

	function somefunction() {
		exec("php dosomething.php > /dev/null &");
		//do something else without waiting for the above to finish
	}

symfony / process提供的功能比简单的exec更为广泛, 因此, 如果你不使用symphony软件包, 则可以在查看Symphony软件包源代码之后进一步调整PHP脚本。

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

使用`symfony / process`包, 你可以独立于请求而在单独的线程上生成PHP进程。

鸣叫

导入代码

现在, 让我们编写一个处理导入的php artisan命令文件。首先创建命令类文件:php artisan make:console ImportManager, 然后在/app/console/Kernel.php的$ commands属性中引用它, 如下所示:

   protected $commands = [
       Commands\ImportManager::class, ];

运行artisan命令将在/ app / Console / Commands文件夹中创建一个名为ImportManager.php的文件。我们将把代码编写为handle()方法的一部分。

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

为避免Excel文件过大而导致内存不足的问题, 一个好方法是处理各个数据集的小块而不是一次处理数千行;一个会引起很多问题的命题, 而不仅仅是内存问题。

对于此基于Excel的示例, 我们将对ImportManager :: handle()方法进行调整, 使其仅获取一小组行, 直到导入了整个图纸为止。这有助于跟踪任务进度;在处理完每个块之后, 我们通过增加import_rows列的块大小来更新flag_table。

注意:不需要分页, 因为Maatwebsite \ Excel可以按照Laravel文档中的说明为你处理。

最终的ImportManager类如下所示:

namespace App\Console\Commands;

use Illuminate\Console\Command;

use DB;
use Validator;
use Config;
use Maatwebsite\Excel\Facades\Excel;

use App\Flag;

class ImportManager extends Command
{
   protected $signature = 'import:excelfile';
   protected $description = 'This imports an excel file';
   protected $chunkSize = 100;

   public function handle()
   {
       $file = Flag::where('imported', '=', '0')
                   ->orderBy('created_at', 'DESC')
                   ->first();

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

      // let's first count the total number of rows
       Excel::load($file_path, function($reader) use($file) {
           $objWorksheet = $reader->getActiveSheet();
           $file->total_rows = $objWorksheet->getHighestRow() - 1; //exclude the heading
           $file->save();
       });

      //now let's import the rows, one by one while keeping track of the progress
       Excel::filter('chunk')
           ->selectSheetsByIndex(0)
           ->load($file_path)
           ->chunk($this->chunkSize, function($result) use ($file) {
               $rows = $result->toArray();
              //let's do more processing (change values in cells) here as needed
               $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();
   }
}

相关:雇用自由职业Laravel开发人员的前3%。

递归进度通知系统

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

这是一个简单的jQuery脚本, 它将向服务器发送请求, 直到它收到一条消息, 指出作业已完成:

	(function($){
       'use strict';
		function statusUpdater() {
			$.ajax({
				'url': THE_ROUTE_TO_THE_SCRIPT, }).done(function(r) {
				if(r.msg==='done') {
				    console.log( "The import is completed. Your data is now available for viewing ... " );
				} else {
					//get the total number of imported rows
					console.log("Status is: " + r.msg);
					console.log( "The job is not yet done... Hold your horses, it takes a while :)" );
					statusUpdater();
				}
			  })
			  .fail(function() {
				  console.log( "An error has occurred... We could ask Neo about what happened, but he's taken the red pill and he's at home sleeping" );
			  });
		}
		statusUpdater();
	})(jQuery);

回到服务器上, 添加一个名为status的GET路由, 该路由将调用一种方法, 该方法报告导入任务的当前状态为完成或从X导入的行数。

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

	//...controller.php
	...
   public function status(Request $request)
   {
       $flag_table = DB::table('flag_table')
                       ->orderBy('created_at', 'desc')
                       ->first();
       if(empty($flag)) {
           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 rows have been imported out of a total of ' . $flag_table->total_rows;
           return response()->json(['msg' => $status]);
       }
   }
	...
将Ajax请求发送到状态报告路由,以通知用户进度。

将Ajax请求发送到状态报告路由, 以通知用户进度。

Cron工作延缓

当数据检索对时间不敏感时, 另一种方法是在服务器空闲时稍后处理导入。例如, 在午夜。可以使用cron作业在所需的时间间隔执行php artisan import:excelfile命令来完成。

在Ubuntu服务器上, 它很简单:

crontab -e
	
#and add this line
@midnight cd path/to/project && /usr/bin/php artisan import:excelfile >> /my/log/folder/import.log

你的经验是什么?

在类似情况下, 你还有其他建议来进一步改善性能和用户体验吗?我很想知道你是如何处理他们的。

赞(0)
未经允许不得转载:srcmini » 使用Laravel处理密集任务

评论 抢沙发

评论前必须登录!