Mastering Code Maintenance: Proven Strategies for Sustainable Software Development
February 5, 2025
Introduction
When working on a project, it is vital that you keep your code organized. It is your responsibility, as a project developer, to keep everything well-maintained and documented.
If you are curious to learn more about how to effectively maintain code, you can read Mastering Code Maintenance: Proven Strategies for Sustainable Software Development by Evan Taylor.
When maintaining code, it is important to keep in mind how long it will take for your project’s pages and functions to load, especially if the project consists sizeable of tables.
In this article, we will be going through the best practices that will improve the speed of your project.
Optimize your Database Queries
When working on a project, there will be instances where you need to retrieve data from a large table or from multiple tables. If you are retrieving data from a table that has over 20 columns, but you only require data from 3 columns, it would be best to use the SELECT clause.
It is important to note that placing your WHERE clauses in a certain order can minimize your site’s processing power.
$collectors = Collector::select('id', 'name', 'branch', 'phone_work', 'collection_code')
->where('name', $collector_name)
->where('status', 'enabled')
->get();
If there are instances where you need to find records that are linked to another table’s records, you can either use WHERE clause with a function query or you could use the eloquent relationship to determine which record matches your query.
Note: You can also add WHERE clause with a function query in a scope query if you are planning on using the query more than once:
/* QUERY */
$delivery_assignment = \App\DeliveryAssignment::whereNull('deleted_at')
->where(function ($q) {
$q->whereNotNull('released_at')
->orWhereNotNull('declined_at');
})
->get();
...
/* Scope and Eloquent Relationship found in Model*/
public function discount()
{
return $this->hasMany(\App\Discount::class);
}
...
public function scopeUnresolved($query)
{
return $query->where(function ($subQuery) {
$subQuery->whereNull('automation_state')
->orWhereNotIn('automation_state', [AppointmentAutomation::PENDING, AppointmentAutomation::CANCELLED]);
})
->whereNull('contact_id');
}
...
If you are in a situation where you need to find records based on the ids of another query, you will need to use pluck()->toArray():
$query->pluck('id')->toArray()
If your project consists of large set of data that cannot be retrieved instantaneously, the best alternative would be to either use DB with a RAW clause or a TABLE clause:
DB::raw('
(SELECT
project.id as slot_id,
max(project.id) as max_id
FROM
supplier_project as project
GROUP BY
project.id
) as project2');
...
DB::table('failed_jobs')->where('failed_at', 'like', $year_month."%")->get();
Enable Pagination for Tables
Now that we have gone through how we can improve our queries, the next important step is to add pagination to our tables.
If you are expecting a page to consist of more than 50 records, it would be advisable to add a paginate function to your query. Doing so ensures that the page loads faster, since it will spend less processing time retrieving the designated number of records instead of all the records:
deliverySlotController:
...
$delivery_slots = $record->delivery_slots();
$delivery_slots = $delivery_slots
->with('tradepoint.region.province')
->with('contact')
->with('delivery_assignments.client');
$delivery_slots = $delivery_slots
->orderBy('day_of_week')
->orderBy('date')
->orderBy('time');
$delivery_slots = $delivery_slots
->paginate(config('project.records_per_long_page'))
->appends(Input::except('page'));
...
delivery.blade:
...
@if (count($delivery_slots) == 1)
{{ count($delivery_slots) }} delivery_slot found
@else
{{ count($delivery_slots) }} delivery_slots found
@endif
{!! $delivery_slots->render() !!}
...
By implementing the code above, you are adding page buttons, which will allow the user to view data within that paginated group. You can also apply a similar code structure to a livewire component:
Livewire/Completed:
...
use Livewire\WithPagination;
class Completed extends Component
{
use WithPagination;
...
protected $paginationTheme = 'bootstrap';
private $records;
...
public function filter()
{
...
if($this->filter_clients_completed == "all" || $this->filter_clients_completed == null){
$records = Product::select('id',
'name',
'country_area_id',
'gps_latitude',
'gps_longitude',
'started_at',
'ended_at',
'user_id'
)
->whereIn('id', $product_ids);
} else {
$records = Product::select('id',
'name',
'country_area_id',
'gps_latitude',
'gps_longitude',
'started_at',
'ended_at',
'user_id'
)
->whereIn('id', $product_ids)
->where('client_id', $this->filter_clients_completed);
}
if($this->filter_country_area_id_completed == "all" || $this->filter_country_area_id_completed == null){
$records = $records->where("started_at", '>=', $start_date)
->where("ended_at", '<=', $end_date)->withTrashed()->with('country_area', 'user');
} else {
$records = $records->where('country_area_id', $this->filter_country_area_id_completed)
->where("started_at", '>=', $start_date)
->where("ended_at", '<=', $end_date)->withTrashed()->with('country_area', 'user');
}
$this->records = $records;
...
}
...
public function render()
{
$this->filter();
$total_count = $this->records->count();
$viewData = [
'records' => $this->records->paginate(10),
'total_count' => $total_count,
];
return view('livewire.report.completed', $viewData);
}
}
livewire.report.completed:
...
@if (count($records) == 1)
{{ count($records) }} of 1 record displayed
@else
{{ count($records) }} of {{ $total_count }} records displayed
@endif
@if (! $records->isEmpty())
{{$records->links()}}
@endif
...
Use Queues for Mails and Time-Consuming Tasks
Laravel queues are a useful tool that assists in handling large sets of data and processing mails.
It is essential to ensure that your project uses a mail queue if you expect the project to send out over a thousand emails daily.
This way, you can ensure that when a function is triggered by either a scheduled command or user, and the emails will be placed in a queue that will gradually send to the intended recipient. The project will not be negatively affected by the large number of emails and the emails can be resent in the event an error occurs with the queue.
You will need to ensure you have something like the code below within Redis and job instances:
config/queue.php:
...
'connections' => [
...
'redis' => [
'driver' => 'redis',
'client' => env('REDIS_CLIENT', 'predis'),
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 420,
'timeout' => 240,
'block_for' => null,
],
'rateLimits' => [
'mail' => [
'allows' => 300,
'every' => 60
]
],
]
...
SendEmailJob:
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\Mail;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class SendEmailJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private $mailer;
private $recipient;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($recipient, $mailer)
{
$this->mailer = $mailer;
$this->recipient = $recipient;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
Mail::to($this->recipient)->send($this->mailer);
}
}
Dispatch Email:
dispatch(new SendEmailJob($this->contact_admin_email_address, $mailer_class))->onQueue('mail');
Note: You will need to ensure that have an active worker for your site. You can either run commands such as php artisan queue:work and php artisan queue:work --queue=mail, or you can add a worker on platforms such as Forge.
If you ever in a situation where need to export large amounts of data without the need to wait 30 minutes, we can also use queue to do it within 30 seconds:
Livewire/Completed:
...
use Maatwebsite\Excel\Facades\Excel;
class Completed extends Component
{
...
public function export(){
$export_ids = $this->excelData;
$fileName = 'exports/completed-' . Auth::id() . '-' . Carbon::now()->format('Y-m-d') . '-' . time() . '.xlsx';
$export = new ExportCompleted($export_ids);
Excel::queue($export, $fileName, 'public')
->chain([
function () use ($fileName) {
$downloadUrl = asset('storage/' . $fileName);
$reportType = "Completed Report";
$user_email = $this->user->email;
$user_name = $this->user->name_first." ".$this->user->name_last;
Mail::to($user_email)->send(new ExportedReport($user_name, $reportType, $downloadUrl));
}
]);
return session()->flash('message', 'Export is being processed. You will receive an email once it is ready.');
}
...
}
ExportCompleted
...
namespace App\Exports;
use Carbon\Carbon;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class ExportCompleted implements FromCollection, WithHeadings, ShouldAutoSize, WithStyles, ShouldQueue
{
/**
* @return \Illuminate\Support\Collection
*/
protected $export;
protected $fileName;
public function __construct($export){
$this->export = $export;
// fileName is responsible for naming the export file and ensuring it gets saved to public/export
$this->fileName = 'exports/completed-' . Auth::id() . '-' . Carbon::now()->format('Y-m-d') . '-' . time() . '.xlsx';
}
public function collection()
{
$export = collect();
DB::table('product') // Query builder helps optimize speed
->whereIn('id', $this->export)
->orderBy('id')
->chunk(1000, function ($records) use (&$export) { // Processesing 1000 rows at a time
$productIds = $records->pluck('id')->unique()->toArray();
// Fetch latest completed workflow deliveryTask for each product_id
$deliveryTasks = DB::table('deliveryTasks')
->select('content')
->where('state', 'completed')
->whereRaw("JSON_EXTRACT(content, '$.product_id') IN (" . implode(',', $productIds) . ")")
->orderByDesc('id') // Ensures latest deliveryTask comes first
->get()
->mapWithKeys(fn($deliveryTask) => [json_decode($deliveryTask->content, true)['product_id'] => json_decode($deliveryTask->content, true)]);
foreach ($records as $record) {
$completedTask = $deliveryTasks->get($record->id);
$km_difference = $completedTask ? coordinatesDistance(
$record->gps_latitude,
$record->gps_longitude,
optional($completedTask)['gps_coordinates']['latitude'] ?? null,
optional($completedTask)['gps_coordinates']['longitude'] ?? null
) : "---";
$export->push([
"id" => $record->id ?? "---",
"name" => $record->name ?? "---",
"province" => optional(DB::table('province')->where('id', $record->country_area_id)->first())->title ?? "---",
"km_difference" => $km_difference,
"started_at" => $record->started_at ?? "---",
"ended_at" => $record->ended_at ?? "---",
"handled_by" => optional(DB::table('users')->where('id', $record->user_id)->first())->name_first . ' ' . optional(DB::table('users')->where('id', $record->user_id)->first())->name_last ?? "---"
]);
}
});
return $export;
}
public function headings(): array
{
return [
'ID',
'Name',
'Province',
'KM Difference',
'Started At',
'Ended At',
'Handled By'
];
}
public function styles(Worksheet $sheet)
{
$sheet->getStyle(1)->getFont()->setBold(true);
$sheet->getStyle("A1:G1")->getFill()->applyFromArray([
'fillType' => 'solid',
'rotation' => 0,
'color' => ['rgb' => 'D9D9D9'],
]);
}
}
Of course, you can try different approaches, but the reason I have implemented the above example is to guarantee that the Laravel site will not give out a 500 error while the export functionality is running to store data on the document. After the file has been sorted, it sends the email to the user who wants to export the data.
Optimize Your Commands
If you are using commands that update and delete data, especially of considerable size, you will need to ensure that your command has optimized queries, as discussed in the first section.
You will need to ensure that the command is run when the site isn’t too busy and doesn’t overlap with other existing commands.
It is imperative that you know what type of loops you want to use for your command, as the purpose of the command can vastly affect how your command will be executed.
If the command needs to update certain values of a record that matches your query, you can use a foreach loop. If the command is supposed to delete data that is older than 2 months, you will then need to use a do while loop to avoid the command from exhausting resources. Please see the examples below:
...
/*
The simple query below uses a hasMany relationship to determine if the appointment record does not have a record in the discount table
and uses a query scope to determine if the appointment has been confirmed.
The query results will then processed via the foreach loop.
*/
$appointments = \App\Appointment::enabled()
->where('automation_state', AppointmentAutomation::START)
->doesntHave('discount')
->hasConfirmed();
$appointments = $appointments->get();
foreach ($appointments as $appointment) {
$appointment->automation_state = AppointmentAutomation::PENDING;
$appointment->save();
}
------------
OR
------------
/*
We are limiting the amount of records being deleted to a thousand every 10 seconds.
The sleep function ensures there is delay between each delete in order to avoid command from failing
*/
do {
$deleted = Audit::where('created_at', '<', $date)->limit(1000)->delete();
sleep(10);
} while ($deleted > 0);
...
In the end, it is always important to keep in mind what is expected to be shown to a user, how many emails will be sent, and how much data needs to be processed. There are of course many other guides that give additional tips on improving your site, such as Mastering Laravel Performance Optimization: A Comprehensive Guide by Satyam Verma and Practical strategies for Laravel performance optimization by Samson Omojola.
I hope that this article has been informative and that it will guide you in improving new and old Laravel projects.