การจัดการงานเร่งรัดด้วย 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 สำหรับงานเฉพาะนี้ด้วยเหตุผลดังต่อไปนี้:

  1. Laravel มาพร้อมกับ Artisan ซึ่งทำให้การสร้างงานบรรทัดคำสั่งเป็นเรื่องง่าย สำหรับผู้ที่ไม่รู้จัก Artisan มันคืออินเทอร์เฟซบรรทัดคำสั่งที่รวมอยู่ใน Laravel ซึ่งขับเคลื่อนโดยส่วนประกอบ Symfony Console อันทรงพลัง
  2. Laravel มี Eloquent ORM สำหรับการแมปข้อมูล Excel ของเรากับคอลัมน์ตาราง
  3. ได้รับการดูแลอย่างดีและมีเอกสารที่ละเอียดมาก
  4. Laravel พร้อม 100 เปอร์เซ็นต์สำหรับ PHP 7; อันที่จริงกล่อง Homestead นั้นรัน PHP 7 . อยู่แล้ว

ในขณะที่ฉันเลือกใช้ Laravel แนวคิดและโค้ดของบทช่วยสอนนี้สามารถรวมเข้ากับเฟรมเวิร์กใดก็ได้ที่ใช้ส่วนประกอบ Symfony/Process ด้วย (ซึ่งคุณสามารถติดตั้งผ่านผู้แต่งโดยใช้ composer require symfony/process )

ที่เกี่ยวข้อง: ทำไมฉันถึงตัดสินใจโอบกอด Laravel

ในการเริ่มต้น ให้เปิดกล่องพเนจรของคุณโดยใช้ 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 แล้ว

เมื่อใช้แพ็คเกจ Symfony คุณสามารถวางไข่กระบวนการ PHP บนเธรดที่แยกจากกัน โดยไม่ขึ้นกับคำขอ

รหัสนำเข้า

ตอนนี้ เรามาเขียนไฟล์คำสั่ง 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(); } }
ที่เกี่ยวข้อง: จ้าง 3% อันดับแรกของนักพัฒนา Laravel อิสระ

ระบบแจ้งความคืบหน้าแบบเรียกซ้ำ

ไปที่ส่วนหน้าของโครงการของเรา การแจ้งเตือนผู้ใช้ เราสามารถส่งคำขอ 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]); } } ... 

ส่งคำขอ 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

ประสบการณ์ของคุณคืออะไร?

คุณมีข้อเสนอแนะอื่น ๆ สำหรับการปรับปรุงประสิทธิภาพและประสบการณ์ผู้ใช้ในกรณีที่คล้ายกันหรือไม่? ฉันอยากจะรู้ว่าคุณจัดการกับพวกเขาอย่างไร

ที่เกี่ยวข้อง: การ ตรวจสอบสิทธิ์ผู้ใช้แบบเต็มและการควบคุมการเข้าถึง – บทช่วยสอน Laravel Passport, Pt. 1