At time of writting, there are three approaches.
But I use both first and third approach in the same project, because there are many Composer packages which don't use my second or third approach.
First approach is a workaround
Simply ignore Laravel's "magic" in phpstan.neon
file, like:
parameters:
paths:
- app
excludePaths:
- ./vendor/**
reportUnmatchedIgnoredErrors: false
ignoreErrors:
- '#Call to an undefined method#'
- '#Call to an undefined static method#'
Notes
You can customize above Regular-expression(s) to ignore only what's needed.
But PHP does not allow a class to have same name twice:
PHP Fatal error: Cannot redeclare App\Models\MyModel::active()
While Laravel's scope "magic" allows the same name from both "static" and "instance" context, like:
$query = MyModel::active();
$query = MyModel::where('id', '!=', 1)->active();
Meaning, even if you use doc-comment, like:
/**
* @method static active()
* @method active()
*/
class MyModel { ... }
It only changes phpstan
's message, to something like:
Static call to instance method App\Models\MyModel::active().
Second approach is a solution
For reasons mentioned in above note-section;
Prefix all uses of Laravel's "static scope magic" with "query()->", like change from:
MyModel::active();
Into:
MyModel::query()->active();
Finally, add doc-comment(s), like:
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
/**
* @method Builder|static active()
* @method static Builder|static query()
*/
class MyModel extends Model { ... }
Third approach is a trick
Basically, do everything that second approach says,
But instead of doc-comment(s), like most libraries, pair the scope-method with a normal-method which checks the same field(s) without any query, like:
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
/**
* @method static Builder|static query()
*/
class MyModel extends Model {
/**
* @return bool|Builder|static
*/
public function active()
{
return $this->status == 'active';
}
public function scopeActive(Builder $query): void
{
/** @var Builder|static $query */
$query->where('status', '=', 'active');
// NOTE: above `@var` is only required to chain other scope(s).
}
}
Note that above tricks both IDE and phpstan
into thinking that ->active()
may return Builder|static
, which allows chaining, like:
$query = MyModel::query()
->active()
->where(...);
using a scope inside scope
" or around "chaining a scope after another scope
" can be fixed by doc-commenting; see my answer's edit. – Spinthariscope