Larastan(phpstan)で遭遇したエラーまとめ
エラー内容
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);
}
詳しくはこちら
エラー内容
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();
エラー内容
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;
エラー内容
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();
エラー内容
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()
]);
});
}
エラー内容
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));
}
}
}
解決策
$handler
に Monolog\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));
}
}
}
エラー内容
Macro登録した関数(wareki()
)なんて知らないぞって怒られた。
Call to an undefined method Illuminate\Support\Carbon::wareki().
原因
./vendor/nesbot/carbon/extension.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
エラー内容
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);
エラー内容
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
を使用していなかったことが原因
/**
* @param Illuminate\Database\Query\Builder $query
* @return CustomBuilder<User>
*/
public function newEloquentBuilder($query): CustomBuilder
{
return new CustomBuilder($query);
}
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
を使用する
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();
}
エラー内容
Relation 'tags' is not found in App\Models\Post model.
原因
Postテーブル内で定義しているリレーションメソッドに戻り値の適切な型が付与されていないことが原因
public function tags()
{
return $this->belongsToMany(Tag::class);
}
解決策
適切な型を付与する
今回の場合は BelongsToMany
を付与
- public function tags()
+ public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}
エラー内容
Caught class Swift_RfcComplianceException not found.
原因
現在は使用されていない例外を使用しているのが原因
try {
// some actions
} catch (\Swift_RfcComplianceException $e) {
Log::error($e);
}
解決策
Swift_RfcComplianceException
(Swift Mailer)をRfcComplianceException
(Symfony Mailer)に置き換える
+ use Symfony\Component\Mime\Exception\RfcComplianceException;
try {
// some actions
- } catch (\Swift_RfcComplianceException $e) {
+ } catch(RfcComplianceException $e)
Log::error($e);
}
エラー内容
Class App\Models\Tag referenced with incorrect case: App\Models\tag\
原因
クラス名の大文字小文字の区別が間違っている
public function tags()
{
// tag::class ❌
return $this->beongsToMany(tag::class);
}
解決策
大文字小文字をきちんと区別してクラス名を記載する
public function tags(): BelongsToMany
{
- return $this->belongsToMany(tag::class);
+ return $this->belongsToMany(Tag::class);
}
エラー内容
Called 'count' on Laravel collection, but could have been retrieved as a query
原因
SQL側でカウントできる状態だが、無駄にCollectionを生成してから要素をカウントしている
public function countCompletedSubscription()
{
return Subscription::query()
->where('status', 'completed')
->pluck('subscriptions.id')
->count();
}
解決策
QueryBuilderのcount()メソッドを使用して、直接SQL側でカウントする
public function tags(): BelongsToMany
{
return Subscription::query()
->where('status', 'completed')
- ->pluck('subscriptions.id')
- ->count();
+ -> count('subscriptions.id');
}
エラー内容
Call to an undefined static method App\Providers\AppServiceProvider::getInstances
原因
callback内の$thisの型が何かをPHPStanに伝えていないのが原因
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に型を知らせてあげる
public function boot(): void
{
Enum::macro('getOptions', function () {
+ /** @var Enum $this **/
return collect($this->getInstances())
->mapWithKeys(function ($enum) {
return [$enum->value => $enum->description];
});
});
}
エラー内容
Access to an undefined property App\Http\Resources\UserResource::$id.
原因
Resourceクラス内の$this
がどのモデルクラスを指すのか示していない
<?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に型を知らせてあげる
<?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,
];
}
}
エラー内容
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側がカラム名の変更を認識できなかった模様
結構特殊ケースかも。。
<?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
でキャッシュを消さないといけないかも。
<?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();
+ });
}
}