Theme
SD MILIEU

2018-9-11

LaravelのModelクラス等にコード補完が働かない理由

PHPStorm を使い初めたのですが、Laravel においてコード補完が働かない場合がちょくちょくあります。

例えば、php artisan make:model Worldで作成した World クラスに関して、Controller で

World::where(~)

と書くと、Method where not found ~といった警告が PHPStorm で出ます。

出るのですが、このコードは正常に動作しますし、公式ドキュメントでもこの書き方が例として書かれています。

World クラスの親クラスである Model クラスの中身を見ても、whereメソッドなど存在していません。(存在していれば当然の如く補完が働きます。)

ではなぜ前述のコードが動作してるのかというと、Model クラスにおいて__callおよび__callStaticメソッドを定義しているからです。

__callはそのクラスにおいて定義されていないメソッドが呼ばれた際に、__callStaticは定義されていない静的メソッドが呼ばれた際に呼ばれるメソッドになります。

Model クラスがそれをどのように使ってるかというと、簡単なコードにすると以下のような仕組みになります。

<?php

class Builder
{
    public function where($a, $b)
    {
        echo "where(${a}, ${b}) called." . PHP_EOL;
    }
}

abstract class Model
{
    public function newQuery()
    {
        return new Builder();
    }

    public function __call($name, $arguments)
    {
        return $this->newQuery()->$name(...$arguments);
    }

    public static function __callStatic($name, $arguments)
    {
        return (new static)->$name(...$arguments);
    }
}

class World extends Model
{
}

World::where('id', '1');

流れとしては、まずWorld::whereを実行しようとしますが、World 及び Model にstatic whereメソッドが存在していないので__callStaticが呼ばれます。__callStatic内のnew staticは呼び出し元のクラスのインスタンスを生成する処理です。(今回の場合は World クラスのインスタンスを生成しています。)

new staticにより World クラスのインスタンスが生成されたので、次は static でない where メソッドを探しますが、これもないので__callメソッドに入ります。__callメソッドでは Builder インスタンスを生成して返すnewQueryメソッドが呼ばれ、それ経由で Builder クラスの where メソッドが呼ばれます。

参考記事

Laravel コードで見るファサードクラスの仕組み - Shin x blog

今回の話に Facade は出てこないのですが、__callStaticを使って実現する辺りが似ています。

ちなみに、Routeクラスも各種メソッドが直接的には実装されておらず、親クラスである Facade クラスの__callStatic経由で\Illuminate\Routing\Routerのメソッドが呼ばれているので補完が働かなさそうですが、

<?php

namespace Illuminate\Support\Facades;

/**
 * @method static \Illuminate\Routing\Route get(string $uri, \Closure|array|string|null $action = null)
 * @method static \Illuminate\Routing\Route post(string $uri, \Closure|array|string|null $action = null)
 * @method static \Illuminate\Routing\Route put(string $uri, \Closure|array|string|null $action = null)
 * @method static \Illuminate\Routing\Route delete(string $uri, \Closure|array|string|null $action = null)
 * @method static \Illuminate\Routing\Route patch(string $uri, \Closure|array|string|null $action = null)
 * @method static \Illuminate\Routing\Route options(string $uri, \Closure|array|string|null $action = null)
 * @method static \Illuminate\Routing\Route any(string $uri, \Closure|array|string|null $action = null)
 * @method static \Illuminate\Routing\Route match(array|string $methods, string $uri, \Closure|array|string|null $action = null)
 * @method static \Illuminate\Routing\RouteRegistrar prefix(string  $prefix)
 * @method static \Illuminate\Routing\RouteRegistrar where(array  $where)
 * @method static \Illuminate\Routing\PendingResourceRegistration resource(string $name, string $controller, array $options = [])
 * @method static \Illuminate\Routing\PendingResourceRegistration apiResource(string $name, string $controller, array $options = [])
 * @method static void apiResources(array $resources)
 * @method static \Illuminate\Routing\RouteRegistrar middleware(array|string|null $middleware)
 * @method static \Illuminate\Routing\Route substituteBindings(\Illuminate\Support\Facades\Route $route)
 * @method static void substituteImplicitBindings(\Illuminate\Support\Facades\Route $route)
 * @method static \Illuminate\Routing\RouteRegistrar as(string $value)
 * @method static \Illuminate\Routing\RouteRegistrar domain(string $value)
 * @method static \Illuminate\Routing\RouteRegistrar name(string $value)
 * @method static \Illuminate\Routing\RouteRegistrar namespace(string $value)
 * @method static \Illuminate\Routing\Router|\Illuminate\Routing\RouteRegistrar group(array|\Closure|string $attributes, \Closure|string $routes)
 * @method static \Illuminate\Routing\Route redirect(string $uri, string $destination, int $status = 301)
 * @method static \Illuminate\Routing\Route view(string $uri, string $view, array $data = [])
 * @method static void bind(string $key, string|callable $binder)
 * @method static \Illuminate\Routing\Route current()
 * @method static string|null currentRouteName()
 * @method static string|null currentRouteAction()
 *
 * @see \Illuminate\Routing\Router
 */
class Route extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'router';
    }
}

上記のようにコメントが書かれているのが理由かこちらはコード補完が働きました。

で、どうやれば補完が効くようになる?

GitHub - barryvdh/laravel-ide-helper: Laravel IDE Helper

こちらの ide-helper を使います。

手順

公式を毎回見直すべきだが、一応。

# 仮想環境を使っているなら、事前に仮想環境にログインしておくこと

composer require --dev barryvdh/laravel-ide-helper
composer require doctrine/dbal

php artisan ide-helper:generate
# --dirには、モデルが配置されているディレクトリを入力
# こちらの作業は、Modelが増えるたびに毎回行う必要がある
php artisan ide-helper:models --dir="app/Models"

Fluent クラスの補完

さらに、Fluent クラスの補完をきくようにするには以下の作業

ex: Fluentは例えばマイグレーションファイルで
    $table->foreign('type_id')->references('id')->on('types');
    みたいなことをする際にforeignが返すクラス

まず、config/ide-helper.phpが無ければ生成

php artisan vendor:publish --provider="Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider" --tag=config

config/ide-helper.phpにて以下のように変更

'include_fluent' => true,

で、再ジェネレート

php artisan ide-helper:generate