ホームページ  >  記事  >  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 で適切な静的分析カバレッジを取得するのに苦労してきました。 Larastans ルールにより、コードベースにより多くの静的分析を適用できるようになり、その結果、自分のコードにさらに自信を持つことができました。現在、PHP 8.1 と Laravel 9 を使用しています。膨大な数の素晴らしいツールを自由に使えるおかげで、自分が書くコードにこれまで以上に自信が持てるようになりました。

このチュートリアルでは、Larastan を新しい Laravel プロジェクトに追加し、レベルを最高に設定する手順を説明します。

まず、larastan-test という名前の新しい Laravel プロジェクトを作成します:

laravel new larastan-test

新しいプロジェクトを作成した後、次の Composer コマンドを実行して Larastan をインストールします:

composer require nunomaduro/larastan --dev

開発依存関係が存在する理由は、運用環境では静的分析を実行すべきではないためです。これは、コードが可能な限り安全であることを確認するための開発目的のみに使用されます。 PHPStan は、yaml に似た neon と呼ばれる構成形式を使用します。したがって、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 歳です (結局、技術的負債はそれほど多くありません)。

次に、

ignoreErrorsexcludePaths これら 2 つのオプションを使用すると、関心のないファイルや特定のエラー (たとえば、次のエラー) を無視するように PHPStan に指示できます。この段階では制御できないか、バグが修正されています。もしかしたら、ビジネスをリファクタリングしていてバグに遭遇したかもしれません。後で静的分析のためにこのコードをリファクタリングする場合は、この設定を使用して、リファクタリングが完了する前に PHPStan に関連エラーを無視させることができます。

includes 基本的な phpstan ルールが含まれています。 parameters 設定パラメータ、最初のオプション paths phpstan がチェックするディレクトリを設定します - 私の場合、アプリケーション コードが配置されている app ディレクトリのみをチェックしました チェック, もちろん、他のディレクトリも設定できます。 level 構成レベル。PHPStan はさまざまなレベルで構成できます。現在、0 が最低、9 が最高です。ご覧のとおり、レベルを 9 に設定しました。レベルを 9 に設定することをお勧めします。次に、ignoreErrorsexcludePaths があり、チェックされていないファイルや特定のエラー、あるいは現在チェックする必要のないファイルやエラーを無視するように 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 アプリケーションで 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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はlearnku.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。