التعامل مع المهام المكثفة باستخدام Laravel

نشرت: 2022-03-11

عند التعامل مع المهام التي تستغرق وقتًا طويلاً في استخدام الموارد ، فإن معظم مطوري PHP يميلون إلى اختيار "مسار الاختراق السريع". لا تنكر ذلك! لقد استخدمنا جميعًا ini_set('max_execution_time', HUGE_INT); من قبل ، لكن لا يجب أن يكون الأمر على هذا النحو.

في درس اليوم ، أوضحت كيف يمكن تحسين تجربة مستخدم التطبيق (بأقل جهد من المطور) من خلال فصل المهام طويلة الأمد عن تدفق الطلب الرئيسي باستخدام حلول تطوير Laravel. من خلال الاستفادة من قدرة PHP على إنتاج عمليات منفصلة تعمل في الخلفية ، سيستجيب النص الرئيسي بشكل أسرع لإجراءات المستخدم. وبالتالي ، فإنه يدير توقعات المستخدم بشكل أفضل بدلاً من جعلهم ينتظرون للأعمار (بدون ملاحظات) حتى ينتهي الطلب.

تأجيل مهام PHP طويلة المدى ، لا تنتظر.

المفهوم الأساسي لهذا البرنامج التعليمي هو التأجيل ؛ أخذ المهام التي تستغرق وقتًا طويلاً (وفقًا لمعايير الإنترنت) وبدلاً من ذلك تأجيل التنفيذ إلى عملية منفصلة تعمل بشكل مستقل عن الطلب. يسمح لنا هذا التأجيل بتنفيذ نظام إعلام يوضح للمستخدم حالة المهمة (تم استيراد عدد X من الصفوف من Y ، على سبيل المثال) وتنبيه المستخدم عند انتهاء المهمة.

يعتمد برنامجنا التعليمي على سيناريو واقعي أنا متأكد من أنك صادفته من قبل: أخذ البيانات من جداول بيانات Excel الضخمة ودفعها إلى قاعدة بيانات تطبيقات الويب. المشروع الكامل متاح على جيثب الخاص بي.

لا تجعل المستخدمين يجلسون وينتظرون مهمة تشغيل طويلة. تأجيل.

التمهيد باستخدام 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 box يشغل بالفعل PHP 7

بينما اخترت استخدام Laravel ، يمكن دمج مفهوم ورمز هذا البرنامج التعليمي في أي إطار يستخدم أيضًا مكون Symfony/Process (والذي يمكنك تثبيته عبر الملحن باستخدام composer require symfony/process ).

الموضوعات ذات الصلة: لماذا قررت احتضان Laravel

للبدء ، قم بتشغيل صندوق المتشرد الخاص بك بناءً على Homestead (المعيار عند تطوير التطبيقات القائمة على Laravel هذه الأيام). إذا لم يكن لديك إعداد Homestead ، فإن الوثائق الرسمية توفر دليلاً شاملاً خطوة بخطوة.

مع تثبيت Homestead ، ستحتاج إلى تعديل Homestead.yaml قبل بدء صندوق المتشرد الخاص بك من أجل القيام بأمرين: تعيين مجلد التطوير المحلي الخاص بك إلى مجلد داخل الجهاز الظاهري ، وتوفير 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 جديد. (إذا لم تسر الأمور على ما يرام ، فراجع وثائق Hashicorp's Vagrant للحصول على المساعدة.)

 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 .

تحتاج أيضًا إلى إضافة مزود الخدمة والواجهة / الاسم المستعار في ملف config/app.php .

مقدمو الخدمات هم جوهر تطبيق Laravel ؛ يتم تمهيد كل شيء في Laravel من خلال مزود خدمة ، في حين أن الواجهات عبارة عن واجهات ثابتة بسيطة تتيح وصولاً أسهل لمزودي الخدمة هؤلاء. بمعنى آخر ، بدلاً من الوصول إلى قاعدة البيانات (مزود خدمة) باستخدام 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 ، والتي تحتوي على data Excel الفعلية.

إذا كنت تنوي تضمين مؤشر تقدم لتتبع حالة مهمة الاستيراد ، فأنت بحاجة إلى إضافة عمودين آخرين إلى 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, ];

سيؤدي تشغيل الأمر الحرفي إلى إنشاء ملف باسم ImportManager.php في مجلد /app/Console/Commands . سنكتب الكود الخاص بنا كجزء من طريقة handle() .

سيقوم رمز الاستيراد أولاً بتحديث flag_table الإجمالي للصفوف المراد استيرادها ، ثم يقوم بالتكرار خلال كل صف Excel ، وإدراجه في قاعدة البيانات ، وتحديث الحالة.

لتجنب مشاكل نفاد الذاكرة مع ملفات Excel الكبيرة بشكل استثنائي ، من الجيد معالجة أجزاء صغيرة الحجم من مجموعة البيانات المعنية بدلاً من آلاف الصفوف في وقت واحد ؛ اقتراح من شأنه أن يسبب الكثير من المشاكل ، وليس فقط مشاكل الذاكرة.

بالنسبة لهذا المثال المستند إلى Excel ، سنقوم بتكييف طريقة ImportManager::handle() لجلب مجموعة صغيرة فقط من الصفوف حتى يتم استيراد الورقة بأكملها. هذا يساعد في تتبع تقدم المهمة ؛ بعد معالجة كل جزء ، نقوم بتحديث flag_table عن طريق زيادة عمود النطاق الذي تم imported_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 التي تنفذ الأمر 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