Open17

Larastan(phpstan)で遭遇したエラーまとめ

wadakatuwadakatu

エラー内容

Call to private method orderBy() of parent class Illuminate\Database\Eloquent\Builder<Illuminate\Database\Eloquent\Model>.

原因

原因は以下の2つ

  • Custom Eloquent Builderクラスに@annotation@templateを記述していない
  • newEloquentBuilderメソッドに、@param@returnを記述していない
    class KeywordBuilder extends Builder
    {
    }

    public function newEloquentBuilder($query): KeywordBuilder
    {
        return new KeywordBuilder($query);
    }

解決策

@annotation, @template, @param, @returnをそれぞれ追加すればOK

/**
+* @template TModelClass of \Illuminate\Database\Eloquent\Model
+* @extends Builder<TModelClass>
 */
class CustomBuilder extends Builder
{
}

/**
+* @param \Illuminate\Database\Query\Builder $query
+* @return CustomBuilder<YourModelWithCustomBuilder>
 */
public function newEloquentBuilder($query): CustomBuilder
{
    return new CustomBuilder($query);
}

詳しくはこちら
https://github.com/nunomaduro/larastan/blob/master/UPGRADE.md#custom-eloquent-builders

wadakatuwadakatu

エラー内容

 Call to an undefined method Illuminate\Database\Eloquent\Relations\BelongsTo::withTrashed().

原因

withメソッドでEager Loadingの条件を決めるときに、BelongsToクラスの変数からwithTrashedメソッドを呼び出していること
withTrashedメソッドは、SoftDeletesトレイトをuseすると利用可能だが、BelongsToクラス内にはそのトレイトはuseされていないのでwithTrashedメソッドは使えない。

public function staff(): BelongsTo{
    return $this->belongsTo(Staff::class);
}

StaffMemo::query()
    ->with(['staff' => fn (BelongsTo $builder) => $builder->withTrashed()->select(['id', 'first_name', 'last_name'])])
    ->get();

解決策

1. BelongsToのタイプヒンティングをやめる

StaffMemo::query()
-    ->with(['staff' => fn (BelongsTo $builder) => $builder->withTrashed()->select(['id', 'first_name', 'last_name'])])
+    ->with(['staff' => fn ($builder) => $builder->withTrashed()->select(['id', 'first_name', 'last_name'])])
    ->get();

2. withTrashed()を含むリレーション用メソッドを作成し、使用する

public function staffWithTrashed(): BelongsTo{
    return $this->belongsTo(Staff::class)->withTrashed();
}

StaffMemo::query()
-    ->with(['staff' => fn (BelongsTo $builder) => $builder->select(['id', 'first_name', 'last_name'])])
+    ->with(['staffWithTrashed' => fn (BelongsTo $builder) => $builder->select(['id', 'first_name', 'last_name'])])
    ->get();
wadakatuwadakatu

エラー内容

Cannot access property $uri on object|string. 

原因

Illuminate\Http\Requestクラスのroute()から直接$uriを呼び出していた
route()の戻り値は「Illuminate\Routing\Route, null, object, string」と多種多様な値が返ってくる可能性がある。
nullの場合は、ヌルセーフ演算子を使えば良いが、objectとstringの場合はそうはいかない。

// object | stringが帰ってきた際に、uriプロパティが取れない
$request->route()->uri

解決策

$request->route()の戻り値が、Illuminate\Routing\Routeであることをタイプヒンティングやinstanceofを使って確定させた後、uriプロパティを呼び出し

if ($route instanceof Illuminate\Routeing\Route) {
    $url = $route->uri;
}

or

 /** @var Route $route */
 $route = $request->route();

 $url = $route->uri;
wadakatuwadakatu

エラー内容

Cannot call method getName() on object|string. 

原因

Illuminate\Http\Requestクラスのroute()から直接getName()を呼び出していた
route()の戻り値は「Illuminate\Routing\Route, null, object, string」と多種多様な値が返ってくる可能性がある。
nullの場合は、ヌルセーフ演算子を使えば良いが、objectとstringの場合はそうはいかない。

// object | stringが帰ってきた際に、uriプロパティが取れない
$request->route()->getName();

解決策

$request->route()の戻り値が、Illuminate\Routing\Routeであることをタイプヒンティングやinstanceofを使って確定させた後、getName()を呼び出し

if ($route instanceof Illuminate\Routeing\Route) {
    $name = $route->getName();
}

or

/** @var Route $route */
$route = $request->route();
$name = $route->getName();
wadakatuwadakatu

エラー内容

Call to an undefined method Illuminate\Contracts\Validation\Validator::setData().
Call to an undefined method Illuminate\Contracts\Validation\Validator::getData().

原因

Illuminate\Contracts\Validation\Validatorインターフェースから上記2メソッドを呼び出していたこと

protected function withValidator(Illuminate\Contracts\Validation\Validator $validator): void
    {
        $validator->after(function (Illuminate\Contracts\Validation\Validator $validator) {
                $validator->setData([
                     'important_data' => $validator->getData()
                ]);
        });
    }

解決策

Illuminate\Validation\ValidatorクラスからsetData()getData()を呼び出すようにする

+ protected function withValidator(Illuminate\Validation\Validator $validator): void
- protected function withValidator(Illuminate\Contracts\Validation\Validator $validator): void
    {
+        $validator->after(function (Illuminate\Validation\Validator $validator) {
-        $validator->after(function (Illuminate\Contracts\Validation\Validator $validator) {
                $validator->setData([
                     'important_data' => $validator->getData()
                ]);
        });
    }
wadakatuwadakatu

エラー内容

Call to an undefined method Monolog\Handler\HandlerInterface::setFormatter().  

原因

ログの形式をJSON形式に変更する際に、$handlerの型指定をしていなかった。

use App\Logging\Formatters\CustomJsonFormatter;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Log\Logger;

class JsonFormatCustomizer
{
    /**
     * @throws BindingResolutionException
     */
    public function __invoke(Logger $logger): void
    {
        foreach ($logger->getHandlers() as $handler) {
            $handler->setFormatter(app()->make(CustomJsonFormatter::class));
        }
    }
}

解決策

$handlerMonolog\Handler\FormattableHandlerInterface;の指定を加える

use App\Logging\Formatters\CustomJsonFormatter;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Log\Logger;
+ use Monolog\Handler\FormattableHandlerInterface;

class JsonFormatCustomizer
{
    /**
     * @throws BindingResolutionException
     */
    public function __invoke(Logger $logger): void
    {
+       /** @var FormattableHandlerInterface $handler */
        foreach ($logger->getHandlers() as $handler) {
            $handler->setFormatter(app()->make(CustomJsonFormatter::class));
        }
    }
}
wadakatuwadakatu

エラー内容

Macro登録した関数(wareki())なんて知らないぞって怒られた。

Call to an undefined method Illuminate\Support\Carbon::wareki().  

原因

./vendor/nesbot/carbon/extension.neonphpstan.neonに導入していなかった

解決策

phpstan.neon
    - ./vendor/phpstan/phpstan/conf/bleedingEdge.neon
    - ./vendor/nunomaduro/larastan/extension.neon
+   - ./vendor/nesbot/carbon/extension.neon
parameters:
    level: 2
    paths:
        - app
    checkMissingIterableValueType: false

https://github.com/nunomaduro/larastan/issues/1050#issuecomment-986202192

wadakatuwadakatu

エラー内容

Binary operation "+" between non-falsy-string and 1 results in an error. 

non-falsy-stringとは・・・?

PHPStanのドキュメント曰く。。。
non-falsy-string (also known as truthy-string) is any string that is true after casting to boolean.

日本語訳すると
non-falsy-stringとは、booleanにキャストした後に真となる文字列です。

原因

数値計算と文字列結合を同時に行なっているところで、()を使って計算部分を囲っていなかったから

$a = 'a';
$aa = 10

$b = $a . $aa + 1; // $aa + 1 が原因

解決策

数値計算部分を()で囲む

$a = 'a';
$aa = 10;

$b = $a . ($aa + 1);
wadakatuwadakatu

エラー内容

Anonymous function should return Illuminate\Database\Eloquent\Builder<Illuminate\Database\Eloquent\Model> but returns Illuminate\Database\Eloquent\Builder<App\Models\User>.

原因

Userテーブルからレコード取得するコードを書く際に、when関数の第二引数の無名関数にカスタムしているIlluminate\Database\Eloquent\Builderを使用していなかったことが原因

User.php
/**
     * @param  Illuminate\Database\Query\Builder  $query
     * @return CustomBuilder<User>
     */
    public function newEloquentBuilder($query): CustomBuilder
    {
        return new CustomBuilder($query);
    }
GetUser.php
public function getUsers(array $ids){
    return App\Models\User::query()
               ->when(true, fn(Illuminate\Database\Eloquent\Builder $builder) => $builder->whereIn('id', $ids))
               ->get();   
}

解決策

Illuminate\Database\Eloquent\Builderの代わりに、CustomBuilderを使用する

GetUser.php
public function getUsers(array $ids){
    return App\Models\User::query()
-              ->when(true, fn(Illuminate\Database\Eloquent\Builder $builder) => $builder->whereIn('id', $ids))
+              ->when(true, fn(CustomBuilder $builder) => $builder->whereIn('id', $ids))
               ->get();   
}
wadakatuwadakatu

エラー内容

Relation 'tags' is not found in App\Models\Post model.

原因

Postテーブル内で定義しているリレーションメソッドに戻り値の適切な型が付与されていないことが原因

Post.php
    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }

解決策

適切な型を付与する
今回の場合は BelongsToManyを付与

Post.php
-   public function tags()
+   public function tags(): BelongsToMany  
    {
        return $this->belongsToMany(Tag::class);
    }
wadakatuwadakatu

エラー内容

Caught class Swift_RfcComplianceException not found.

原因

現在は使用されていない例外を使用しているのが原因
https://symfony.com/blog/the-end-of-swiftmailer

Error.php
     try {
          // some actions
     } catch (\Swift_RfcComplianceException $e) {
         Log::error($e);
     }

解決策

Swift_RfcComplianceException(Swift Mailer)をRfcComplianceException(Symfony Mailer)に置き換える

Success.php
+    use Symfony\Component\Mime\Exception\RfcComplianceException;

     try {
          // some actions
-    } catch (\Swift_RfcComplianceException $e) {
+    } catch(RfcComplianceException $e)
         Log::error($e);
     }
wadakatuwadakatu

エラー内容

Class App\Models\Tag referenced with incorrect case: App\Models\tag\

原因

クラス名の大文字小文字の区別が間違っている

Post.php
    public function tags()
    {
        // tag::class ❌
        return $this->beongsToMany(tag::class);
    }

解決策

大文字小文字をきちんと区別してクラス名を記載する

Post.php

    public function tags(): BelongsToMany  
    {
-        return $this->belongsToMany(tag::class);
+        return $this->belongsToMany(Tag::class);
    }
wadakatuwadakatu

エラー内容

Called 'count' on Laravel collection, but could have been retrieved as a query

原因

SQL側でカウントできる状態だが、無駄にCollectionを生成してから要素をカウントしている

Post.php
    public function countCompletedSubscription()
    {
        return Subscription::query()
            ->where('status', 'completed')
            ->pluck('subscriptions.id')
            ->count();
    }

解決策

QueryBuilderのcount()メソッドを使用して、直接SQL側でカウントする

Post.php

    public function tags(): BelongsToMany  
    {
        return Subscription::query()
            ->where('status', 'completed')
-            ->pluck('subscriptions.id')
-            ->count();
+           -> count('subscriptions.id');
    }
wadakatuwadakatu

エラー内容

Call to an undefined static method App\Providers\AppServiceProvider::getInstances

原因

callback内の$thisの型が何かをPHPStanに伝えていないのが原因

AppServiceProvider.php
use BenSampo\Enum\Enum;

    public function boot(): void
    {
        Enum::macro('getOptions', function () {
            return collect($this->getInstances())
                ->mapWithKeys(function ($enum) {
                    return [$enum->value => $enum->description];
                });
        });
    }

解決策

/** @var Enum $this **/のように書いて、PHPStanに型を知らせてあげる

AppServiceProvider.php

    public function boot(): void
    {
        Enum::macro('getOptions', function () {
+           /** @var Enum $this **/  
            return collect($this->getInstances())
                ->mapWithKeys(function ($enum) {
                    return [$enum->value => $enum->description];
                });
        });
    }

https://github.com/nunomaduro/larastan/issues/166

wadakatuwadakatu

エラー内容

Access to an undefined property App\Http\Resources\UserResource::$id.

原因

Resourceクラス内の$thisがどのモデルクラスを指すのか示していない

UserResource
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id'                    => $this->id,
            'name'                  => $this->name,
        ];
    }
}

解決策

Resourceクラスの上に/** @mixin User **/のように書いて、PHPStanに型を知らせてあげる

AppServiceProvider.php

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;
+ use App\Models\User;

+ /** @mixin User */
class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id'                    => $this->id,
            'name'                  => $this->name,
        ];
    }
}

https://github.com/larastan/larastan/pull/584

wadakatuwadakatu

エラー内容

Access to an undefined property App\Models\User::$last_login_at.
  • 該当テーブルにlast_login_atカラムがあることは確認済
  • 該当モデルクラスにも$fillableプロパティ内にlast_login_atを記述済
  • カラム名をlast_login_onからlast_login_atに変更した履歴あり

原因

カラム名変更時のMigrationファイルの書き方が良くなかった
SQLを直に書いていたので、Larastan側がカラム名の変更を認識できなかった模様
結構特殊ケースかも。。

migration-before
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class ChangeLastLoginOnToUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        DB::statement("ALTER TABLE `users` CHANGE COLUMN `last_login_on` `last_login_at` timestamp NULL DEFAULT NULL");
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        DB::statement("ALTER TABLE `users` CHANGE COLUMN `last_login_at` `last_login_on` date DEFAULT NULL;");
    }
}

解決策

直接SQLを書くのではなく、Laravelが用意しているメソッドを使う
./vendor/bin/phpstan clear-result-cacheでキャッシュを消さないといけないかも。

migration-after
 <?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class ChangeLastLoginOnToUsersTable extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
-        DB::statement("ALTER TABLE `users` CHANGE COLUMN `last_login_on` `last_login_at` timestamp NULL DEFAULT NULL");
+        Schema::table('users', function (Blueprint $table) {
+            $table->renameColumn('last_login_on', 'last_login_at');
+            $table->timestamp('last_login_at')->nullable()->change();
+        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
-        DB::statement("ALTER TABLE `users` CHANGE COLUMN `last_login_at` `last_login_on` date DEFAULT NULL;");
+        Schema::table('users', function (Blueprint $table) {
+            $table->renameColumn('last_login_at', 'last_login_on');
+            $table->date('last_login_on')->nullable()->change();
+        });
    }
}
wadakatuwadakatu

エラー内容

Strict comparison using === between string and false will always evaluate to false.

stringfalseの比較だと絶対にfalseになるから意味ないよとのこと。

原因

base64_decode()は、第二引数にtrueが指定されていないとfalseが返却されることはない。
つまり、以下の例の場合戻り値は常にstringだけになる。

    public function decodeResult()
    {
        if($this->base64UrlDecode($input) === false){
            throw new Exception();
        }
       ...中略...
    }

    private function base64UrlDecode($inputStr): false|string
    {
        return base64_decode(strtr($inputStr, '-_,', '+/='));
    }

https://github.com/phpstan/phpstan/issues/3816
https://github.com/phpstan/phpstan/issues/1365

解決策

  1. 戻り値をfalse|stringからstringに変更する
    private function base64UrlDecode($inputStr): string
    {
        return base64_decode(strtr($inputStr, '-_,', '+/='));
    }

or

  1. base64_decode()の第二引数にtrueを指定する
    private function base64UrlDecode($inputStr): false|string
    {
        return base64_decode(strtr($inputStr, '-_,', '+/='), true);
    }

https://www.php.net/manual/ja/function.base64-decode.php