การจัดการงานเร่งรัดด้วย Laravel
เผยแพร่แล้ว: 2022-03-11เมื่อต้องรับมือกับงานที่ต้องใช้ทรัพยากรมากซึ่งต้องใช้เวลามาก นักพัฒนา PHP ส่วนใหญ่มักจะเลือก "เส้นทางแฮ็กด่วน" อย่าปฏิเสธมัน! เราใช้ ini_set('max_execution_time', HUGE_INT);
เมื่อก่อน แต่ต้องไม่เป็นแบบนี้
ในบทช่วยสอนวันนี้ ฉันสาธิตวิธีปรับปรุงประสบการณ์ผู้ใช้ของแอปพลิเคชัน (โดยใช้ความพยายามของนักพัฒนาเพียงเล็กน้อย) โดยแยกงานที่ใช้เวลานานออกจากโฟลว์คำขอหลักโดยใช้โซลูชันการพัฒนา Laravel ด้วยการใช้ความสามารถของ PHP ในการวางโปรเซสแยกต่างหากที่ทำงานอยู่เบื้องหลัง สคริปต์หลักจะตอบสนองต่อการกระทำของผู้ใช้ได้เร็วขึ้น ดังนั้นจึงจัดการความคาดหวังของผู้ใช้ได้ดีกว่าแทนที่จะทำให้พวกเขารอนาน (โดยไม่มีคำติชม) เพื่อให้คำขอเสร็จสิ้น
เลื่อนงาน PHP ที่รันเป็นเวลานาน อย่ารอช้า
แนวคิดพื้นฐานของบทช่วยสอนนี้คือ การเลื่อนเวลา การรับงานที่ทำงานนานเกินไป (ตามมาตรฐานอินเทอร์เน็ต) และแทนที่จะเลื่อนการดำเนินการไปเป็นกระบวนการแยกต่างหากที่ทำงานโดยอิสระจากคำขอ การเลื่อนเวลานี้ทำให้เราสามารถใช้ระบบการแจ้งเตือนที่แสดงสถานะของงานให้ผู้ใช้เห็น (เช่น มีการนำเข้าจำนวนแถว X จาก Y) และแจ้งเตือนผู้ใช้เมื่องานเสร็จสิ้น
บทช่วยสอนของเราอิงจากสถานการณ์จริงในชีวิต ซึ่งฉันแน่ใจว่าคุณเคยเจอมาก่อน: การนำข้อมูลจากสเปรดชีต Excel ขนาดใหญ่มาใส่ในฐานข้อมูลแอปพลิเคชันบนเว็บ โครงการเต็มมีอยู่ใน GitHub ของฉัน
Bootstrapping ด้วย Laravel
เราจะใช้ "laravel/framework": "5.2.*"
และ "maatwebsite/excel": "~2.1.0"
; เสื้อคลุมที่ดีสำหรับ phpoffice/phpexcel
ฉันเลือกใช้ Laravel สำหรับงานเฉพาะนี้ด้วยเหตุผลดังต่อไปนี้:
- Laravel มาพร้อมกับ Artisan ซึ่งทำให้การสร้างงานบรรทัดคำสั่งเป็นเรื่องง่าย สำหรับผู้ที่ไม่รู้จัก Artisan มันคืออินเทอร์เฟซบรรทัดคำสั่งที่รวมอยู่ใน Laravel ซึ่งขับเคลื่อนโดยส่วนประกอบ Symfony Console อันทรงพลัง
- Laravel มี Eloquent ORM สำหรับการแมปข้อมูล Excel ของเรากับคอลัมน์ตาราง
- ได้รับการดูแลอย่างดีและมีเอกสารที่ละเอียดมาก
- Laravel พร้อม 100 เปอร์เซ็นต์สำหรับ PHP 7; อันที่จริงกล่อง Homestead นั้นรัน PHP 7 . อยู่แล้ว
ในขณะที่ฉันเลือกใช้ Laravel แนวคิดและโค้ดของบทช่วยสอนนี้สามารถรวมเข้ากับเฟรมเวิร์กใดก็ได้ที่ใช้ส่วนประกอบ Symfony/Process
ด้วย (ซึ่งคุณสามารถติดตั้งผ่านผู้แต่งโดยใช้ composer require symfony/process
)
ในการเริ่มต้น ให้เปิดกล่องพเนจรของคุณโดยใช้ Homestead
(มาตรฐานเมื่อพัฒนาแอปพลิเคชันที่ใช้ Laravel ในปัจจุบัน) หากคุณไม่ได้ตั้งค่า Homestead ไว้ เอกสารอย่างเป็นทางการจะให้คำแนะนำทีละขั้นตอนอย่างละเอียด
เมื่อติดตั้ง Homestead แล้ว คุณจะต้องแก้ไข Homestead.yaml
ก่อนเริ่มกล่อง Vagrant เพื่อทำสองสิ่ง: แมปโฟลเดอร์การพัฒนาในพื้นที่ของคุณกับโฟลเดอร์ภายในเครื่องเสมือน จัดเตรียม NGINX โดยอัตโนมัติเพื่อให้เข้าถึง URL เช่น http://heavyimporter.app
จะโหลดโครงการใหม่ของคุณ
นี่คือลักษณะของไฟล์กำหนดค่าของฉัน:
folders: - map: ~/public_html/toptal to: /home/vagrant/toptal sites: - map: heavyimporter.app to: /home/vagrant/toptal/heavyimporter/public databases: - heavyimporter
ตอนนี้ให้บันทึกไฟล์และเรียกใช้ vagrant up && vagrant provision
ซึ่งเริ่มต้น VM และกำหนดค่าตามนั้น หากทุกอย่างเป็นไปด้วยดี ตอนนี้คุณสามารถเข้าสู่ระบบเครื่องเสมือนด้วย vagrant ssh
และเริ่มโครงการ Laravel ใหม่ (หากทุกอย่างไม่เป็นไปด้วยดี โปรดดูเอกสาร Vagrant ของ Hashicorp เพื่อขอความช่วยเหลือ)
cd /home/vagrant/toptal && 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=*****
ตอนนี้เพิ่ม maatwebsite/excel
โดยดำเนินการ composer require maatwebsite/excel:~2.1.0
คุณต้องเพิ่มผู้ให้บริการและ Facade/alias ในไฟล์ config/app.php
ของคุณด้วย
ผู้ให้บริการเป็นแกนหลักของแอปพลิเคชัน Laravel ทุกอย่างใน Laravel ถูกบูตผ่านผู้ให้บริการ ในขณะที่ส่วนหน้าเป็นอินเทอร์เฟซแบบสแตติกอย่างง่ายที่ช่วยให้เข้าถึงผู้ให้บริการเหล่านั้นได้ง่ายขึ้น กล่าวอีกนัยหนึ่ง แทนที่จะเข้าถึงฐานข้อมูล (ผู้ให้บริการ) ด้วย Illuminate\Database\DatabaseManager … คุณสามารถใช้ DB::staticmethod() ได้
สำหรับเรา ผู้ให้บริการของเราคือ Maatwebsite\Excel\ExcelServiceProvider
และ Facades ของเราคือ '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
เพื่อสร้างตารางเหล่านี้จริงๆ จากนั้น เปิดไฟล์ที่สร้างขึ้นใหม่จาก database/migrations
และเติมวิธีการขึ้นและลงด้วยโครงสร้างตาราง
//...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'=>'Controller@import']); });
ตรรกะของงาน
ตอนนี้ ให้เราหันความสนใจไปที่ตัวควบคุมหลัก ซึ่งจะยึดแกนหลักของตรรกะของเราไว้ในวิธีการที่รับผิดชอบดังต่อไปนี้:
- ทำการตรวจสอบที่จำเป็นที่เกี่ยวข้องกับประเภทไฟล์ที่กำลังอัปโหลด
- การอัปโหลดไฟล์ไปยังเซิร์ฟเวอร์และเพิ่มรายการลงใน
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
เพื่อวางกระบวนการบนเธรดที่แยกจากกัน โดยไม่ขึ้นกับคำขอ ซึ่งหมายความว่าสคริปต์ที่ทำงานอยู่จะไม่รอให้การนำเข้าเสร็จสิ้น แต่จะเปลี่ยนเส้นทางพร้อมข้อความไปยังผู้ใช้เพื่อรอจนกว่าการนำเข้าจะเสร็จสิ้น วิธีนี้ทำให้คุณสามารถแสดงข้อความสถานะ "รอการนำเข้า" ให้กับผู้ใช้ได้ หรือคุณสามารถส่งคำขอ Ajax ทุกๆ X วินาทีเพื่ออัปเดตสถานะ

เมื่อใช้ vanilla PHP เท่านั้น ผลลัพธ์แบบเดียวกันสามารถทำได้ด้วยโค้ดต่อไปนี้ แต่แน่นอนว่าต้องอาศัย exec
ซึ่งถูกปิดใช้งานโดยค่าเริ่มต้น ในหลายกรณี
function somefunction() { exec("php dosomething.php > /dev/null &"); //do something else without waiting for the above to finish }
ฟังก์ชันที่ symfony/process
มอบให้นั้นครอบคลุมมากกว่า exec ธรรมดา ดังนั้น หากคุณไม่ได้ใช้แพ็คเกจ symphony คุณสามารถปรับแต่งสคริปต์ PHP เพิ่มเติมได้หลังจากดูซอร์สโค้ดของแพ็คเกจ Symphony แล้ว
รหัสนำเข้า
ตอนนี้ เรามาเขียนไฟล์คำสั่ง php artisan
ที่จัดการการนำเข้ากัน เริ่มต้นด้วยการสร้างไฟล์คลาสคำสั่ง: php artisan make:console ImportManager
จากนั้นอ้างอิงในคุณสมบัติ $commands
ใน /app/console/Kernel.php
ดังนี้:
protected $commands = [ Commands\ImportManager::class, ];
การรันคำสั่ง artisan จะสร้างไฟล์ชื่อ ImportManager.php
ในโฟลเดอร์ /app/Console/Commands
เราจะเขียนโค้ดของเราเป็นส่วนหนึ่งของเมธอด handle()
ขั้นแรก รหัสนำเข้าของเราจะอัปเดต flag_table
ด้วยจำนวนแถวทั้งหมดที่จะนำเข้า จากนั้นจะวนซ้ำในแต่ละแถวของ Excel แทรกลงในฐานข้อมูล และอัปเดตสถานะ
เพื่อหลีกเลี่ยงปัญหาหน่วยความจำไม่เพียงพอกับไฟล์ Excel ที่มีขนาดใหญ่เป็นพิเศษ คุณควรประมวลผลกลุ่มข้อมูลขนาดพอดีคำของชุดข้อมูลที่เกี่ยวข้อง แทนที่จะประมวลผลเป็นพันแถวในคราวเดียว ข้อเสนอที่จะทำให้เกิดปัญหามากมาย ไม่ใช่แค่ปัญหาหน่วยความจำ
สำหรับตัวอย่างที่ใช้ Excel นี้ เราจะปรับเมธอด ImportManager::handle()
เพื่อดึงข้อมูลเฉพาะแถวเล็กๆ จนกว่าจะนำเข้าทั้งแผ่น ซึ่งจะช่วยในการติดตามความคืบหน้าของงาน หลังจากประมวลผลแต่ละอันแล้ว เราจะอัปเดต flag_table
โดยเพิ่มคอลัมน์ที่ imported_rows
_rows ด้วยขนาดของอันนั้น
หมายเหตุ: ไม่จำเป็นต้องใส่เลขหน้าเพราะ 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(); } }
ระบบแจ้งความคืบหน้าแบบเรียกซ้ำ
ไปที่ส่วนหน้าของโครงการของเรา การแจ้งเตือนผู้ใช้ เราสามารถส่งคำขอ 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);
กลับไปที่เซิร์ฟเวอร์ เพิ่มเส้นทาง GET
ที่เรียกว่า status
ซึ่งจะเรียกวิธีการที่รายงานสถานะปัจจุบันของงานนำเข้าว่าเสร็จสิ้นแล้ว หรือจำนวนแถวที่นำเข้าจาก X
//...routes.php Route::get('/status', ['as'=>'status', 'uses'=>'Controller@status']); //...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]); } } ...
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
ประสบการณ์ของคุณคืออะไร?
คุณมีข้อเสนอแนะอื่น ๆ สำหรับการปรับปรุงประสิทธิภาพและประสบการณ์ผู้ใช้ในกรณีที่คล้ายกันหรือไม่? ฉันอยากจะรู้ว่าคุณจัดการกับพวกเขาอย่างไร