Once we switch to higher levels of Larastan - we'll start to see more errors that are stricter and could be a great help. Let's set up the example:
app/Services/ClientReportsService.php
<?php namespace App\Services; use App\Models\Transaction;use Carbon\Carbon; class ClientReportsService{ public function getReport($request) { $query = Transaction::with('project') ->with('transaction_type') ->with('income_source') ->with('currency') ->orderBy('transaction_date', 'desc'); if ($request->has('project')) { $query->where('project_id', $request->input('project')); } $transactions = $query->get(); $entries = []; foreach ($transactions as $row) { $date = Carbon::createFromFormat(config('panel.date_format'), $row->transaction_date)->format('Y-m'); if (!isset($entries[$date])) { $entries[$date] = []; } $currency = $row->currency->code; if (!isset($entries[$date][$currency])) { $entries[$date][$currency] = [ 'income' => 0, 'expenses' => 0, 'fees' => 0, 'total' => 0, ]; } $income = 0; $expenses = 0; $fees = 0; if ($row->amount > 0) { $income += $row->amount; } else { $expenses += $row->amount; } if (!is_null($row->income_source->fee_percent)) { $fees = $row->amount * ($row->income_source->fee_percent / 100); } $total = $income + $expenses - $fees; $entries[$date][$currency]['income'] += $income; $entries[$date][$currency]['expenses'] += $expenses; $entries[$date][$currency]['fees'] += $fees; $entries[$date][$currency]['total'] += $total; } return $entries; }}
For this we'll expand our code example with our Relationship definitions to better see the whole picture:
app/Models/Transaction.php
// ...public function income_source(): BelongsTo{ return $this->belongsTo(IncomeSource::class, 'income_source_id');} public function currency(): BelongsTo{ return $this->belongsTo(Currency::class, 'currency_id');}
So let's see what Larastan will show us here!
For this example, we'll be at level 8.
phpstan.neon
includes: - ./vendor/nunomaduro/larastan/extension.neon parameters: paths: - app/ # Level 9 is the highest level level: 8
------ ---------------------------------------------------------------------------------------------------------- Line ClientReportsService.php------ ---------------------------------------------------------------------------------------------------------- 31 Parameter #2 $time of static method Carbon\Carbon::createFromFormat() expects string, string|null given. 39 Cannot access property $code on App\Models\Currency|null. 56 Cannot access property $fee_percent on App\Models\IncomeSource|null. 57 Cannot access property $fee_percent on App\Models\IncomeSource|null.------ ----------------------------------------------------------------------------------------------------------
Quickly we can see a few issues reported already. Keep in mind that the code passed checks on level 6 but as levels add more checks - we'll get more errors.
$time
variable. It might be null and that would break our code.$row->currency
will always be set. The related Model might have been Soft Deleted.$row->income_source
will always be set. The related Model might have been Soft Deleted.Let's protect our code from these issues!
$time
VariableHow can this be? Well, we have our $time
variable as nullable in the database, and looking in the code we can find that our Model has an accessor for this field:
app/Models/Transaction.php
// ... public function getTransactionDateAttribute($value): ?string{ return $value ? Carbon::parse($value)->format(config('panel.date_format')) : null;} public function setTransactionDateAttribute($value): void{ $this->attributes['transaction_date'] = $value ? Carbon::createFromFormat(config('panel.date_format'), $value)->format('Y-m-d') : null;}
This means that sometimes it can actually return null
which would break our flow. How do we fix that? We can of course add a condition that would check if it's null
to skip that row from being included:
app/Services/ClientReportsService.php
// ...if (!$row->transaction_date) { continue;} $date = Carbon::createFromFormat(config('panel.date_format'), $row->transaction_date); // ...
With this solution, we will simply skip the row if it doesn't have a date set. This might change our reports, but it's better than having an error.
If we run ./vendor/bin/phpstan analyse
again we'll see that the error is gone:
------ ---------------------------------------------------------------------- Line ClientReportsService.php------ ---------------------------------------------------------------------- 43 Cannot access property $code on App\Models\Currency|null. 60 Cannot access property $fee_percent on App\Models\IncomeSource|null. 61 Cannot access property $fee_percent on App\Models\IncomeSource|null.------ ----------------------------------------------------------------------
As with many relationships - we have to be sure that the relationship was loaded correctly. It may not seem that important but with the usage of Soft Delete - you can have some resources that have an ID but the actual Model can't be loaded.
In this case, we have two possible cases - Currency
and IncomeSource
not being loaded when we are trying to access the data. Here's how we can fix them:
To fix an issue with our Currency
we'll set a default value for it:
app/Services/ClientReportsService
// ... // Locating line #43$currency = $row->currency->code ?? 'USD'; // ...
By adding ?? 'USD'
we've told the code to use USD
as our default currency if there's no code available.
Running ./vendor/bin/phpstan analyse
will not show an error anymore:
------ ---------------------------------------------------------------------- Line ClientReportsService.php------ ---------------------------------------------------------------------- 60 Cannot access property $fee_percent on App\Models\IncomeSource|null. 61 Cannot access property $fee_percent on App\Models\IncomeSource|null.------ ----------------------------------------------------------------------
As for our IncomeSource
, we can see in the code that it's completely optional and may not always be present. So we can change our code a little to add a check if it's not null:
app/Services/ClientReportsService
// ... // Locating line #60if ($row->income_source && !is_null($row->income_source->fee_percent)) { $fees = $row->amount * ($row->income_source->fee_percent / 100);} // ...
By adding the $row->income_source &&
to our condition we've told the code that ignores the fee if it is null
.
Running ./vendor/bin/phpstan analyse
will now completely pass our checks:
[OK] No errors
It is indeed that simple, but also it may not reflect our business needs for both of the changes. Sometimes it might be better to prevent the deletion of Relationships by adding checks