在過去的幾年裡,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中文網其他相關文章!