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);
}
}
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
インスタンスを返すアクセサの両方で作成して検証を行いました。
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();
});
<?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は以下のようになりました。
<?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は以下のようになりました。
<?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モデルに追加した現形式のアクセサについて以下のように修正
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は以下のようになりました。
<?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コメントを作成していること- リフレクションによってモデルクラス内の全メソッドを取得(privateなメソッドは除く)
- foreachでループし1件ずつ検証
- メソッド名が
get
で始まりAttribute
で終わるときLaravel8.xの頃のアクセサとして解釈しPHPDocコメントを追加 - メソッドの戻り値の型が
\Illuminate\Database\Eloquent\Casts\Attribute
のとき現形式のアクセサとして解釈($isAttribute
がtrue
) -
$this->getAttributeReturnType($model, $reflection);
によって得られる$types
についてgetというキーを含むときPHPDocコメントを追加
※ちなみに↑の
$types
は\Illuminate\Support\Collection
のコレクション
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
を実行し、以下のようなログが得られました。
[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
を実行したところ以下のようなログが得られました。
[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用のrector定義(driftingly/rector-laravel)を導入している場合にMigrateToSimplifiedAttributeRectorのルールを有効化できなくなってしまう問題が起きてしまいます(有効化するとLaravel8.xの形式のアクセサについて現形式への整形対象として挙がってしまうため)。
- laravel-ide-helperについて正式のリリースではないリポジトリのmasterをインストールすることを良しとするかは要確認
その他
この記事に書いてある内容について検証したときのリポジトリ↓
参考サイト
- https://readouble.com/laravel/8.x/ja/eloquent-mutators.html
- https://readouble.com/laravel/9.x/ja/eloquent-mutators.html
- https://readouble.com/laravel/10.x/ja/eloquent-mutators.html
その他、今回の問題についてのissueの存在も確認できました