在过去的几年里,PHP 中的静态分析,更具体地说是 Laravel,变得越来越流行。 随着越来越多的人在他们的软件开发中采用它,我认为现在是编写一篇关于如何将它添加到 Laravel 项目中的教程的好时机。
早在 2019 年,Nuno Maduro 发布了一个名为 Larastan 的包,这是一组适用于 Laravel 项目的 PHPStan 规则,我非常兴奋。 到目前为止,我一直在努力使用 PHPStan 或 Psalm 在 Laravel 中获得良好的静态分析覆盖率。 Larastans 规则允许我开始对我的代码库应用更多的静态分析,进而对我的代码更有信心。 在使用 PHP 8.1 和 Laravel 9 的现在 - 由于我可以使用大量令人惊叹的工具,我对自己编写的代码感到前所未有的自信。
在本教程中,我会逐步将 Larastan 添加到新的 Laravel 项目中,将级别设置为最高。
先创建一个名为 larastan-test 的新 Laravel 项目:
laravel new larastan-test
新建项目后,安装 Larastan,通过运行以下 composer 命令:
composer require nunomaduro/larastan --dev
我们希望它作为开发依赖项的原因是因为在生产中我们不应该运行任何静态分析 - 它仅用于开发目的,以确保您的代码尽可能安全。 PHPStan 使用一种称为 neon 的配置格式,在某种程度上类似于 yaml。 因此,我们将在 out 应用程序的根目录中创建一个名为 ./phpstan.neon 的新文件 - 如果您正在构建一个包,推荐的方法是将 .dist 添加到这些配置文件的末尾。 在这个文件中,我们将开始定义 phpstan 运行所需的配置以及我们可能想要强加的规则,将以下代码添加到配置文件中,我们可以了解它的含义:
includes: - ./vendor/nunomaduro/larastan/extension.neon parameters: paths: - app level: 9 ignoreErrors: excludePaths:
我们从 includes
开始,这些通常是我们希望包含在我们的基本 phpstan 规则集中的包中的规则。这个配置的参数部分,第一个选项 paths
允许我们定义我们希望 PHPStan 检查的位置——在案例中,我们只需要聚焦到应用程序代码所在的 app
目录。如果你愿意,你可以将其扩展到覆盖多个目录,但要小心你所引入的范围,因为所有的事情即将变得严格(严谨)!接下来,PHPStan 的 level
参数决定了可以检查的各种级别,0 是最低的,9 目前是最高的。
如你所见,我们已将级别设置为 9,我建议在现有应用程序上这样做,因为只有理想情况下你才达到这个级别 - 但由于这是一个全新的项目,我们可以在 9 时感到非常舒服(毕竟技术债务没有那么多)。
接下来,ignoreErrors
和 excludePaths
这两个选项允许我们告诉 PHPStan 忽略我们不感兴趣的文件或特定的错误,比如现阶段我们无法控制或修复的错误。也许你正在重构一些业务并且遇到了错误。你可能正在重构这段代码,以便稍后进行静态分析,那你可以通过这个配置,让 PHPStan 在你结束重构前,忽略相关的错误。
includes
包含基本的 phpstan 的规则。parameters
配置参数,第一个选项 paths
配置 phpstan 检查的目录——在我的例子中,我只对应用程序代码所在的 app
目录进行检查,当然您也可以配置其他目录。 level
配置级别,PHPStan 可以配置各种级别,0 是最低的,9 目前是最高的。如您所见,我已将级别设置为 9,我建议将级别设置为 9。接下来有 ignoreErrors
和 excludePaths
这两个选项告诉 PHPStan 忽略不检测的文件或特定错误,或者现在不需要检测的文件和错误。例如正在重构的代码,您希望在完成之前忽略错误,完成后再进行静态分析。
因此,让我们针对默认的 Laravel 应用程序运行 phpstan,看看我们遇到了什么错误,如果有的话。在终端中运行以下命令:
./vendor/bin/phpstan analyse
我们从默认 Laravel 应用程序获得的输出如下所示:
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 应用程序中只得到一个错误,即使我们将检查的级别设置到了最严格的等级。
这很好,对吧?当然,如果你将其添加到现有项目中,你可能会看到不同的结果,按照本教程,你将学习如何解决这些问题,以便你有一个很好的工作流程可以遵循。
在 Laravel 应用程序运行 phpstan,如果发生错误。 在终端中运行以下命令:
./vendor/bin/phpstan analyse
输出如下所示
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视频教程】
以上是详解Laravel中怎么设置PHPStan最高验证级别的详细内容。更多信息请关注PHP中文网其他相关文章!