使用Laravel处理密集任务

本文概述

  • 推迟长时间运行的PHP任务, 请不要等待。
  • 用Laravel引导
  • 使用PHP Artisan设置数据库
  • 路由
  • 任务逻辑
  • 导入代码
  • 递归进度通知系统
  • 你的经验是什么?
在处理耗时的资源密集型任务时, 大多数PHP开发人员都倾向于选择” 快速破解途径” 。不要否认!我们都使用过ini_set(‘ max_execution_time’ , HUGE_INT); 之前, 但不一定非要这样。
在今天的教程中, 我演示了如何通过使用Laravel开发解决方案将长时间运行的任务与主要请求流分离开来, 从而(以最小的开发人员精力)改善应用程序的用户体验。通过利用PHP生成在后台运行的单独进程的功能, 主脚本将更快地响应用户操作。因此, 它可以更好地管理用户期望, 而不是让他们等待年龄(无反馈)来完成请求。
推迟长时间运行的PHP任务, 请不要等待。 本教程的基本概念是延迟。承担运行时间过长的任务(根据Internet标准), 而将执行推迟到独立于请求的独立进程中执行。此延迟使我们可以实施一个通知系统, 该系统向用户显示任务的状态(例如, 已导入Y中的X行), 并在作业完成时提醒用户。
我们的教程基于一个我肯定已经遇到过的现实生活场景:从庞大的Excel电子表格中获取数据并将其推送到Web应用程序数据库中。完整的项目可以在我的github上找到。
使用Laravel处理密集任务

文章图片
不要让用户坐下来等待长时间运行的任务。推迟。
用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=*****

【使用Laravel处理密集任务】现在通过执行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脚本。
使用Laravel处理密集任务

文章图片
使用`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]); } } ...

使用Laravel处理密集任务

文章图片
将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

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

    推荐阅读