Back to Course |
Larastan: Catch Bugs with Static Analysis

Example 2: Missing Types

In this example, we'll dive deeper into Parameter Types and Return Types. Let's see what we'll be working with here:

Our codebase that we'll check:

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;
}
}

So let's see what Larastan will show us here!

For this example, we'll be at level 6.

phpstan.neon

includes:
- ./vendor/nunomaduro/larastan/extension.neon
 
parameters:
paths:
- app/
 
# Level 9 is the highest level
level: 6
------ ------------------------------------------------------------------------------------------------------
Line Services/ClientReportsService.php
------ ------------------------------------------------------------------------------------------------------
10 Method App\Services\ClientReportsService::getReport() has no return type specified.
10 Method App\Services\ClientReportsService::getReport() has parameter $request with no type specified.
26 Cannot call method format() on Carbon\Carbon|false.
------ ------------------------------------------------------------------------------------------------------

This is a working code but here are some issues with it:

  1. We don't know what the $request should be. Can it be a string? An array? An object? We don't know. Sure, the name of the variable is request but naming is hard!
  2. What will the code return? An array? An object? A string? Hard to tell yet... Of course, we can look at the code and guess, but it's not ideal as we might miss something.
  3. When creating a carbon instance - we might get false back. This is not ideal as we'll get an error when trying to call format() on it.

To fix this, we can add the following:

app/Services/ClientReportsService.php

use Illuminate\Http\Request;
 
// ...
 
public function getReport(Request $request): array {
// ...
// Fixing line #26
$date = Carbon::createFromFormat(config('panel.date_format'), $row->transaction_date);
if(!$date) {
throw new \Exception('Invalid date format');
}
$date = $date->format('Y-m');
// ...
}

Let's run Larastan again and see what we get:

------ ----------------------------------------------------------------------------------------------------------------
Line Services/ClientReportsService.php
------ ----------------------------------------------------------------------------------------------------------------
12 Method App\Services\ClientReportsService::getReport() return type has no value type specified in iterable type array.
đź’ˇ See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
------ ----------------------------------------------------------------------------------------------------------------

Oh, no! This is new! But don't worry, we can fix it (It even gave us a documentation link to read on how to fix it!)!

In this case, since we are returning an array back from the function - our tool wants to know what will the array look like. For this example we'll solve this issue by modifying our code a little more:

app/Services/ClientReportsService.php

// ...
 
/**
* @return array<string, array<string, array<string, float>>>
*/
public function getReport(Request $request): array {
// ...
}

And after running Larastan again, we'll see:

Awesome! Now, what did we do? Let's dive deeper:


"Method App\Services\ClientReportsService::getReport() has no return type specified."

We've defined our return type by adding : array to our function:

// ...
 
public function getReport($request): array {
// ...
}

So what did this do? It added an expected return type which is an array in this case. If you try to return anything than an array you'll get an error in your browser:

And of course, on the next Larastan run you will see:

------ ---------------------------------------------------------------------------------------------------------------
Line ClientReportsService.php
------ ---------------------------------------------------------------------------------------------------------------
17 Method App\Services\ClientReportsService::getReport() should return array<string, array<string, array<string,
float>>> but returns App\Models\Transaction.
------ ---------------------------------------------------------------------------------------------------------------

Which prevents us from returning unwanted code. This is especially helpful when working on the same code after some time as you'll have a safety check - you'll know that it should always be an array.


"Method App\Services\ClientReportsService::getReport() has parameter $request with no type specified."

We've added a Request type on our $request:

// ...
 
public function getReport(Request $request): array {
// ...
}

This way we added a requirement for us to pass a Request class as a parameter and not anything else. If you try to pass an array when calling this function you'll get an error in your browser:

And of course, on the next Larastan run you will see:

------ -----------------------------------------------------------------------------------------------------------------
Line Http/Controllers/Admin/ClientReportController.php
------ -----------------------------------------------------------------------------------------------------------------
18 Parameter #1 $request of method App\Services\ClientReportsService::getReport() expects Illuminate\Http\Request,
array<string, int> given.
------ -----------------------------------------------------------------------------------------------------------------

This way we can be sure that we are passing the Request class to our function and not something else. We've just protected ourselves from incorrect data in our function.


"Cannot call method format() on Carbon\Carbon|false."

We've changed how the code works and added a separate check to see if Carbon::createFromFormat() was successful:

// ...
 
$date = Carbon::createFromFormat(config('panel.date_format'), $row->transaction_date);
if(!$date) {
throw new \Exception('Invalid date format'); // Or you can attempt to re-create the date here
}
$date = $date->format('Y-m');
 
// ...

After this change, we've added a check to see if the $date is correctly created. If it's not - we throw an exception. This way we can be sure that we are not trying to call format() on a false value which would throw an unexpected exception. It's a rare case, but it's better to be safe than sorry!


"Method App\Services\ClientReportsService::getReport() return type has no value type specified in iterable type array."

We've added a return array type as a docblock:

// ...
 
/**
* @return array<string, array<string, array<string, float>>>
*/
 
//...

This one is a bit more complicated. We've added a doc block to our function which tells the tool what type of array we are returning. It seems unnecessary at first, but it's very helpful. Let's assume that we changed our code to return a different format of data:

app/Services/ClientReportsService.php

// ...
foreach ($transactions as $row) {
// ...
 
$entries[$row->project_id][$date][$currency]['income'] += $income;
$entries[$row->project_id][$date][$currency]['expenses'] += $expenses;
$entries[$row->project_id][$date][$currency]['fees'] += $fees;
$entries[$row->project_id][$date][$currency]['total'] += $total;
}
 
return $entries;

We are now grouping our data by project_id. If we try to return this data to our controller - we'll get an error in our browser:

Since we didn't change our view - the @foreach is now broken. This is not a desired outcome and can be caught by Larastan as well:

------ ---------------------------------------------------------------------------------------------------------------
Line Services/ClientReportsService.php
------ ---------------------------------------------------------------------------------------------------------------
67 Method App\Services\ClientReportsService::getReport() should return array<string, array<string, array<string,
float>>> but returns array<int|string, array<string, array<string, array<string, float|int>>>>.
------ ---------------------------------------------------------------------------------------------------------------

It instantly indicates to us that we have differences in what we would expect as per our doc block and what we returned. This way we can be sure that we are returning the correct data and that no unexpected errors will occur.