Menangani Tugas Intensif dengan Laravel

Diterbitkan: 2022-03-11

Ketika berhadapan dengan tugas-tugas intensif sumber daya yang memakan waktu, sebagian besar pengembang PHP tergoda untuk memilih "rute hack cepat." Jangan menyangkalnya! Kita semua pernah menggunakan ini_set('max_execution_time', HUGE_INT); sebelumnya, tetapi tidak harus seperti ini.

Dalam tutorial hari ini, saya mendemonstrasikan bagaimana pengalaman pengguna aplikasi dapat ditingkatkan (dengan upaya pengembang minimal) dengan memisahkan tugas yang berjalan lama dari aliran permintaan utama menggunakan solusi pengembangan Laravel. Dengan memanfaatkan kemampuan PHP untuk menelurkan proses terpisah yang berjalan di latar belakang, skrip utama akan merespons tindakan pengguna dengan lebih cepat. Dengan demikian, lebih baik mengelola harapan pengguna daripada membuat mereka menunggu lama (tanpa umpan balik) untuk menyelesaikan permintaan.

Tunda tugas PHP yang berjalan lama, jangan menunggu.

Konsep dasar dari tutorial ini adalah penundaan; mengambil tugas yang berjalan terlalu lama (menurut standar Internet) dan sebagai gantinya menunda eksekusi menjadi proses terpisah yang berjalan secara independen dari permintaan. Penangguhan ini memungkinkan kami untuk menerapkan sistem notifikasi yang menunjukkan status tugas kepada pengguna (misalnya, jumlah baris X dari Y telah diimpor) dan memperingatkan pengguna saat pekerjaan selesai.

Tutorial kami didasarkan pada skenario kehidupan nyata yang saya yakin pernah Anda temui sebelumnya: Mengambil data dari spreadsheet Excel yang besar dan memasukkannya ke dalam database aplikasi web. Proyek lengkap tersedia di github saya.

Jangan membuat pengguna Anda duduk dan menunggu tugas yang berjalan lama. Menunda.

Bootstrap dengan Laravel

Kami akan menggunakan "laravel/framework": "5.2.*" dan "maatwebsite/excel": "~2.1.0" ; pembungkus yang bagus untuk paket phpoffice/phpexcel .

Saya memilih untuk menggunakan Laravel untuk tugas khusus ini karena alasan berikut:

  1. Laravel hadir dengan Artisan, yang membuat pembuatan tugas baris perintah menjadi mudah. Bagi mereka yang tidak tahu Artisan, ini adalah antarmuka baris perintah yang disertakan dalam Laravel, didorong oleh komponen Symfony Console yang kuat
  2. Laravel memiliki ORM Eloquent untuk memetakan data Excel kami ke kolom tabel
  3. Itu terpelihara dengan baik dan memiliki dokumentasi yang sangat menyeluruh
  4. Laravel 100 persen siap untuk PHP 7; sebenarnya, kotak Homestead sudah menjalankan PHP 7

Sementara saya memilih untuk menggunakan Laravel, konsep dan kode tutorial ini dapat dimasukkan ke dalam kerangka kerja apa pun yang juga menggunakan komponen Symfony/Process (yang dapat Anda instal melalui composer menggunakan composer require symfony/process ).

Terkait: Mengapa Saya Memutuskan Untuk Merangkul Laravel

Untuk memulai, jalankan kotak gelandangan Anda berdasarkan Homestead (standar saat mengembangkan aplikasi berbasis Laravel akhir-akhir ini). Jika Anda belum menyiapkan Homestead, dokumentasi resmi menyediakan panduan langkah demi langkah yang menyeluruh.

Dengan Homestead terinstal, Anda perlu memodifikasi Homestead.yaml sebelum memulai kotak gelandangan Anda untuk melakukan dua hal: Memetakan folder pengembangan lokal Anda ke folder di dalam mesin virtual Secara otomatis menyediakan NGINX sehingga mengakses URL, seperti http://heavyimporter.app , akan memuat proyek baru Anda.

Inilah tampilan file konfigurasi saya:

 folders: - map: ~/public_html/toptal to: /home/vagrant/toptal sites: - map: heavyimporter.app to: /home/vagrant/toptal/heavyimporter/public databases: - heavyimporter

Sekarang, simpan file dan jalankan vagrant up && vagrant provision , yang memulai VM dan mengonfigurasinya sesuai dengan itu. Jika semuanya berjalan dengan baik, Anda sekarang dapat masuk ke mesin virtual Anda dengan vagrant ssh , dan memulai proyek Laravel baru. (Jika semuanya tidak berjalan dengan baik, lihat dokumentasi Vagrant Hashicorp untuk mendapatkan bantuan.)

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

Setelah membuat proyek, Anda perlu mengatur beberapa variabel konfigurasi dengan mengedit file .env di folder rumah Anda juga harus mengamankan instalasi Anda dengan menjalankan php artisan key:generate .

Inilah bagian yang relevan dari file .env yang terlihat di pihak saya:

 APP_ENV=local APP_DEBUG=true APP_KEY=*** DB_HOST=127.0.0.1 DB_DATABASE=heavyimporter DB_USERNAME=homestead DB_PASSWORD=*****

Sekarang tambahkan paket maatwebsite/excel dengan menjalankan composer require maatwebsite/excel:~2.1.0 .

Anda juga perlu menambahkan penyedia layanan dan fasad/alias di file config/app.php Anda.

Penyedia layanan adalah inti dari aplikasi Laravel; segala sesuatu di Laravel di-bootstrap melalui penyedia layanan, sementara fasad adalah antarmuka statis sederhana yang memungkinkan akses lebih mudah ke penyedia layanan tersebut. Dengan kata lain, daripada mengakses database (penyedia layanan) dengan Illuminate\Database\DatabaseManager … Anda bisa menggunakan DB::staticmethod().

Bagi kami, penyedia layanan kami adalah Maatwebsite\Excel\ExcelServiceProvider dan fasad kami adalah 'Excel'=>'Maatwebsite\Excel\Facades\Excel' .

app.php sekarang akan terlihat seperti ini:

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

Menyiapkan Database dengan PHP Artisan

Mari kita siapkan migrasi database untuk dua tabel. Satu tabel memegang bendera dengan status impor, yang akan kita sebut flag_table , dan tabel yang memiliki data Excel yang sebenarnya, data .

Jika Anda bermaksud menyertakan indikator kemajuan untuk melacak status tugas impor, Anda perlu menambahkan dua kolom lagi ke flag_table : rows_imported dan total_rows . Kedua variabel ini akan memungkinkan kami untuk menghitung dan mengirimkan persentase yang diselesaikan jika kami ingin menunjukkan kemajuan kepada pengguna.

Pertama jalankan php artisan make:migration CreateFlagTable dan php artisan make:migration CreateDataTable untuk benar-benar membuat tabel ini. Kemudian, buka file yang baru dibuat dari database/migrations dan isi metode naik dan turun dengan struktur tabel.

 //...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'); }

Sebelum kita benar-benar menulis kode impor, mari buat model kosong untuk tabel database kita. Ini dicapai melalui Artisan dengan menjalankan dua perintah sederhana: php artisan make:model Flag dan php artisan make:model Data , lalu masuk ke setiap file yang baru dibuat dan menambahkan nama tabel sebagai properti yang dilindungi dari kelas itu, seperti ini:

 //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 }

Rute

Rute adalah mata dari aplikasi Laravel; mereka mengamati permintaan HTTP dan mengarahkannya ke pengontrol yang tepat. Ini dikatakan, pertama, kita memerlukan rute POST yang memberikan tugas mengunggah file Excel kita ke metode import di controller. File akan diunggah di suatu tempat di server sehingga kami dapat mengambilnya nanti ketika kami menjalankan tugas baris perintah. Pastikan untuk menempatkan semua rute Anda (bahkan yang default) ke dalam grup rute middleware web sehingga Anda mendapat manfaat dari status sesi dan perlindungan CSRF. File rute akan terlihat seperti ini:

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

Logika Tugas

Sekarang mari kita alihkan perhatian kita ke pengontrol utama, yang akan menyimpan inti logika kita dalam metode yang bertanggung jawab untuk hal berikut:

  • Membuat validasi yang diperlukan terkait dengan jenis file yang diunggah
  • Mengunggah file ke server dan menambahkan entri ke flag_table (yang akan diperbarui oleh proses baris perintah setelah tugas dijalankan dengan jumlah total baris dan status unggahan saat ini)
  • Memulai proses impor (yang akan memanggil tugas Artisan) lalu kembali untuk memberi tahu pengguna bahwa proses telah dimulai

Ini adalah kode untuk pengontrol utama:

 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')); }

Garis terkait proses di atas melakukan sesuatu yang sangat keren. Mereka menggunakan paket symfony/process untuk menelurkan proses pada utas terpisah, terlepas dari permintaan. Ini berarti bahwa skrip yang sedang berjalan tidak akan menunggu impor selesai, melainkan akan dialihkan dengan pesan kepada pengguna untuk menunggu hingga impor selesai. Dengan cara ini Anda dapat menampilkan pesan status "impor tertunda" kepada pengguna. Atau, Anda dapat mengirim permintaan Ajax setiap X detik untuk memperbarui status.

Hanya menggunakan vanilla PHP, efek yang sama dapat dicapai dengan kode berikut, tetapi tentu saja ini bergantung pada exec , yang dinonaktifkan secara default, dalam banyak kasus.

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

Fungsionalitas yang diberikan symfony/process lebih luas daripada exec sederhana, jadi jika Anda tidak menggunakan paket symphony, Anda dapat men-tweak skrip PHP lebih lanjut setelah melihat kode sumber paket Symphony.

Menggunakan paket Symfony, Anda dapat menelurkan proses PHP pada utas terpisah, terlepas dari permintaan.

Kode Impor

Sekarang mari kita menulis file perintah php artisan yang menangani impor. Mulailah dengan membuat file kelas perintah: php artisan make:console ImportManager , lalu rujuk ke properti $commands di /app/console/Kernel.php , seperti ini:

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

Menjalankan perintah artisan akan membuat file bernama ImportManager.php di folder /app/Console/Commands . Kami akan menulis kode kami sebagai bagian dari metode handle() .

Kode impor kami pertama-tama akan memperbarui flag_table dengan jumlah total baris yang akan diimpor, kemudian akan mengulangi setiap baris Excel, memasukkannya ke dalam database, dan memperbarui status.

Untuk menghindari masalah kehabisan memori dengan file Excel yang sangat besar, ada baiknya untuk memproses potongan berukuran kecil dari kumpulan data masing-masing alih-alih ribuan baris sekaligus; proposisi yang akan menyebabkan banyak masalah, bukan hanya masalah memori.

Untuk contoh berbasis Excel ini, kami akan mengadaptasi metode ImportManager::handle() untuk mengambil hanya sekumpulan kecil baris hingga seluruh lembar telah diimpor. Ini membantu melacak kemajuan tugas; setelah setiap potongan diproses, kami memperbarui flag_table dengan menambahkan kolom imported_rows dengan ukuran potongan.

Catatan: Tidak perlu membuat paginasi karena Maatwebsite\Excel menanganinya untuk Anda seperti yang dijelaskan dalam dokumentasi Laravel.

Seperti inilah tampilan akhir kelas 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(); } }
Terkait: Pekerjakan 3% pengembang Laravel lepas teratas.

Sistem Pemberitahuan Kemajuan Rekursif

Mari beralih ke bagian front-end dari proyek kita, pemberitahuan pengguna. Kami dapat mengirim permintaan Ajax ke rute pelaporan status di aplikasi kami untuk memberi tahu pengguna tentang kemajuan atau mengingatkan mereka saat impor selesai.

Berikut adalah skrip jQuery sederhana yang akan mengirim permintaan ke server hingga menerima pesan yang menyatakan bahwa pekerjaan telah selesai:

 (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);

Kembali ke server, tambahkan rute GET yang disebut status , yang akan memanggil metode yang melaporkan status tugas impor saat ini sebagai selesai, atau jumlah baris yang diimpor dari 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]); } } ... 

Kirim permintaan Ajax ke rute pelaporan status untuk memberi tahu pengguna tentang kemajuan.

Kirim permintaan Ajax ke rute pelaporan status untuk memberi tahu pengguna tentang kemajuan.

Penundaan Pekerjaan Cron

Pendekatan lain, ketika pengambilan data tidak sensitif terhadap waktu, adalah menangani impor di lain waktu saat server tidak digunakan; katakanlah, tengah malam. Ini dapat dilakukan dengan menggunakan tugas cron yang menjalankan perintah php artisan import:excelfile pada interval waktu yang diinginkan.

Di server Ubuntu, sesederhana ini:

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

Apa Pengalaman Anda?

Apakah Anda memiliki saran lain untuk lebih meningkatkan kinerja dan pengalaman pengguna dalam kasus serupa? Saya ingin tahu bagaimana Anda menangani mereka.

Terkait: Otentikasi Pengguna Penuh dan Kontrol Akses – Tutorial Paspor Laravel, Pt. 1