2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravel10.xのアクセサの記述がlaravel-ide-helperで補完可能なプロパティとして生えない件について備忘録

Last updated at Posted at 2024-01-30

Laravelのモデルクラスに記述できるアクセサについてIDE Helper Generator for Laravel(以後、laravel-ide-helper)を使用したときに期待するPHPDocが生えないことがあり調査しました。

アクセサについておさらい

  • Laravel8.xの頃とLaravel9.x以降でドキュメントに書いてあるアクセサの書き方が異なる
    • 8.xの頃はget〇〇Attributeのルールに則ったメソッドを定義し、任意の値を戻り値として返す
    • 9.x以降は\Illuminate\Database\Eloquent\Casts\Attributeのインスタンスを返す
      • 9.x以降のLaravelで8.xの頃のアクセサを記述することは可能
      • Attribute::make()のgetパラメータにセットする関数でアクセサとしてアクセスさせたい任意の値を戻り値として返す
8.xのアクセサ
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * ユーザーの名前の取得
     *
     * @param  string  $value
     * @return string
     */
    public function getFirstNameAttribute($value)
    {
        return ucfirst($value);
    }
}
9.x以降のアクセサ
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * ユーザーの名前の取得
     */
    protected function firstName(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => ucfirst($value),
        );
    }
}

問題点

  • Laravel10.xにおいて現形式のアクセサの記述を行った場合、PHPDocに期待する@property-readのプロパティが生えない
  • Laravel8.xの頃のget〇〇Attributeの記述を行った場合、PHPDocに期待する@property-readのプロパティが生える

検証

検証内容について

usersテーブルを使用しnameとemailの文字列を連結して返すようなアクセサについてget〇〇AttributeのアクセサとAttributeインスタンスを返すアクセサの両方で作成して検証を行いました。

2014_10_12_000000_create_users_table.php
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->rememberToken();
    $table->timestamps();
});
User.php
<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    // use宣言、$fillable、$hidden、$castsは省略

+    /**
+     * @comment ユーザー名とパスワードを連結して取得1
+     *
+     * @return string
+     */
+    public function getNameAndEmail1Attribute(): string
+    {
+        return "{$this->name} {$this->email}";
+    }
+
+    /**
+     * @comment ユーザー名とパスワードを連結して取得2
+     *
+     * @return \Illuminate\Database\Eloquent\Casts\Attribute
+     */
+    protected function nameAndEmail2(): Attribute
+    {
+        return Attribute::make(
+            get: fn () => "{$this->name} {$this->email}",
+        );
+    }
}

検証1 laravel-ide-helperの最新のリリースで検証

まず初めに、2024/01/30現在の最新のリリース(v2.13)で検証しました

$ composer require --dev barryvdh/laravel-ide-helper:^2.13

php artisan ide-helper:models --write --resetを実行したところUserモデルクラスの先頭に生えたPHPDocは以下のようになりました。

User.php
<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

+/**
+ * App\Models\User
+ *
+ * @property int $id
+ * @property string $name
+ * @property string $email
+ * @property \Illuminate\Support\Carbon|null $email_verified_at
+ * @property mixed $password
+ * @property string|null $remember_token
+ * @property \Illuminate\Support\Carbon|null $created_at
+ * @property \Illuminate\Support\Carbon|null $updated_at
+ * @property-read string $name_and_email1 ユーザー名とパスワードを連結して取得1
+ * @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
+ * @property-read int|null $notifications_count
+ * @property-read \Illuminate\Database\Eloquent\Collection<int, \Laravel\Sanctum\PersonalAccessToken> $tokens
+ * @property-read int|null $tokens_count
+ * @method static \Database\Factories\UserFactory factory($count = null, $state = [])
+ * @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder|User query()
+ * @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|User whereEmail($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|User whereEmailVerifiedAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|User whereId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|User whereName($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|User wherePassword($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|User whereRememberToken($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|User whereUpdatedAt($value)
+ * @mixin \Eloquent
+ */
class User extends Authenticatable
{
    // use宣言、$fillable、$hidden、$castsは省略

    /**
     * @comment ユーザー名とパスワードを連結して取得1
     *
     * @return string
     */
    public function getNameAndEmail1Attribute(): string
    {
        return "{$this->name} {$this->email}";
    }

    /**
     * @comment ユーザー名とパスワードを連結して取得2
     *
     * @return \Illuminate\Database\Eloquent\Casts\Attribute
     */
    protected function nameAndEmail2(): Attribute
    {
        return Attribute::make(
            get: fn () => "{$this->name} {$this->email}",
        );
    }
}
  • 8.xの頃の形式のアクセサを元にした@property-read string $name_and_email1 ユーザー名とパスワードを連結して取得1というPHPDocが生成されていることがわかります。
  • また、9.x以降の形式のアクセサを元にしたPHPDocが生成されていないこともわかります。

検証2 最新のmasterブランチのコミットIDで検証

2024/01/30現在の最新のmasterのコミットID(726e5955786969b7c650d989657b55c12d4521e1の状態をcomposer経由でインストール、検証しました。

$ composer require --dev barryvdh/laravel-ide-helper:dev-master#726e5955786969b7c650d989657b55c12d4521e1

単純に最新のmasterをインストールしたいときは以下(↑は今回の記事用)
$ composer require --dev barryvdh/laravel-ide-helper:dev-master

php artisan ide-helper:models --write --resetを実行したところUserモデルクラスの先頭に生えたPHPDocは以下のようになりました。

User.php
<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

/**
 * App\Models\User
 *
 * @property int $id
 * @property string $name
 * @property string $email
 * @property \Illuminate\Support\Carbon|null $email_verified_at
 * @property mixed $password
 * @property string|null $remember_token
 * @property \Illuminate\Support\Carbon|null $created_at
 * @property \Illuminate\Support\Carbon|null $updated_at
 * @property-read string $name_and_email1 ユーザー名とパスワードを連結して取得1
 * @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
 * @property-read int|null $notifications_count
 * @property-read \Illuminate\Database\Eloquent\Collection<int, \Laravel\Sanctum\PersonalAccessToken> $tokens
 * @property-read int|null $tokens_count
 * @method static \Database\Factories\UserFactory factory($count = null, $state = [])
 * @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery()
 * @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
 * @method static \Illuminate\Database\Eloquent\Builder|User query()
 * @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value)
 * @method static \Illuminate\Database\Eloquent\Builder|User whereEmail($value)
 * @method static \Illuminate\Database\Eloquent\Builder|User whereEmailVerifiedAt($value)
 * @method static \Illuminate\Database\Eloquent\Builder|User whereId($value)
 * @method static \Illuminate\Database\Eloquent\Builder|User whereName($value)
 * @method static \Illuminate\Database\Eloquent\Builder|User wherePassword($value)
 * @method static \Illuminate\Database\Eloquent\Builder|User whereRememberToken($value)
 * @method static \Illuminate\Database\Eloquent\Builder|User whereUpdatedAt($value)
 * @mixin \Eloquent
 */
class User extends Authenticatable
{
    // use宣言、$fillable、$hidden、$castsは省略

    /**
     * @comment ユーザー名とパスワードを連結して取得1
     *
     * @return string
     */
    public function getNameAndEmail1Attribute(): string
    {
        return "{$this->name} {$this->email}";
    }

    /**
     * @comment ユーザー名とパスワードを連結して取得2
     *
     * @return \Illuminate\Database\Eloquent\Casts\Attribute
     */
    protected function nameAndEmail2(): Attribute
    {
        return Attribute::make(
            get: fn () => "{$this->name} {$this->email}",
        );
    }
}
  • 検証1の頃と比べて変化なし

検証3 Attribute::make()のgetパラメータにセットする関数に戻り値の型を明示して検証

Userモデルに追加した現形式のアクセサについて以下のように修正

User.php
class User extends Authenticatable
{
    /**
     * @comment ユーザー名とパスワードを連結して取得2
     *
     * @return \Illuminate\Database\Eloquent\Casts\Attribute
     */
    protected function nameAndEmail2(): Attribute
    {
        return Attribute::make(
-            get: fn () => "{$this->name} {$this->email}",
+            get: fn (): string => "{$this->name} {$this->email}",
        );
    }
}

php artisan ide-helper:models --write --resetを実行したところUserモデルクラスの先頭に生えたPHPDocは以下のようになりました。

User.php
<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

/**
 * App\Models\User
 *
 * @property int $id
 * @property string $name
 * @property string $email
 * @property \Illuminate\Support\Carbon|null $email_verified_at
 * @property mixed $password
 * @property string|null $remember_token
 * @property \Illuminate\Support\Carbon|null $created_at
 * @property \Illuminate\Support\Carbon|null $updated_at
 * @property-read string $name_and_email1 ユーザー名とパスワードを連結して取得1
+ * @property-read string $name_and_email2 ユーザー名とパスワードを連結して取得2
 * @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
 * @property-read int|null $notifications_count
 * @property-read \Illuminate\Database\Eloquent\Collection<int, \Laravel\Sanctum\PersonalAccessToken> $tokens
 * @property-read int|null $tokens_count
 * @method static \Database\Factories\UserFactory factory($count = null, $state = [])
 * @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery()
 * @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
 * @method static \Illuminate\Database\Eloquent\Builder|User query()
 * @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value)
 * @method static \Illuminate\Database\Eloquent\Builder|User whereEmail($value)
 * @method static \Illuminate\Database\Eloquent\Builder|User whereEmailVerifiedAt($value)
 * @method static \Illuminate\Database\Eloquent\Builder|User whereId($value)
 * @method static \Illuminate\Database\Eloquent\Builder|User whereName($value)
 * @method static \Illuminate\Database\Eloquent\Builder|User wherePassword($value)
 * @method static \Illuminate\Database\Eloquent\Builder|User whereRememberToken($value)
 * @method static \Illuminate\Database\Eloquent\Builder|User whereUpdatedAt($value)
 * @mixin \Eloquent
 */
class User extends Authenticatable
{
    // use宣言、$fillable、$hidden、$castsは省略

    /**
     * @comment ユーザー名とパスワードを連結して取得1
     *
     * @return string
     */
    public function getNameAndEmail1Attribute(): string
    {
        return "{$this->name} {$this->email}";
    }

    /**
     * @comment ユーザー名とパスワードを連結して取得2
     *
     * @return \Illuminate\Database\Eloquent\Casts\Attribute
     */
    protected function nameAndEmail2(): Attribute
    {
        return Attribute::make(
-            get: fn () => "{$this->name} {$this->email}",
+            get: fn (): string => "{$this->name} {$this->email}",
        );
    }
}
  • 10.xのアクセサを元にした@property-read string $name_and_email2 ユーザー名とパスワードを連結して取得2というPHPDocが生成されていることがわかります。

なぜ戻り値の型を明示することで解決するのか、そもそもなんでこういう修正をするに至ったかについてはソースの調査が必要でした。
今回はphp artisan ide-helper:modelsよりlaravel-ide-helperのmodelsコマンドを使用していることまでは既にわかっているためライブラリ内のConsole/ModelsCommandを調べました。
調べた結果としてModelsCommandについて以下のようなことがわかりました。

  • setPropertyというメソッドを使用して1行のPHPDocコメントに関する情報を作成していること

  • getPropertiesFromMethodsというメソッドの中の処理によってアクセサに関するPHPDocコメントを作成していること

    1. リフレクションによってモデルクラス内の全メソッドを取得(privateなメソッドは除く)
    2. foreachでループし1件ずつ検証
    3. メソッド名がgetで始まりAttributeで終わるときLaravel8.xの頃のアクセサとして解釈しPHPDocコメントを追加
    4. メソッドの戻り値の型が\Illuminate\Database\Eloquent\Casts\Attributeのとき現形式のアクセサとして解釈($isAttributetrue)
    5. $this->getAttributeReturnType($model, $reflection);によって得られる$typesについてgetというキーを含むときPHPDocコメントを追加

    ※ちなみに↑の$types\Illuminate\Support\Collectionのコレクション

vendor/barryvdh/laravel-ide-helper/src/Console/ModelsCommand.phpの抜粋

vendor/barryvdh/laravel-ide-helper/src/Console/ModelsCommand.php
~~~省略~~~
    public function getPropertiesFromMethods($model)
    {
~~~省略~~~
                $type = $this->getReturnTypeFromReflection($reflection);
                $isAttribute = is_a($type, '\Illuminate\Database\Eloquent\Casts\Attribute', true);
                $method = $reflection->getName();
                if (
                    Str::startsWith($method, 'get') && Str::endsWith(
                        $method,
                        'Attribute'
                    ) && $method !== 'getAttribute'
                ) {
                    //Magic get<name>Attribute
                    $name = Str::snake(substr($method, 3, -9));
                    if (!empty($name)) {
                        $type = $this->getReturnType($reflection);
                        $type = $this->getTypeInModel($model, $type);
                        $comment = $this->getCommentFromDocBlock($reflection);
                        $this->setProperty($name, $type, true, null, $comment);
                    }
                } elseif ($isAttribute) {
                    $name = Str::snake($method);
                    $types = $this->getAttributeReturnType($model, $reflection);
                    $comment = $this->getCommentFromDocBlock($reflection);
+                   info($name);
+                   info($types);
+                   info($comment);

                    if ($types->has('get')) {
                        $type = $this->getTypeInModel($model, $types['get']);
                        $this->setProperty($name, $type, true, null, $comment);
                    }
                } elseif (
~~~省略~~~

↑のinfo()を埋め込んだ状態でphp artisan ide-helper:models --write --resetを実行し、以下のようなログが得られました。

laravel.log(検証3の修正後)
[2024-01-30 05:49:29] production.INFO: name_and_email2  
[2024-01-30 05:49:29] production.INFO: {"get":"string"}  
[2024-01-30 05:49:29] production.INFO: ユーザー名とパスワードを連結して取得2  

一方、検証2の状態にコードを戻した状態でphp artisan ide-helper:models --write --resetを実行したところ以下のようなログが得られました。

laravel.log(検証2の状態に戻したときのログ)
[2024-01-30 05:52:48] production.INFO: name_and_email2  
[2024-01-30 05:52:48] production.INFO: []  
[2024-01-30 05:52:48] production.INFO: ユーザー名とパスワードを連結して取得2  
  • 見てわかる通り検証2の状態のときにinfo($types)の出力が空になっていました。
  • これが原因で$this->setProperty()が実行されず、結果として10.xの形式のアクセサを元にしたPHPDocが生成できていませんでした

結論

  • laravel-ide-helperの最新を導入しつつLaravel10.xでアクセサを使う場合はAttribute::make()のgetパラメータにセットする関数に戻り値の型を明示することで入力の補完が効いた開発が行える。

備考

  • Laravel8.xの頃のアクセサの書き方をすれば良いのでは?
  • laravel-ide-helperについて正式のリリースではないリポジトリのmasterをインストールすることを良しとするかは要確認

その他

この記事に書いてある内容について検証したときのリポジトリ↓

参考サイト

その他、今回の問題についてのissueの存在も確認できました

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?