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:
$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!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:
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
.
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.
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!
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.