I have a Laravel v.10 application integrated with MaatWebsite library to help in Excel spreadsheet files. While running the application on a Linux VM on Azure cloud, the background processing works well. But when I deployed it on Azure App Service, I observed that the library can only process a maximum of 2 records on the spreadsheet. I am at a loss as to why the same application with the same library is not able to process as many records in Azure App Service as it used to process in an Azure VM.
Below is the code for processing the spreadsheet
<?php
namespace App\Imports;
use App\Helpers\ExcelFileUploadColumnValidatorHelper;
use App\Helpers\Generators;
use App\Mail\InvalidCourseCodeMail;
use App\Mail\SponsoredTraineeMail;
use App\Models\Course;
use App\Models\Sponsor;
use App\Models\SponsoredUser;
use App\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Illuminate\Support\Facades\Validator;
class ExcelSponsorImportTrainees implements ToCollection, WithHeadingRow
{
private $batchcode;
private $sponsor;
private $authUser;
private $invalidCourseCodes = [];
private $existCourseCodes = [];
private $registeredSponsors = [];
private $messages;
private $expectedHeaders = [
'first_name', 'last_name', 'email', 'phone', 'dob', 'gender',
'course_code', 'sponsor_code', 'centre_code', 'session_code', 'semester_code'
];
public function __construct($sponsor, $batchcode)
{
$this->sponsor = $sponsor;
$this->batchcode = $batchcode;
$this->authUser = Auth::user();
}
public function collection(Collection $rows)
{
if ($rows === null || $rows->isEmpty()) {
// Handle the null or empty rows scenario
throw new \Exception('No data found in the uploaded Excel file.');
}
ExcelFileUploadColumnValidatorHelper::validateHeaders($rows->first()->toArray(),$this->expectedHeaders);
DB::beginTransaction();
try {
$this->validateHeaders($rows->first()->keys());
foreach ($rows as $key => $row) {
$validation = $this->validateRow($row->toArray(), $key);
if ($validation->fails()) {
$this->messages .= "Validation errors occurred at Row $key: " . implode(', ', $validation->errors()->all()) . "\n";
continue; // Skip invalid row
}
if (!empty($row['course_code'])) {
$request = $this->mapRowToRequest($row);
$course_code = trim($request->course_code);
$user = $this->findOrCreateUser($request);
if (!$this->isUserAlreadySponsored($user, $request->course_code, $this->sponsor)) {
$course = Course::where(\DB::raw('LOWER(code)'), 'LIKE', '%' . strtolower(trim($request->course_code)) . '%')->first();
if ($course) {
$this->recordSponsoredUser($user, $course, $request, $this->sponsor);
} else {
$this->invalidCourseCodes[] = $course_code;
$this->messages .= 'Invalid course code: ' . $course_code . "\n";
}
}
}
}
$this->sendSummaryEmail();
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
$this->messages .= $e->getMessage() . "\n";
$this->handleFailure($e);
}
return $this->registeredSponsors;
}
private function validateHeaders($headers)
{
$missingHeaders = array_diff($this->expectedHeaders, $headers->toArray());
if (!empty($missingHeaders)) {
throw new \Exception('Missing headers: ' . implode(', ', $missingHeaders));
}
}
private function validateRow(array $row, $rowIndex)
{
return Validator::make($row, [
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'email' => 'required|email|max:255',
'phone' => 'required|max:15', // Adjust max length as needed
'dob' => 'required|date|before:today',
'gender' => 'required|in:Male,Female,other', // Adjust as per your gender options
'course_code' => 'required',
'sponsor_code' => 'required',
'centre_code' => 'required',
'session_code' => 'required',
'semester_code' => 'required',
], [
'email.email' => 'The email address must be valid.',
'dob.before' => 'The date of birth must be a date before today.',
'gender.in' => 'The gender must be one of the following: male, female, other.',
]);
}
private function mapRowToRequest($row)
{
$request = new \StdClass;
$request->first_name = $row['first_name'];
$request->last_name = $row['last_name'];
$request->email = $row['email'];
$request->phone = $row['phone'];
$request->dob = $row['dob'];
$request->gender = $row['gender'];
$request->course_code = $row['course_code'];
$request->sponsor_code = $row['sponsor_code'];
$request->centre_code = $row['centre_code'];
$request->session_code = $row['session_code'];
$request->semester_code = $row['semester_code'];
return $request;
}
private function findOrCreateUser($request)
{
$user = User::where('email', $request->email)->first();
if (!$user) {
$user = $this->createNewUser($request);
}
return $user;
}
private function createNewUser($request)
{
$user = new User();
$user->name = $request->first_name . ' ' . $request->last_name;
$user->email = $request->email;
$user->email_verification_code = Generators::generateRandomString(7);
$user->password = bcrypt($request->password ?? '123456');
$user->category = 'Trainee';
$user->dob = Carbon::parse($request->dob);
$user->phone = $this->formatPhone($request->phone);
$user->gender = $this->stringToPascal($request->gender);
$user->sponsor_id = $this->sponsor->id;
$user->batchcode = $this->batchcode;
$user->status = 1;
$user->code = $this->getUserCodePrefix($user->category) . '-' . time();
$user->save();
$user->bio()->create([
'firstname' => $request->first_name,
'lastname' => $request->last_name,
'email' => $request->email,
'phone' => $user->phone,
'stateofresidence' => 0,
'stateoforigin' => 0,
'nationality' => 0,
'dob' => Carbon::parse($user->dob),
'gender' => $user->gender
]);
return $user;
}
private function isUserAlreadySponsored($user, $course_code, $sponsor)
{
$course = Course::where('code', $course_code)->first();
if ($course && $user) {
$sponsoredUser = SponsoredUser::where('user_id', $user->id)
->where('course_id', $course->id)
->where('sponsor_id', $sponsor->id)
->first();
if ($sponsoredUser) {
$emailKey = $user->name . ' - ' . $user->email;
$this->existCourseCodes[$emailKey] = $course->code;
$this->messages .= $user->email . ' is already sponsored for course ' . $course->name . "\n";
return true;
}
}
return false;
}
private function recordSponsoredUser($user, $course, $request, $sponsor)
{
if (!$course) {
$this->messages .= 'Failed to import trainees: invalid course code' . "\n";
return null;
}
$sponsored_user = new SponsoredUser();
$sponsored_user->user_id = $user->id;
$sponsored_user->sponsor_id = $sponsor->id;
$sponsored_user->batchcode = $this->batchcode;
$sponsored_user->course_id = $course->id;
$sponsored_user->centre_code = $request->centre_code;
$sponsored_user->session_code = $request->session_code;
$sponsored_user->term_code = $request->semester_code;
$sponsored_user->save();
$this->registeredSponsors[] = $sponsored_user;
}
private function sendSummaryEmail()
{
if (!empty($this->existCourseCodes) || !empty($this->sponsor->template_sponsor_code)) {
Mail::to($this->authUser->email)->cc('[email protected]')->send(new InvalidCourseCodeMail([], $this->sponsor, $this->existCourseCodes));
}
}
private function handleFailure(\Exception $e)
{
Mail::to($this->authUser->email)->cc('[email protected]')->send(new InvalidCourseCodeMail($this->invalidCourseCodes, $this->sponsor, []));
Log::error('Failed to import trainees: ' . $e->getMessage());
$this->messages .= 'Failed to import trainees: ' . $e->getMessage() . "\n";
}
private function getUserCodePrefix($category)
{
switch ($category) {
case 'Trainee': return 'TNE';
case 'Trainer': return 'TNR';
case 'Staff': return 'STF';
case 'Super Admin': return 'SA';
default: return 'USR';
}
}
private function formatPhone($phone)
{
if (is_null($phone) || strlen((string)$phone) < 10) {
return null;
}
$phone = (string) $phone; // Convert to string if it is numeric
if (strlen($phone) == 10) {
$phone = "0{$phone}";
}
// Check if the number starts with + or 234 and replace it with 0
if (substr($phone, 0, 1) == '+') {
$phone = substr($phone, 1);
}
if (substr($phone, 0, 3) == '234') {
$phone = '0' . substr($phone, 3);
}
return $phone;
}
public function getSponsoredUsers()
{
return $this->registeredSponsors;
}
private function stringToPascal(string $string): string
{
// Convert the entire string to lowercase
$value = strtolower($string);
$pascalCaseString = ucfirst($value);
return $pascalCaseString;
}
public function getMessages()
{
return $this->messages;
}
}
Azure App Service enforces limits on script execution time and memory usage. If your script takes too long or exceeds memory, it may stop processing after two records. Increase limits using php.ini
or ini_set()
.
ini_set('memory_limit', '512M');
ini_set('max_execution_time', '300');
$path = $request->file('file')->store('uploads');
Excel::import(new YourImportClass(), storage_path('app/' . $path));
Set the path for Excel files to save in writable locations like /storage/app
or /tmp
.
Use Laravel Queues to process the spreadsheet asynchronously.
Configure config/queue.php
to use database queue.
Run queue worker as an Azure WebJob or Daemon.
Key Differences Between Azure App Service and an Azure Linux VM: