Over the past few years, static analysis in PHP, and more specifically Laravel, has become increasingly popular. As more and more people adopt it in their software development, I thought now would be a good time to write a tutorial on how to add it to your Laravel project.
Back in 2019, Nuno Maduro released a package called Larastan, a set of PHPStan rules for Laravel projects that I was super excited about. So far I've been struggling to get good static analysis coverage in Laravel using PHPStan or Psalm. Larastans rules allowed me to start applying more static analysis to my codebase and, in turn, have more confidence in my code. Now using PHP 8.1 and Laravel 9 - I feel more confident than ever in the code I write thanks to the sheer number of amazing tools at my disposal.
In this tutorial, I will step through adding Larastan to a new Laravel project, setting the level to highest.
First create a new Laravel project named larastan-test:
laravel new larastan-test
After creating the new project, install Larastan by running the following composer command:
composer require nunomaduro/larastan --dev
We want it to work as The reason for development dependencies is because in production we shouldn't be running any static analysis - it's only for development purposes to make sure your code is as secure as possible. PHPStan uses a configuration format called neon, which is somewhat similar to yaml. Therefore, we will create a new file called ./phpstan.neon in the root directory of the out application - if you are building a package, the recommended approach is to add .dist to the end of these configuration files. In this file we will start defining the configuration required for phpstan to run and the rules we may want to impose. By adding the following code to the configuration file we can understand what it means:
includes: - ./vendor/nunomaduro/larastan/extension.neon parameters: paths: - app level: 9 ignoreErrors: excludePaths:
We start with includes
To start, these are usually the rules in the package that we want to include in our basic phpstan ruleset. In the parameters section of this configuration, the first option paths
allows us to define the locations we want PHPStan to check - in this case, we just need to focus on the app
directory where the application code is located . You can extend this to cover multiple directories if you want, but be careful what scope you introduce, because everything is about to get serious! Next, PHPStan's level
parameter determines the various levels that can be checked, with 0 being the lowest and 9 currently being the highest.
As you can see we have set the level to 9, I would recommend doing this on an existing application as only ideally you would reach this level - but since this is a brand new project, We can feel very comfortable at 9 (the technical debt isn't that much after all).
Next, ignoreErrors
and excludePaths
These two options allow us to tell PHPStan to ignore files or specific errors that we are not interested in, such as those that we have no control over at this stage or Bugs fixed. Maybe you're refactoring some business and encounter a bug. You may be refactoring this code for static analysis later, then you can use this configuration to let PHPStan ignore related errors before you finish refactoring.
includes
Contains basic phpstan rules. parameters
Configuration parameters, the first option paths
Configure the directory that phpstan checks - in my case, I only checked the app
directory where the application code is located Check, of course you can configure other directories as well. level
Configuration level, PHPStan can be configured with various levels, 0 is the lowest, and 9 is currently the highest. As you can see, I've set the level to 9 and I recommend setting the level to 9. Next there are ignoreErrors
and excludePaths
which tell PHPStan to ignore files or specific errors that are not being checked, or files and errors that do not need to be checked now. For example, code is being refactored, and you want to ignore errors until it is complete, and then perform static analysis after it is completed.
So, let’s run phpstan against the default Laravel application and see what errors we encounter, if any. Run the following command in the terminal:
./vendor/bin/phpstan analyse
The output we get from the default Laravel application looks like this:
Note: Using configuration file /Users/steve/code/sites/larastan-test/phpstan.neon. 18/18 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% ------ ---------------------------------------------------------------------------------------------------------------------------- Line Providers/RouteServiceProvider.php ------ ---------------------------------------------------------------------------------------------------------------------------- 49 Parameter #1 $key of method Illuminate\Cache\RateLimiting\Limit::by() expects string, int<min, -1>|int<1, max>|string|null given. ------ ---------------------------------------------------------------------------------------------------------------------------- [ERROR] Found 1 error
As you can see, we only get Getting an error even though we set the check level to the strictest level.
That’s good, right? Of course, you may see different results if you add this to an existing project, and by following this tutorial you'll learn how to fix those issues so that you have a good workflow to follow.
Run phpstan in your Laravel application if an error occurs. Run the following command in the terminal:
./vendor/bin/phpstan analyse
The output will look like this
Note: Using configuration file /Users/steve/code/sites/larastan-test/phpstan.neon. 18/18 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% ------ ---------------------------------------------------------------------------------------------------------------------------- Line Providers/RouteServiceProvider.php ------ ---------------------------------------------------------------------------------------------------------------------------- 49 Parameter #1 $key of method Illuminate\Cache\RateLimiting\Limit::by() expects string, int<min, -1>|int<1, max>|string|null given. ------ ---------------------------------------------------------------------------------------------------------------------------- [ERROR] Found 1 error
现在,我们在最严格的级别下,在默认的 Laravel 应用程序中也只得到一个错误。 当然,如果您将其添加到现有项目中,您可能会看到不同的结果,但是按照本教程,您将学习如何解决这些问题。
如果您希望有一种简便的运行方式,可以将脚本添加到您的composer文件中来运行此命令,那么现在让我们添加它,以便我们可以更轻松地运行此命令,将以下代码块添加到你的 composer.json
文件中:
"scripts": { "phpstan": [ "./vendor/bin/phpstan analyse" ] }, "scripts-descriptions": { "phpstan": "Run PHPStan static analysis against your application." },
你的 composer 文件中有了 scripts
记录 - 只需将 phpstan
脚本附加到块的末尾即可。 现在我们可以再次运行 PHPStan ,但这次使用 composer , 更容易输入:
composer phpstan
所以当我们有 1 个错误时,查看对应的行,并且查看它当前的样子:
protected function configureRateLimiting() { RateLimiter::for('api', function (Request $request) { return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); }); }
本节开始,我们会聊聊静态分析让人抱怨的一些具体问题:
$request->user()?->id ?: $request->ip()
当我们想要获取请求用户,如果有的话返回ID,或者如果第一部分为空,则返回 IP 地址。在这个例子中,没有真正的方法来确保这永远是一个字符串,用户可能是空的,请求 IP 也可能是空的。
这是你想要消除错误的情况,但因为它是来自供应商(第三方包)的代码,你无法强制执行此操作。在这种特定情况下,你可以做的最好的事情是告诉 PHPStan 忽略该错误,但这不是全局性的。我们在这里要做的是添加一个命令块而不是设置规则,以告诉 PHPStan 在分析此代码时忽略此特定行。将此方法重构为如下所示:
protected function configureRateLimiting(): void { RateLimiter::for('api', static function (Request $request): Limit { /** @phpstan-ignore-next-line */ return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); }); }
我们为方法添加了返回类型,使回调成为静态闭包 - 并提示返回类型。但随后我们在返回值上方添加命令块,告诉 PHPStan 我们要忽略下一行。如果我们现在再次在命令行中运行 PHPStan,你将看到以下输出:
Note: Using configuration file /Users/steve/code/sites/larastan-test/phpstan.neon. 18/18 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% [OK] No errors
所以我们有默认的 Laravel 应用程序在 PHPStan 上运行,现在我们需要开始向我们的应用程序添加一些实际的逻辑,以便我们在添加功能和逻辑时可以确保类型安全。为此,我们将创建一个简单的应用程序来存储书签,这没什么特别的。
让我们开始使用 artisan 添加模型,并使用 -mf 参数同时创建迁移任务和工厂模式:
php artisan make:model Bookmark -mf
其中,迁移任务的 up
方法如下所示:
Schema::create('bookmarks', static function (Blueprint $table): void { $table->id(); $table->string('name'); $table->string('url'); $table->boolean('starred')->default(false); $table->foreignId('user_id')->index()->constrained()->cascadeOnDelete(); $table->timestamps(); });
将以下代码添加到我们的模型中:
class Bookmark extends Model { use HasFactory; protected $fillable = [ 'name', 'url', 'starred', 'user_id', ]; protected $casts = [ 'starred' => 'boolean', ]; /** * @return BelongsTo */ public function user(): BelongsTo { return $this->belongsTo( related: User::class, foreignKey: 'user_id', ); } }
从上面可以看出,我们在这里唯一关心的是名称、url,如果用户想要加星标/收藏书签并且该书签属于用户。现在我们可以把它留在这里,但我个人喜欢将类型定义添加到我的模型属性中——因为目前在 Laravel 9 中我无法输入提示它们。因此,重构你的模型,使其如下所示:
class Bookmark extends Model { use HasFactory; /** * @var array<int,string> */ protected $fillable = [ 'name', 'url', 'starred', 'user_id', ]; /** * @var array<string,string> */ protected $casts = [ 'starred' => 'boolean', ]; /** * @return BelongsTo */ public function user(): BelongsTo { return $this->belongsTo( related: User::class, foreignKey: 'user_id', ); } }
我们在这里所做的只是告诉 PHP 和我们的 IDE,可填充数组是一个没有键的字符串数组——这意味着它将默认为整数。然后我们的 casts 数组是一个带键的字符串数组,其中的键也是字符串。现在,即使在没有类型定义的情况下运行静态分析,它也不会失败 - 但这是一个很好的实践,以便你的 IDE 在你工作时拥有尽可能多的信息。
让我们继续处理路由和控制器,以便我们可以继续运行静态分析检查。现在我是可调用控制器的忠实粉丝——我发现它们非常适合我的代码风格,但是你可能不喜欢它们或有不同的偏好,所以如果你是的话,下一部分可以随意偏离我的编码风格,会让你更舒服。
我们现在将创建一个控制器,运行以下 artisan 命令来为书签创建索引控制器:
php artisan make:controller Bookmarks/IndexController --invokable
这是我们路由所需的索引控制器,所以我们可以去添加一个新的路由组在 routes/web.php
:
Route::middleware(['auth'])->prefix('bookmarks')->as('bookmarks:')->group(static function (): void { Route::get('/', App\Http\Controllers\Bookmarks\IndexController::class)->name('index'); });
添加在在我们的 auth 中间件中,以便我们控制作者对书签的访问,我们还希望在 bookmarks
下为所有路由添加前缀,并将该组的命名策略设置为 bookmarks:*
。 如果我们现在在我们的代码库上运行我们的静态分析,我们会看到一些错误,但这主要是因为我们的控制器中没有内容:
composer phpstan
Note: Using configuration file /Users/steve/code/sites/larastan-test/phpstan.neon. 20/20 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% ------ ------------------------------------------------------------------------------------------------- Line Http/Controllers/Bookmarks/IndexController.php ------ ------------------------------------------------------------------------------------------------- 15 Method App\Http\Controllers\Bookmarks\IndexController::__invoke() has no return type specified. ------ ------------------------------------------------------------------------------------------------- ------ ----------------------------------------------------------------------------------------------------------------------------- Line Models/Bookmark.php ------ ----------------------------------------------------------------------------------------------------------------------------- 33 Method App\Models\Bookmark::user() return type with generic class Illuminate\Database\Eloquent\Relations\BelongsTo does not specify its types: TRelatedModel, TChildModel ? You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your phpstan.neon. ------ ----------------------------------------------------------------------------------------------------------------------------- ------ ---------------------------------------------------------------------------------------------------------------------------- Line Models/User.php ------ ---------------------------------------------------------------------------------------------------------------------------- 49 Method App\Models\User::bookmarks() return type with generic class Illuminate\Database\Eloquent\Relations\HasMany does not specify its types: TRelatedModel ? You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your phpstan.neon. ------ ---------------------------------------------------------------------------------------------------------------------------- [ERROR] Found 3 errors
摆在我面前的第一个错误是 Method App\Models\User::bookmarks() return type with generic class
。现在我不想在这个应用中过度依赖通用类型。这一错误实际上告诉我们可以做什么,所以让我们将checkGenericClassInNonGenericObjectType: false
添加到我们的 phpstan.neon
文件中:
includes: - ./vendor/nunomaduro/larastan/extension.neon parameters: paths: - app level: 9 ignoreErrors: excludePaths: checkGenericClassInNonGenericObjectType: false
现在,如果我们再次运行分析,将只有 5 个错误,这些错误都和控制器相关 - 让我们从 IndexController
开始,看看我们能做些什么。像这样重构 IndexController
:
class IndexController extends Controller { public function __invoke(Request $request) { return View::make( view: 'bookmarks.list', data: [ 'bookmarks' => Bookmark::query() ->where('user_id', $request->user()->id) ->paginate(), ] ); } }
如果我们现在对我们的代码进行静态分析,并且只关注正在使用的控制器,我们将看到如下问题:
------ ------------------------------------------------------------------------------------------------- Line Http/Controllers/Bookmarks/IndexController.php ------ ------------------------------------------------------------------------------------------------- 15 Method App\Http\Controllers\Bookmarks\IndexController::__invoke() has no return type specified. 21 Cannot access property $id on App\Models\User|null. ------ -------------------------------------------------------------------------------------------------
那么我们对这两个错误能做些什么呢?第一个相对容易修复,我们可以添加返回类型:
public function __invoke(Request $request): \Illuminate\Contracts\View\View
我们可以对此约束起个别名,使之看起来更为美观:
public function __invoke(Request $request): ViewContract
然而下一个问题,Cannot access property $id on App\Models\User|null.
,类似于我们在默认 Laravel 应用中,在请求的用户可以为空的情况下去获取ID时会碰到的问题。因此我用以解决此问题的方法是,使用 Auth 的辅助函数直接从 Auth 守卫中获取 ID。重构查询如下:
Bookmark::query() ->where('user_id', auth()->id()) ->paginate()
使用 Auth 的 ID 方法,我们直接从认证守卫中获取 ID,而不是从可能是 null 的请求(request)中获取。需要记住的一点是,如果路由没有使用认证中间件,那么 id 方法会出现“正在尝试获取 null 的属性ID(you are trying to get the property ID of null)”的报错。因此,请记得为该路由设置对应中间件。
现在,如果我们再次运行静态分析,我们应该已经消除了这些错误:
composer phpstan
Note: Using configuration file /Users/steve/code/sites/larastan-test/phpstan.neon. 20/20 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% [OK] No errors
既然 IndexController
已经没有错误了。下一步我们要做的是遍历我们的应用,确保在重要的节点中都运行静态分析检查。我们最不想做的事情就是等到 sprint 格式化打印结束,或者在添加新功能来运行它时,才发现我们必须花费无数个小时来修复静态分析问题。无论如何,到最后 - 你将拥有可信任的代码了,这也是我通常喜欢使用静态分析的一个重要原因。如果你可以配合好的测试套件进行静态分析,那么就没有理由不信任你的代码。
你的项目使用了 Larastan 吗? 你敢把验证级别提高到最高吗? 在推特上告诉我们, 或者让我们知道你的恐怖故事!
原文地址:https://laravel-news.com/running-phpstan-on-max-with-laravel
译文地址:https://learnku.com/laravel/t/69412
【相关推荐:laravel视频教程】
The above is the detailed content of Detailed explanation of how to set the highest verification level of PHPStan in Laravel. For more information, please follow other related articles on the PHP Chinese website!