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.
- We are not sure if we will always get a string for our
$time
variable. It might be null and that would break our code. - We are not sure if our
$row->currency
will always be set. The related Model might have been Soft Deleted. - We are not sure if our
$row->income_source
will always be set. The related Model might have been Soft Deleted.
Let's protect our code from these issues!
Fixing our $time
Variable
How 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.------ ----------------------------------------------------------------------
Fixing our Currency and Income Source Relationships
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
So far so good!; however the null cleanup is the worst for me I am getting several pages with the user |null error
Line Http\Controllers\Auth\AuthenticatedSessionController.php 34 Cannot call method getRedirectRouteName() on App\Models\User|null.
Line Http\Controllers\Auth\ConfirmablePasswordController.php 29 Cannot access property $email on App\Models\User|null.
Line Http\Controllers\Auth\EmailVerificationNotificationController.php 17 Cannot call method hasVerifiedEmail() on App\Models\User|null. 21 Cannot call method sendEmailVerificationNotification() on App\Models\User|null.
Line Http\Controllers\Auth\EmailVerificationPromptController.php 18 Cannot call method hasVerifiedEmail() on App\Models\User|null.
Line Http\Controllers\Auth\PasswordController.php 23 Cannot call method update() on App\Models\User|null.
Line Http\Controllers\Auth\RegisteredUserController.php 51 Cannot call method getRedirectRouteName() on App\Models\User|null.
Line Http\Controllers\Auth\VerifyEmailController.php 18 Cannot call method hasVerifiedEmail() on App\Models\User|null. 22 Cannot call method markEmailAsVerified() on App\Models\User|null. 23 Parameter #1 $user of class Illuminate\Auth\Events\Verified constructor expects Illuminate\Contracts\Auth\MustVerifyEmail, App\Models\User|null given.
Line Http\Controllers\ProfileController.php 29 Cannot call method fill() on App\Models\User|null. 31 Cannot call method isDirty() on App\Models\User|null. 32 Cannot access property $email_verified_at on App\Models\User|null. 35 Cannot call method save() on App\Models\User|null. 53 Cannot call method delete() on App\Models\User|null.
Line Http\Middleware\RoleMiddleware.php 18 Cannot access property $role_id on App\Models\User|null.
Line Http\Requests\ProfileUpdateRequest.php 21 Cannot access property $id on App\Models\User|null.
This could be one of two cases:
return type
in the Model itselfnull
. This can happen with->find($id)
In order to hlep more efficient - can you add some code examples? If you think it's too private - you can send us an email and Povilas will forward it to me for help!