7
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Laravel #2Advent Calendar 2019

Day 19

PHPStanによる静的解析をLaravelアプリケーションに導入するためにやったこと(レベル1編)

Last updated at Posted at 2019-12-18

この記事はLaravel #2 Advent Calendar 2019の19日目です。
そして去年のアドベントカレンダーの記事の続きです。

PHPStanによる静的解析をLaravelアプリケーションに導入するためにやったこと


はじめに

弊社サービス「リネット1」のLaravelアプリケーションのPHPStanをレベル1に上げました。
そのためにやったことについて書きます。

使用しているソフトウェアのバージョン

この記事で使用しているソフトウェアのバージョンは下記の通りです。

ソフトウェア バージョン
PHP 7.2.12
Laravel 5.5.44
PHPStan 0.10.5
Laravel 5 IDE Helper Generator 2.5.1

Laravel 5.6以上を使っている人は、この記事を読まずLarastanを使うのが良いと思います。
Larastanを使えばこの記事でやっているようなことを自分でやらなくて良いはずです。多分。

Laravel 5.5を使っている人には参考になると思います。

PHPStanのレベル1で検知できること

PHPStanをレベル1にすると、下記のことが検知可能になります。2

1. 存在しないマジックメソッドとマジックプロパティの呼び出し

2. 未定義の可能性がある変数の使用

if ($flag) {
    $hoge = 'hoge';
}
echo $hoge; // $flagの値次第で未定義の可能性がある
Variable $hoge might not be defined.

3. 存在しない定数の使用

date(w); // date('w')の間違い
Constant w not found.

4. 無駄なisset関数の使用

$hoge = 'hoge';
isset($hoge); // 常にtrueになるので無駄
Variable $hoge in isset() always exists and is not nullable.

1を除いては、検知されたら粛々と修正すればOKです。

しかし、1は誤検知が多いと思います。
Eloquentのマジックメソッド(findwhere)が誤検知されてしまいますし、自分でマジックメソッドやマジックプロパティを使っている箇所も同様です。

マジックメソッドやマジックプロパティの存在をPHPStanに教える方法

1. エクステンションを書く

PHPStanにはエクステンションという機構があります。3
エクステンションを書くことで、マジックメソッドやマジックプロパティの存在をPHPStanに教えることができます。

リネットでは次のようなエクステンションを書いてみました。
これがベストな書き方なのかはまったく自信がありませんので参考程度でお願いします。

Eloquentモデルのメソッド・リフレクション用のエクステンション
<?php
declare(strict_types=1);

use Eloquent;
use Illuminate\Database\Eloquent\Model;
use PHPStan\Analyser\OutOfClassScope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Type\ObjectType;

class EloquentModelMethodsClassReflectionExtension implements MethodsClassReflectionExtension
{
    public function hasMethod(ClassReflection $classReflection, string $methodName): bool
    {
        // 無限ループ防止
        if ($classReflection->getName() == Eloquent::class) {
            return false;
        }

        if (!$this->isEloquentModel($classReflection)) {
            return false;
        }

        return $this->findMethod($methodName) !== null;
    }

    public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
    {
        assert($this->isEloquentModel($classReflection));
        $method = $this->findMethod($methodName);
        assert(null !== $method);

        return $method;
    }

    private function findMethod(string $methodName): ?MethodReflection
    {
        $type = new ObjectType(Eloquent::class);

        if (!$type->hasMethod($methodName)) {
            return null;
        }

        return $type->getMethod($methodName, new OutOfClassScope());
    }

    private function isEloquentModel(ClassReflection $classReflection): bool
    {
        $parents = $classReflection->getParents();

        foreach ($parents as $parent) {
            // \Illuminate\Database\Eloquent\ModelのサブクラスであればEloquentモデル
            if ($parent->getName() == Model::class) {
                return true;
            }
        }

        return false;
    }
}
リネットのクラスのメソッド・リフレクション用のエクステンション
<?php
declare(strict_types=1);

use PHPStan\Analyser\OutOfClassScope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Type\ObjectType;

class LenetMethodsClassReflectionExtension implements MethodsClassReflectionExtension
{
    private $reflect = [
        // リフレクション元 => リフレクション先
        SomePresenter::class => SomeModel::class,
    ];

    public function hasMethod(ClassReflection $classReflection, string $methodName): bool
    {
        $reflectFrom = $classReflection->getName();
        $reflectTo = $this->reflect[$reflectFrom] ?? null;

        if (is_null($reflectTo)) {
            return false;
        }

        return $this->findMethod($methodName, $reflectTo) !== null;
    }

    public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
    {
        $reflectFrom = $classReflection->getName();
        assert(isset($this->reflect[$reflectFrom]));
        $reflectTo = $this->reflect[$reflectFrom];

        $method = $this->findMethod($methodName, $reflectTo);
        assert(null !== $method);

        return $method;
    }

    private function findMethod(string $methodName, string $reflectTo): ?MethodReflection
    {
        $type = new ObjectType($reflectTo);

        if (!$type->hasMethod($methodName)) {
            return null;
        }

        return $type->getMethod($methodName, new OutOfClassScope());
    }
}

2. PHPDocコメントを書く

PHPStanはPHPDocコメントを理解します。
エクステンションではなくPHPDocコメントでマジックメソッドやマジックプロパティの存在を教えることもできます。

リネットではPHPで列挙型(enum)を作るで紹介されている列挙型を利用させてもらっていますが、列挙型のファクトリメソッドについては列挙可能なのでPHPDocコメントを書いています。

<?php
declare(strict_types=1);

/**
 * @method static self irui()
 * @method static self futon()
 * @method static self hokan()
 * @method static self kutsu()
 */
class ServiceCode
{
    use EnumTrait;

    private const ENUM = [
        'irui'  => '1',
        'futon' => '2',
        'hokan' => '3',
        'kutsu' => '5',
    ];
}

こちらのブログにはエクステンションで対応する方法が紹介されていました。
https://medium.com/@hatajoe/how-to-use-phpstan-940ba1de6832

まとめ

Laravel 5.5のアプリケーションのPHPStanをレベル1に上げるためにやったことを書きました。
レベル2に上げる頃にはリネットもLaravel 6になっていると思うので、Larastanを使っていると思います。多分。

  1. 宅配クリーニングの『リネット』」「クリーニング×保管の『リネット保管」「布団クリーニングの『ふとんリネット』」「靴クリーニングの『くつリネット』」の4サービス。

  2. ソースコードを読む限りそう見えますが、間違っているかもしれません。ドキュメントがほしい。

  3. https://github.com/phpstan/phpstan#extensibility

7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?