支持Laravel.io的持续发展 →

如何用Larastan将您的Laravel应用从0升级到9

11 Mar, 2024 7分钟阅读

在使用Laravel应用程序之前找到错误是可能的,归功于Larastan,这是一个PHPStan的外壳,旨在支持静态分析中的所有Laravel魔法。

在此,我将引导您通过安装Larastan的步骤,直到达到规则的9级,而不忽略任何内容。

根据Larastan的README,安装它的操作如下

  1. 运行composer require larastan/larastan:^2.0 --dev
  2. 在项目的根目录中添加一个phpstan.neonphpstan.neon.dist文件
includes:
    - vendor/larastan/larastan/extension.neon

parameters:

    paths:
        - app/

    # Level 9 is the highest level
    level: 5

如您所见,默认设置为检查级别5,但我们将将其更改为级别0。

在继续之前,我们需要知道Larastan在每个级别上检查什么

  1. 基本检查,未知类,未知函数,对$this调用的未知方法,传递给这些方法和函数的参数数量不正确,始终未定义的变量
  2. 可能未定义的变量,具有__call__get的类上的未知魔法方法和属性
  3. 检查所有表达式上的未知方法(不仅仅是$this),验证PHPDocs
  4. 返回类型,属性分配的类型
  5. 基本死代码检查 - 总是错误的instanceof和其他类型检查,死代码else分支,返回后的不可达代码等。
  6. 检查传递给方法和函数的参数的类型
  7. 报告缺少类型提示
  8. 报告部分错误的联合类型 - 如果您在联合类型上调用仅存在于其中一些类型上的方法,级别7开始报告此类错误;其他可能不正确的情况
  9. 报告在可空类型上调用的方法和访问的属性
  10. mixed类型严格要求 - 您可以与另一个mixed传递它的唯一操作

考虑到这些规则,假设我们有以下代码(为了简单起见,所有内容都在同一个文件中)

注意: 代码是为了能够得到所需的错误,以便向您展示如何操作而编写的,其中一些部分可以用更简单的方式来编写。

declare(strict_types=1);

use App\Http\Controllers\Controller;
use App\Models\Appointment;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;

class User extends Model
{
    public function appointments()
    {
        return $this->hasMany(Appointment::class);
    }
}

class UserDTO
{
    public function __construct(
        public $name,
        public $is_active,
    ) {
    }

    public function toArray()
    {
        return [
            'name' => $this->name,
            'is_active' => $this->is_active,
        ];
    }
}

class ShowUserQuery
{
    public function run($id)
    {
        $this->doSomething();

        return User::query()
            ->with('appointments')
            ->find($id);
    }
}

class UserController extends Controller
{
    public function show($id, ShowUserQuery $query)
    {
        return response()->json($query->run($id)->toArray());
    }

    public function store(Request $request)
    {
        $request->validate([
            'name' => ['required', 'max:250'],
            'is_active' => ['required', 'boolean'],
        ]);

        if (true) {
            return;
        }

        $isActive = $request->input('is_active');

        $userDTO = new UserDTO(
            $request->input('name'),
            $isActive
        );

        $user = User::create($userDTO->toArray());

        return $user;
    }
}

执行 ./vendor/bin/phpstan analyze 后,我们得到以下错误

  22     Call to an undefined method ShowUserQuery::doSomething().  
  24     Relation 'appointments' is not found in User model.              

为了修复这些错误,我们需要删除或定义未定义的方法,并为模型关系添加返回类型,得到以下代码

class User extends Model
{
    public function appointments(): HasMany
    {
        return $this->hasMany(Appointment::class);
    }
}

class ShowUserQuery
{
    public function run($id)
    {
        return User::query()
            ->with('appointments')
            ->find($id);
    }
}

直到第4级,我们没有得到任何错误,因为在这次操作中,我们没有违反第1级、第2级和第3级的规则。将级别改为4后,我们得到了以下错误

  43     If condition is always true.                           
  47     Unreachable statement - code above always terminates.

为了解决这个问题,我们需要删除控制器中 store 方法内的 if 语句,修复后得到的函数如下

public function store(Request $request)
{
    $data = $request->validate([
        'name' => ['required', 'max:250'],
        'is_active' => ['required', 'boolean'],
    ]);

    $isActive = $request->input('is_active');

    $userDTO = new UserDTO(
        $request->input('name'),
        $isActive
    );

    $user = User::create($userDTO->toArray());

    return $user;
}

对于第5级,我们一切顺利,但对于第6级,我们遇到了很多错误

  13     Method User::appointments() 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.                                                            
  21     Method ShowUserQuery::run() has no return type specified.                   
  21     Method ShowUserQuery::run() has parameter $id with no type specified.       
  31     Method UserController::show() has no return type specified.                 
  31     Method UserController::show() has parameter $id with no type specified.     
  36     Method UserController::store() has no return type specified. 

让我们开始修复问题。在这个级别,我们需要指定代码中所有内容的返回类型和参数类型。对于第一个错误,它要求我们在关系定义中指定相关模型类型。

修复问题后,我们得到了以下代码

class User extends Model
{
    /**
     * @return HasMany<Appointment>
     */
    public function appointments(): HasMany
    {
        return $this->hasMany(Appointment::class);
    }
}

class UserDTO
{
    public function __construct(
        public string $name,
        public bool $is_active,
    ) {
    }

    /**
     * @return array{name: string, is_active: bool}
     */
    public function toArray(): array
    {
        return [
            'name' => $this->name,
            'is_active' => $this->is_active,
        ];
    }
}

class ShowUserQuery
{
    public function run(int $id): ?User
    {
        return User::query()
            ->with('appointments')
            ->find($id);
    }
}

class UserController extends Controller
{
    public function show(int $id, ShowUserQuery $query): JsonResponse
    {
        return response()->json($query->run($id)->toArray());
    }

    public function store(Request $request): User
    {
        $request->validate([
            'name' => ['required', 'max:250'],
            'active' => ['required', 'boolean'],
        ]);
        $isActive = $request->input('is_active');

        $userDTO = new UserDTO(
            $request->input('name'),
            $isActive
        );

        $user = User::create($userDTO->toArray());

        return $user;
    }
}

对于第7级,我们很清楚,但对于第8级,很遗憾,我们已经不清楚

  37     Cannot call method toArray() on User|null.  

为了修复它,我们必须避免在可空类型上调用方法或属性。然后,这里的修复方法是

public function show(int $id, ShowUserQuery $query): JsonResponse
{
    $userArray = $query->run($id)?->toArray() ?? [];

    return response()->json($userArray);
}

最终,我们到达了最严格、最苛刻的第9级,其中有一些与混合值相关的错误。对于这个场景,我故意设置了变量 $isActive,以向您展示您可以通过两种方式修复相同的错误

  1. 使用 assert 方法和请求的 string 方法
public function store(Request $request): User
{
    $request->validate([
        'name' => ['required', 'max:250'],
        'active' => ['required', 'boolean'],
    ]);
    $isActive = $request->input('is_active');
    assert(is_bool($isActive));

    $userDTO = new UserDTO(
        $request->string('name')->toString(),
        $isActive
    );

    $user = User::create($userDTO->toArray());

    return $user;
}
  1. 使用请求的 stringboolean 方法
public function store(Request $request): User
{
    $request->validate([
        'name' => ['required', 'max:250'],
        'active' => ['required', 'boolean'],
    ]);

    $userDTO = new UserDTO(
        $request->string('name')->toString(),
        $request->boolean('is_active')
    );

    $user = User::create($userDTO->toArray());

    return $user;
}

修复了从第0级到第9级所有错误后,我们的最终代码将如下所示

<?php

declare(strict_types=1);

use App\Http\Controllers\Controller;
use App\Models\Appointment;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class User extends Model
{
    /**
     * @return HasMany<Appointment>
     */
    public function appointments(): HasMany
    {
        return $this->hasMany(Appointment::class);
    }
}

class UserDTO
{
    public function __construct(
        public string $name,
        public bool $is_active,
    ) {
    }

    /**
     * @return array{name: string, is_active: bool}
     */
    public function toArray(): array
    {
        return [
            'name' => $this->name,
            'is_active' => $this->is_active,
        ];
    }
}

class ShowUserQuery
{
    public function run(int $id): ?User
    {
        return User::query()
            ->with('appointments')
            ->find($id);
    }
}

class UserController extends Controller
{
    public function show(int $id, ShowUserQuery $query): JsonResponse
    {
        $userArray = $query->run($id)?->toArray() ?? [];

        return response()->json($userArray);
    }

    public function store(Request $request): User
    {
        $request->validate([
            'name' => ['required', 'max:250'],
            'active' => ['required', 'boolean'],
        ]);

        $userDTO = new UserDTO(
            $request->string('name')->toString(),
            $request->boolean('is_active')
        );

        $user = User::create($userDTO->toArray());

        return $user;
    }
}

通过这个例子,我希望您能够了解如何在Larastan和PHPStan的帮助下,在代码被执行之前使其无错误,并达到第9级。

最后更新于4个月前。

driesvints, alanmoe, rawphp, ricventu, sindhani, abdwhidd 喜欢了这篇文章

6
喜欢这篇文章? 让作者知道并给他们点赞!

你可能还喜欢的文章

2024年7月19日

无需 traits 标准化 API 响应

问题 我注意到,大多数用于 API 响应的库都是使用 traits 实现的,并且...

阅读文章
2024年7月17日

在您的 Laravel 项目中通过 Discord 通知收集反馈

如何在 Laravel 项目中创建一个反馈模块,并在收到消息时收到 Discord 通知...

阅读文章
2024年7月12日

Laravel 高级:您不知道存在的 10 个顶级验证规则

您知道 Laravel 中所有可用的验证规则吗?再想想吧!Laravel 有许多现成的...

阅读文章

我们想感谢这些 赞扬的公司 对我们的支持

您的标志在这里吗?

Laravel.io

Laravel 问题的解决方案、知识共享和社区建设的门户网站。

© 2024 Laravel.io - 所有权利保留。