Laravel로 집약적인 작업 처리하기

게시 됨: 2022-03-11

시간이 많이 소요되는 리소스 집약적 작업을 처리할 때 대부분의 PHP 개발자는 "빠른 해킹 경로"를 선택하고 싶은 유혹을 받습니다. 부정하지마! 우리는 모두 ini_set('max_execution_time', HUGE_INT); 전에, 하지만 이 방법으로 할 필요는 없습니다.

오늘의 튜토리얼에서는 Laravel 개발 솔루션을 사용하여 장기 실행 작업을 기본 요청 흐름에서 분리하여 애플리케이션의 사용자 경험을 개선할 수 있는 방법을 보여줍니다(최소한의 개발자 노력으로). 백그라운드에서 실행되는 별도의 프로세스를 생성하는 PHP의 기능을 사용하여 기본 스크립트는 사용자 작업에 더 빠르게 응답합니다. 따라서 요청이 완료될 때까지(피드백 없이) 오래 기다리게 하는 대신 사용자 기대치를 더 잘 관리합니다.

오래 실행되는 PHP 작업을 연기하고 기다리지 마십시오.

이 튜토리얼의 기본 개념은 연기입니다. 인터넷 표준에 따라 너무 오래 실행되는 작업을 대신 요청과 독립적으로 실행되는 별도의 프로세스로 실행을 연기합니다. 이 연기를 통해 사용자에게 작업 상태(예: Y에서 X개의 행을 가져옴)를 표시하고 작업이 완료되면 사용자에게 경고하는 알림 시스템을 구현할 수 있습니다.

우리의 튜토리얼은 이전에 겪었을 것이라고 확신하는 실제 시나리오를 기반으로 합니다. 거대한 Excel 스프레드시트에서 데이터를 가져와 웹 애플리케이션 데이터베이스로 푸시합니다. 전체 프로젝트는 내 github에서 사용할 수 있습니다.

사용자가 오래 실행되는 작업을 앉아서 기다리게 하지 마십시오. 연기하다

라라벨로 부트스트래핑하기

"laravel/framework": "5.2.*""maatwebsite/excel": "~2.1.0" ; phpoffice/phpexcel 패키지를 위한 멋진 래퍼입니다.

저는 다음과 같은 이유로 이 특정 작업에 Laravel을 사용하기로 결정했습니다.

  1. Laravel에는 명령줄 작업을 쉽게 생성할 수 있는 Artisan이 함께 제공됩니다. Artisan을 모르는 사람들을 위해 강력한 Symfony Console 구성 요소로 구동되는 Laravel에 포함된 명령줄 인터페이스입니다.
  2. Laravel에는 Excel 데이터를 테이블 열에 매핑하기 위한 Eloquent ORM이 있습니다.
  3. 잘 관리되고 있으며 매우 철저한 문서가 있습니다.
  4. Laravel은 PHP 7에 대해 100% 준비되어 있습니다. 사실 홈스테드 박스는 이미 PHP 7을 실행하고 있습니다.

Laravel을 사용하기로 선택했지만 이 튜토리얼의 개념과 코드는 composer require symfony/process Symfony/Process 구성 요소를 사용하는 모든 프레임워크에 통합할 수 있습니다.

관련: 내가 Laravel을 채택하기로 결정한 이유

시작하려면 Homestead (요즘 Laravel 기반 애플리케이션을 개발할 때 표준)를 기반으로 방랑자 상자를 시작하십시오. 홈스테드를 설정하지 않았다면 공식 문서에서 철저한 단계별 가이드를 제공합니다.

Homestead가 설치된 상태에서 방랑자 상자를 시작하기 전에 다음 두 가지 작업을 수행하기 위해 Homestead.yaml 을 수정해야 합니다. 로컬 개발 폴더를 가상 머신 내부의 폴더에 매핑 http://heavyimporter.app 와 같은 URL에 액세스할 수 있도록 NGINX를 자동으로 프로비저닝합니다. 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 vagrant ssh 를 사용하여 가상 머신에 로그인하고 새로운 Laravel 프로젝트를 시작할 수 있습니다. (모든 것이 잘 되지 않으면 Hashicorp의 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=*****

이제 composer require maatwebsite/excel:~2.1.0 을 실행하여 maatwebsite/excel 패키지를 추가하세요.

또한 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 이라고 하고 실제 Excel 데이터가 있는 테이블은 data 입니다.

가져오기 작업의 상태를 추적하기 위해 진행 표시기를 포함하려면 flag_table 에 두 개의 열( rows_importedtotal_rows )을 추가해야 합니다. 이 두 변수를 통해 사용자에게 진행 상황을 보여주고 싶은 경우 완료된 비율을 계산하고 전달할 수 있습니다.

먼저 php artisan make:migration CreateFlagTablephp artisan make:migration CreateDataTable 을 실행하여 실제로 이러한 테이블을 생성합니다. 그런 다음 database/migrations 에서 새로 생성된 파일을 열고 up 및 down 메소드를 테이블 구조로 채웁니다.

 //...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 Flagphp 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 요청을 관찰하고 적절한 컨트롤러를 가리킵니다. 즉, 먼저 Excel 파일을 컨트롤러의 import 메서드에 업로드하는 작업을 할당하는 POST 경로가 필요합니다. 파일은 나중에 명령줄 작업을 실행할 때 가져올 수 있도록 서버의 어딘가에 업로드됩니다. 세션 상태 및 CSRF 보호의 이점을 누릴 수 있도록 모든 경로(기본 경로 포함)를 web 미들웨어 경로 그룹에 배치해야 합니다. 경로 파일은 다음과 같습니다.

 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 패키지를 사용하여 요청과 독립적으로 별도의 스레드에서 프로세스를 생성합니다. 즉, 실행 중인 스크립트는 가져오기가 완료될 때까지 기다리지 않고 대신 가져오기가 완료될 때까지 기다리라는 메시지와 함께 리디렉션됩니다. 이렇게 하면 사용자에게 "가져오기 보류" 상태 메시지를 표시할 수 있습니다. 또는 X초마다 Ajax 요청을 보내 상태를 업데이트할 수 있습니다.

바닐라 PHP만 사용하면 다음 코드로 동일한 효과를 얻을 수 있지만 물론 이는 대부분의 경우 기본적으로 비활성화되어 있는 exec 에 의존합니다.

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

symfony/process 가 제공하는 기능은 단순한 exec보다 더 광범위하므로 symphony 패키지를 사용하지 않는 경우 Symphony 패키지 소스 코드를 살펴본 후 PHP 스크립트를 추가로 조정할 수 있습니다.

Symfony 패키지를 사용하면 요청과 별개로 별도의 스레드에서 PHP 프로세스를 생성할 수 있습니다.

가져오기 코드

이제 가져오기를 처리하는 php artisan 명령 파일을 작성해 보겠습니다. php artisan make:console ImportManager 명령 클래스 파일을 생성하여 시작하고 다음과 같이 /app/console/Kernel.php$commands 속성에서 이를 참조합니다.

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

artisan 명령을 실행하면 /app/Console/Commands 폴더에 ImportManager.php 라는 파일이 생성됩니다. 우리는 handle() 메소드의 일부로 코드를 작성할 것입니다.

가져오기 코드는 먼저 가져올 총 행 수로 flag_table 을 업데이트한 다음 각 Excel 행을 반복하고 데이터베이스에 삽입하고 상태를 업데이트합니다.

예외적으로 큰 Excel 파일의 메모리 부족 문제를 방지하려면 한 번에 수천 개의 행을 처리하는 대신 각 데이터 세트의 한입 크기의 청크를 처리하는 것이 좋습니다. 메모리 문제뿐만 아니라 많은 문제를 일으킬 제안입니다.

이 Excel 기반 예제에서는 전체 시트를 가져올 때까지 작은 행 집합만 가져오도록 ImportManager::handle() 메서드를 적용합니다. 이는 작업 진행 상황을 추적하는 데 도움이 됩니다. 각 청크가 처리 imported_rows 후 청크 크기로 import_rows 열을 증가시켜 flag_table 을 업데이트합니다.

참고: Laravel의 문서에 설명된 대로 Maatwebsite\Excel 에서 페이지를 매길 필요가 없습니다.

최종 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(); } }
관련: 프리랜서 Laravel 개발자의 상위 3%를 고용하십시오.

재귀적 진행 알림 시스템

우리 프로젝트의 프론트 엔드 부분인 사용자 알림으로 넘어갑시다. 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);

서버로 돌아가서 가져오기 작업의 현재 상태를 완료 또는 X에서 가져온 행 수로 보고하는 메서드를 호출하는 status 라는 GET 경로를 추가합니다.

 //...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 요청을 보냅니다.

크론 작업 연기

데이터 검색이 시간에 민감하지 않을 때 다른 접근 방식은 나중에 서버가 유휴 상태일 때 가져오기를 처리하는 것입니다. 자정에. 이것은 원하는 시간 간격으로 php artisan import:excelfile 명령을 실행하는 cron 작업을 사용하여 수행할 수 있습니다.

Ubuntu 서버에서는 다음과 같이 간단합니다.

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

당신의 경험은 무엇입니까?

유사한 경우에 성능과 사용자 경험을 더욱 개선하기 위한 다른 제안 사항이 있습니까? 나는 당신이 그들을 어떻게 처리했는지 알고 싶습니다.

관련 항목: 전체 사용자 인증 및 액세스 제어 – A Laravel Passport Tutorial, Pt. 1