Laravel ile Yoğun Görevleri İşlemek
Yayınlanan: 2022-03-11Zaman alan yoğun kaynak gerektiren görevlerle uğraşırken, çoğu PHP geliştiricisi "hızlı hack yolu"nu seçmeye eğilimlidir. İnkar etme! Hepimiz ini_set('max_execution_time', HUGE_INT);
Daha önce, ama bu şekilde olmak zorunda değil.
Bugünkü eğitimde, Laravel geliştirme çözümlerini kullanarak uzun süren görevleri ana istek akışından ayırarak bir uygulamanın kullanıcı deneyiminin (minimum geliştirici çabasıyla) nasıl geliştirilebileceğini gösteriyorum. PHP'nin arka planda çalışan ayrı işlemler oluşturma yeteneğinden yararlanarak, ana komut dosyası kullanıcı eylemine daha hızlı yanıt verecektir. Böylece, bir isteğin tamamlanması için uzun süre (geri bildirim olmadan) beklemelerini sağlamak yerine, kullanıcı beklentilerini daha iyi yönetir.
Uzun süre çalışan PHP görevlerini erteleyin, beklemeyin.
Bu öğreticinin temel konsepti ertelemedir; (İnternet standartlarına göre) çok uzun süre çalışan görevleri almak ve bunun yerine yürütmeyi istekten bağımsız olarak çalışan ayrı bir sürece ertelemek. Bu erteleme, kullanıcıya görevin durumunu gösteren (örneğin, Y'den X sayıda satır içe aktarılmıştır) ve iş bittiğinde kullanıcıyı uyaran bir bildirim sistemi uygulamamıza olanak tanır.
Eğitimimiz, daha önce karşılaştığınızdan emin olduğum gerçek bir hayat senaryosuna dayanmaktadır: Büyük Excel elektronik tablolarından veri almak ve bir web uygulaması veritabanına göndermek. Projenin tamamı github adresimde mevcut.
Laravel ile Önyükleme
"laravel/framework": "5.2.*"
ve "maatwebsite/excel": "~2.1.0"
; phpoffice/phpexcel
paketi için güzel bir paketleyici.
Aşağıdaki nedenlerden dolayı bu özel görev için Laravel'i kullanmayı seçtim:
- Laravel, komut satırı görevleri oluşturmayı çok kolay hale getiren Artisan ile birlikte gelir. Artisan'ı bilmeyenler için, güçlü Symfony Konsol bileşeni tarafından yönlendirilen, Laravel'de bulunan komut satırı arayüzüdür.
- Laravel, Excel verilerimizi tablo sütunlarına eşlemek için Eloquent ORM'ye sahiptir
- İyi korunur ve çok kapsamlı bir dokümantasyona sahiptir.
- Laravel, PHP 7 için yüzde 100 hazır; aslında, Homestead kutusu zaten PHP 7'yi çalıştırıyor
Ben Laravel'i tercih etsem de, bu öğreticinin konsepti ve kodu, aynı zamanda Symfony/Process
bileşenini kullanan herhangi bir çerçeveye dahil edilebilir (besteci aracılığıyla, composer composer require symfony/process
kullanarak kurabilirsiniz).
Başlamak için, Homestead
(bugünlerde Laravel tabanlı uygulamalar geliştirirken standart olan) tabanlı serseri kutunuzu ateşleyin. Homestead kurulumunu yapmadıysanız, resmi belgeler ayrıntılı bir adım adım kılavuz sağlar.
Homestead kuruluyken, iki şey yapmak için serseri kutunuzu başlatmadan önce Homestead.yaml
değişiklik yapmanız gerekir: Yerel geliştirme klasörünüzü sanal makinenin içindeki bir klasöre eşleyin NGINX'i otomatik olarak sağlayın, böylece bir URL'ye, örneğin http://heavyimporter.app
, yeni projenizi yükleyecektir.
Yapılandırma dosyam şöyle görünüyor:
folders: - map: ~/public_html/toptal to: /home/vagrant/toptal sites: - map: heavyimporter.app to: /home/vagrant/toptal/heavyimporter/public databases: - heavyimporter
Şimdi dosyayı kaydedin ve sanal makineyi başlatan ve buna göre yapılandıran vagrant up && vagrant provision
çalıştırın. Her şey yolunda gittiyse, artık sanal makinenize vagrant ssh
ile giriş yapabilir ve yeni bir Laravel projesi başlatabilirsiniz. (Her şey yolunda gitmediyse, yardım için Hashicorp'un Vagrant belgelerine bakın.)
cd /home/vagrant/toptal && composer create-project --prefer-dist laravel/laravel heavyimporter
Projeyi oluşturduktan sonra, ana klasördeki .env
dosyasını düzenleyerek bazı konfigürasyon değişkenlerini ayarlamanız gerekecektir. Ayrıca kurulumunuzu php artisan key:generate
çalıştırarak da güvenli hale getirmelisiniz.
Benim tarafımda .env dosyasının ilgili bölümleri şöyle görünüyor:
APP_ENV=local APP_DEBUG=true APP_KEY=*** DB_HOST=127.0.0.1 DB_DATABASE=heavyimporter DB_USERNAME=homestead DB_PASSWORD=*****
Şimdi composer require maatwebsite/excel:~2.1.0
yürüterek maatwebsite/excel
paketini ekleyin.
Ayrıca config/app.php
dosyanıza servis sağlayıcıyı ve cephe/takma adı da eklemeniz gerekir.
Servis sağlayıcılar, bir Laravel uygulamasının özüdür; Laravel'deki her şey bir servis sağlayıcı tarafından önyüklenirken, cepheler bu servis sağlayıcılara daha kolay erişim sağlayan basit statik arayüzlerdir. Başka bir deyişle, Illuminate\Database\DatabaseManager ile veritabanına (bir servis sağlayıcı) erişmek yerine … sadece DB::staticmethod() kullanabilirsiniz.
Bizim için hizmet sağlayıcımız Maatwebsite\Excel\ExcelServiceProvider
ve cephemiz 'Excel'=>'Maatwebsite\Excel\Facades\Excel'
.
app.php
şimdi şöyle görünmelidir:
//... 'providers' => [ //... Maatwebsite\Excel\ExcelServiceProvider::class ], 'aliases' => [ //... 'Excel'=>'Maatwebsite\Excel\Facades\Excel' ]
PHP Artisan ile Veritabanını Kurmak
Veritabanı geçişlerimizi iki tablo için ayarlayalım. Bir tablo, flag_table
olarak adlandıracağımız içe aktarma durumunu ve gerçek Excel verilerini, data
içeren bir bayrağı tutar.
İçe aktarma görevinin durumunu izlemek için bir ilerleme göstergesi eklemeyi düşünüyorsanız, flag_table
iki sütun daha eklemeniz gerekir: rows_imported
ve total_rows
. Bu iki değişken, ilerlemeyi kullanıcıya göstermek istediğimizde tamamlanan yüzdeyi hesaplamamızı ve iletmemizi sağlayacaktır.
Bu tabloları gerçekten oluşturmak için php artisan make:migration CreateFlagTable
ve php artisan make:migration CreateDataTable
çalıştırın. Ardından database/migrations
yeni oluşturulan dosyaları açın ve yukarı ve aşağı yöntemlerini tablo yapısıyla doldurun.
//...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'); }
Import kodunu yazmadan önce veritabanı tablolarımız için boş modeller oluşturalım. Bu, Artisan aracılığıyla iki basit komut çalıştırarak elde edilir: php artisan make:model Flag
ve php artisan make:model Data
, ardından yeni oluşturulan her dosyaya girip tablo adını o sınıfın korumalı bir özelliği olarak ekleyerek:
//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 }
yönlendirme
Rotalar, bir Laravel uygulamasının gözleridir; HTTP isteğini gözlemler ve uygun denetleyiciye yönlendirirler. Bununla birlikte, ilk olarak, Excel dosyamızı denetleyicideki import
yöntemine yükleme görevini atayan bir POST yoluna ihtiyacımız var. Dosya, komut satırı görevini yürüttüğümüzde daha sonra alabilmemiz için sunucuda bir yere yüklenecek. Oturum durumu ve CSRF korumasından yararlanabilmeniz için tüm rotalarınızı (varsayılan bile olsa) web
ara katman yazılımı rota grubuna yerleştirdiğinizden emin olun. Rota dosyası şöyle görünecektir:
Route::group(['middleware' => ['web']], function () { //homepage Route::get('/', ['as'=>'home', function () { return view('welcome'); }]); //upload route Route::post('/import', ['as'=>'import', 'uses'=>'Controller@import']); });
Görev Mantığı
Şimdi dikkatimizi, mantığımızın özünü aşağıdakilerden sorumlu bir yöntemde tutacak olan ana denetleyiciye çevirelim:
- Yüklenen dosya türü ile ilgili gerekli doğrulamaların yapılması
- Dosyayı sunucuya yüklemek ve
flag_table
bir giriş eklemek (görev, toplam satır sayısı ve yüklemenin mevcut durumu ile birlikte yürütüldüğünde komut satırı işlemi tarafından güncellenecektir) - İçe aktarma sürecini başlatmak (bu, Zanaatkar görevini çağıracaktır), ardından kullanıcıya sürecin başlatıldığını bildirmek için geri dönün
Bu, ana denetleyicinin kodudur:

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')); }
Yukarıdaki süreçle ilgili satırlar gerçekten harika bir şey yapıyor. İstekten bağımsız olarak ayrı bir iş parçacığında bir işlem oluşturmak için symfony/process
paketini kullanırlar. Bu, çalışan komut dosyasının içe aktarmanın bitmesini beklemeyeceği, bunun yerine kullanıcıya içe aktarma tamamlanana kadar beklemesi için bir mesajla yeniden yönlendirileceği anlamına gelir. Bu şekilde, kullanıcıya "içe aktarma bekleniyor" durum mesajını görüntüleyebilirsiniz. Alternatif olarak, durumu güncellemek için her X saniyede bir Ajax istekleri gönderebilirsiniz.
Yalnızca vanilya PHP kullanarak, aşağıdaki kodla aynı etki elde edilebilir, ancak elbette bu, çoğu durumda varsayılan olarak devre dışı bırakılan exec
dayanır.
function somefunction() { exec("php dosomething.php > /dev/null &"); //do something else without waiting for the above to finish }
symfony/process
sağladığı işlevler basit bir yürütmeden daha kapsamlıdır, bu nedenle symphony paketini kullanmıyorsanız, Symphony paketinin kaynak koduna baktıktan sonra PHP betiğini daha da değiştirebilirsiniz.
İthalat Kodu
Şimdi içe aktarmayı işleyen bir php artisan
komut dosyası yazalım. Komut sınıfı dosyasını oluşturarak başlayın: php artisan make:console ImportManager
, ardından buna /app/console/Kernel.php
içindeki $commands
özelliğinde şu şekilde başvurun:
protected $commands = [ Commands\ImportManager::class, ];
Artisan komutunu çalıştırmak, /app/Console/Commands
klasöründe ImportManager.php
adlı bir dosya oluşturacaktır. handle()
metodunun bir parçası olarak kodumuzu yazacağız.
İçe aktarma kodumuz önce flag_table
içe aktarılacak toplam satır sayısıyla günceller, ardından her Excel satırında yinelenir, veritabanına ekler ve durumu günceller.
Olağanüstü büyük Excel dosyalarıyla ilgili bellek yetersizliği sorunlarından kaçınmak için, aynı anda binlerce satır yerine ilgili veri kümesinin küçük boyutlu parçalarını işlemek iyi bir fikirdir; sadece hafıza sorunlarına değil, birçok soruna da neden olacak bir önerme.
Bu Excel tabanlı örnek için, ImportManager::handle()
yöntemini, tüm sayfa içe aktarılana kadar yalnızca küçük bir satır kümesi alacak şekilde uyarlayacağız. Bu, görevin ilerleme durumunu takip etmeye yardımcı olur; her yığın işlendikten sonra, imported_rows
sütununu flag_table
güncelleriz.
Not: Sayfalandırmaya gerek yoktur çünkü Maatwebsite\Excel
bunu sizin için Laravel'in belgelerinde açıklandığı gibi işler.
İşte son ImportManager sınıfının görünüşü:
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(); } }
Özyinelemeli İlerleme Bildirim Sistemi
Gelelim projemizin ön uç kısmına, kullanıcı bildirimine. Kullanıcıyı ilerleme konusunda bilgilendirmek veya içe aktarma tamamlandığında onları uyarmak için uygulamamızdaki bir durum raporlama yoluna Ajax istekleri gönderebiliriz.
İşte, işin tamamlandığını belirten bir mesaj alana kadar sunucuya istek gönderecek basit bir jQuery betiği:
(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);
Sunucuya geri döndüğünüzde, status
adlı bir GET
yolu ekleyin; bu, içe aktarma görevinin mevcut durumunu tamamlandı olarak veya X'ten içe aktarılan satır sayısını bildiren bir yöntemi çağırır.
//...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 İş Erteleme
Diğer bir yaklaşım, veri alımının zamana duyarlı olmadığı durumlarda, içe aktarmayı daha sonra sunucu boştayken ele almaktır; gece yarısı söyle. Bu, istenen zaman aralığında php artisan import:excelfile
komutunu çalıştıran cron işleri kullanılarak yapılabilir.
Ubuntu sunucularında bu kadar basit:
crontab -e #and add this line @midnight cd path/to/project && /usr/bin/php artisan import:excelfile >> /my/log/folder/import.log
Deneyiminiz nedir?
Benzer durumlarda performansı ve kullanıcı deneyimini daha da iyileştirmek için başka önerileriniz var mı? Onlarla nasıl başa çıktığını öğrenmek için sabırsızlanıyorum.