>  기사  >  PHP 프레임워크  >  Laravel에서 PHPStan의 최고 검증 수준을 설정하는 방법에 대한 자세한 설명

Laravel에서 PHPStan의 최고 검증 수준을 설정하는 방법에 대한 자세한 설명

青灯夜游
青灯夜游앞으로
2022-10-25 21:39:491869검색

Laravel에서 PHPStan의 최고 검증 수준을 설정하는 방법에 대한 자세한 설명

지난 몇 년 동안 PHP, 특히 Laravel의 정적 분석이 점점 인기를 얻고 있습니다. 점점 더 많은 사람들이 소프트웨어 개발에 이를 채택함에 따라 이제는 Laravel 프로젝트에 이를 추가하는 방법에 대한 튜토리얼을 작성하기에 좋은 시기라고 생각했습니다.

2019년에 Nuno Maduro는 제가 매우 기대했던 Laravel 프로젝트에 대한 PHPStan 규칙 세트인 Larastan이라는 패키지를 출시했습니다. 지금까지 저는 PHPStan 또는 Psalm을 사용하여 Laravel에서 좋은 정적 분석 범위를 확보하기 위해 고심해 왔습니다. Larastan 규칙을 사용하면 내 코드베이스에 더 많은 정적 분석을 적용할 수 있었고 결과적으로 내 코드에 더 많은 자신감을 가질 수 있었습니다. 이제 PHP 8.1과 Laravel 9를 사용하고 있습니다. 엄청나게 많은 놀라운 도구를 마음대로 사용할 수 있기 때문에 제가 작성하는 코드에 대해 그 어느 때보다 자신감이 생겼습니다.

이 튜토리얼에서는 새로운 Laravel 프로젝트에 Larastan을 단계별로 추가하여 레벨을 최고로 설정하겠습니다.

larastan-test라는 새 Laravel 프로젝트를 생성하여 시작합니다.

laravel new larastan-test

새 프로젝트를 생성한 후 다음 작곡가 명령을 실행하여 Larastan을 설치합니다.

composer require nunomaduro/larastan --dev

우리가 이를 개발 종속성으로 원하는 이유는 프로덕션에서는 어떠한 정적 분석도 실행해서는 안 됩니다. 이는 코드가 최대한 안전한지 확인하기 위한 개발 목적으로만 사용됩니다. PHPStan은 yaml과 다소 유사한 네온이라는 구성 형식을 사용합니다. 따라서 out 애플리케이션의 루트 디렉터리에 ./phpstan.neon이라는 새 파일을 생성합니다. 패키지를 빌드하는 경우 권장되는 접근 방식은 이러한 구성 파일 끝에 .dist를 추가하는 것입니다. 이 파일에서는 phpstan을 실행하는 데 필요한 구성과 적용할 규칙을 정의하기 시작합니다. 구성 파일에 다음 코드를 추가하면 그 의미를 확인할 수 있습니다.

includes:
    - ./vendor/nunomaduro/larastan/extension.neon
parameters:
    paths:
        - app
    level: 9
    ignoreErrors:
    excludePaths:

includes를 시작하면 일반적으로 기본 phpstan 규칙 세트에 포함하려는 패키지의 규칙입니다. 이 구성의 매개변수 섹션에서 첫 번째 옵션 <code>paths를 사용하면 PHPStan이 확인할 위치를 정의할 수 있습니다. 이 경우 app에만 집중하면 됩니다. 애플리케이션 코드가 있는 위치 > 디렉토리. 원하는 경우 여러 디렉터리를 포함하도록 확장할 수 있지만 모든 것이 심각해지기 때문에 어떤 범위를 도입할지 주의하세요! 다음으로, PHPStan의 level 매개변수는 확인할 수 있는 다양한 수준을 결정하며, 0이 가장 낮고 9가 현재 가장 높습니다. includes 开始,这些通常是我们希望包含在我们的基本 phpstan 规则集中的包中的规则。这个配置的参数部分,第一个选项 paths 允许我们定义我们希望 PHPStan 检查的位置——在案例中,我们只需要聚焦到应用程序代码所在的 app 目录。如果你愿意,你可以将其扩展到覆盖多个目录,但要小心你所引入的范围,因为所有的事情即将变得严格(严谨)!接下来,PHPStan 的 level 参数决定了可以检查的各种级别,0 是最低的,9 目前是最高的。

如你所见,我们已将级别设置为 9,我建议在现有应用程序上这样做,因为只有理想情况下你才达到这个级别 - 但由于这是一个全新的项目,我们可以在 9 时感到非常舒服(毕竟技术债务没有那么多)。

接下来,ignoreErrorsexcludePaths 这两个选项允许我们告诉 PHPStan 忽略我们不感兴趣的文件或特定的错误,比如现阶段我们无法控制或修复的错误。也许你正在重构一些业务并且遇到了错误。你可能正在重构这段代码,以便稍后进行静态分析,那你可以通过这个配置,让 PHPStan 在你结束重构前,忽略相关的错误。

includes 包含基本的 phpstan 的规则。parameters 配置参数,第一个选项 paths 配置 phpstan 检查的目录——在我的例子中,我只对应用程序代码所在的 app 目录进行检查,当然您也可以配置其他目录。 level 配置级别,PHPStan 可以配置各种级别,0 是最低的,9 目前是最高的。如您所见,我已将级别设置为 9,我建议将级别设置为 9。接下来有 ignoreErrorsexcludePaths

보시다시피 우리는 레벨을 9로 설정했습니다. 이상적으로는 이 레벨에 도달할 수 있기 때문에 기존 앱에서 이 작업을 수행하는 것이 좋습니다. 그러나 이것은 완전히 새로운 프로젝트이므로 9에서 할 수 있습니다. 매우 편안합니다( 결국 기술 부채는 그다지 많지 않습니다.)

다음으로, ignoreErrorsexcludePaths 두 가지 옵션을 사용하면 PHPStan이 제어할 수 없거나 관심이 없는 파일이나 특정 오류를 무시하도록 지시할 수 있습니다. 이 단계에서 실수를 수정하세요. 일부 비즈니스를 리팩토링하는 중에 버그가 발생할 수도 있습니다. 나중에 정적 분석을 위해 이 코드를 리팩토링할 수 있으며, 이 구성을 사용하면 리팩토링을 완료하기 전에 PHPStan이 관련 오류를 무시하도록 할 수 있습니다.

includes에는 기본 phpstan 규칙이 포함되어 있습니다. parameters 구성 매개변수, 첫 번째 옵션 paths는 phpstan이 확인하는 디렉터리를 구성합니다. 제 경우에는 애플리케이션 코드가 있는 app만 확인했습니다. > 확인할 디렉터리는 물론 다른 디렉터리도 구성할 수 있습니다. <code>level 구성 수준, PHPStan은 다양한 수준으로 구성할 수 있으며, 0이 가장 낮고 9가 현재 가장 높습니다. 보시다시피 저는 레벨을 9로 설정했는데, 레벨을 9로 설정하는 걸 추천드립니다. 다음에는 PHPStan이 확인되지 않은 파일이나 특정 오류, 또는 지금 확인할 필요가 없는 파일과 오류를 무시하도록 지시하는 ignoreErrorsexcludePaths가 있습니다. 예를 들어, 코드가 리팩터링되고 있으며 완료될 때까지 오류를 무시하고 완료된 후 정적 분석을 수행하려고 합니다.

이제 기본 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(&#39;api&#39;, 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(&#39;bookmarks&#39;, static function (Blueprint $table): void {
    $table->id();

    $table->string(&#39;name&#39;);
    $table->string(&#39;url&#39;);

    $table->boolean(&#39;starred&#39;)->default(false);

    $table->foreignId(&#39;user_id&#39;)->index()->constrained()->cascadeOnDelete();

    $table->timestamps();
});

将以下代码添加到我们的模型中:

class Bookmark extends Model
{
    use HasFactory;

    protected $fillable = [
        &#39;name&#39;,
        &#39;url&#39;,
        &#39;starred&#39;,
        &#39;user_id&#39;,
    ];

    protected $casts = [
        &#39;starred&#39; => &#39;boolean&#39;,
    ];

    /**
     * @return BelongsTo
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(
            related: User::class,
            foreignKey: &#39;user_id&#39;,
        );
    }
}

从上面可以看出,我们在这里唯一关心的是名称、url,如果用户想要加星标/收藏书签并且该书签属于用户。现在我们可以把它留在这里,但我个人喜欢将类型定义添加到我的模型属性中——因为目前在 Laravel 9 中我无法输入提示它们。因此,重构你的模型,使其如下所示:

class Bookmark extends Model
{
    use HasFactory;

    /**
     * @var array<int,string>
     */
    protected $fillable = [
        &#39;name&#39;,
        &#39;url&#39;,
        &#39;starred&#39;,
        &#39;user_id&#39;,
    ];

    /**
     * @var array<string,string>
     */
    protected $casts = [
        &#39;starred&#39; => &#39;boolean&#39;,
    ];

    /**
     * @return BelongsTo
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(
            related: User::class,
            foreignKey: &#39;user_id&#39;,
        );
    }
}

我们在这里所做的只是告诉 PHP 和我们的 IDE,可填充数组是一个没有键的字符串数组——这意味着它将默认为整数。然后我们的 casts 数组是一个带键的字符串数组,其中的键也是字符串。现在,即使在没有类型定义的情况下运行静态分析,它也不会失败 - 但这是一个很好的实践,以便你的 IDE 在你工作时拥有尽可能多的信息。

让我们继续处理路由和控制器,以便我们可以继续运行静态分析检查。现在我是可调用控制器的忠实粉丝——我发现它们非常适合我的代码风格,但是你可能不喜欢它们或有不同的偏好,所以如果你是的话,下一部分可以随意偏离我的编码风格,会让你更舒服。

我们现在将创建一个控制器,运行以下 artisan 命令来为书签创建索引控制器:

php artisan make:controller Bookmarks/IndexController --invokable

这是我们路由所需的索引控制器,所以我们可以去添加一个新的路由组在 routes/web.php

Route::middleware([&#39;auth&#39;])->prefix(&#39;bookmarks&#39;)->as(&#39;bookmarks:&#39;)->group(static function (): void {
    Route::get(&#39;/&#39;, App\Http\Controllers\Bookmarks\IndexController::class)->name(&#39;index&#39;);
});

添加在在我们的 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: &#39;bookmarks.list&#39;,
            data: [
                &#39;bookmarks&#39; => Bookmark::query()
                    ->where(&#39;user_id&#39;, $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(&#39;user_id&#39;, 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 learnku.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제