Laravel の FormRequest にアクセサ機能を追加するパッケージを作りました (バージョン2)

FormRequest クラスにアクセサ機能を追加します。

去年の [PHPカンファレンス沖縄2022](https://phpcon-okinawa.connpass.com/event/257992/) にてネタにした拡張機能ですが、縁があり今年も登壇させていただけることになりました。

久し振りに packagist 見てみると意外とインストールしていただいていたので、この機会に？前々からやりたかった改修を入れました。

**Model クラスの property は配列に格納されているデータをマジックメソッドの __get() で取り出す方式で、v2 はそちらに合わせた形にしました**

※v1 は FormRequest クラスの property を生成する仕組みでした。PHP9 だと動的プロパティが禁止されるので、v1 は使えなくなりますね。


# バージョン1の記事はこちら

https://qiita.com/kazumacchi/items/aebfe8dfccbfd28acaf4

# アップデートのお知らせ
## v2.0.1 - 2023/08/24
- before() メソッドの return を自クラスオブジェクトに修正

<br>

# packagist
https://packagist.org/packages/kanagama/laravel-add-formrequest-accessor

# github

https://github.com/kanagama/laravel-add-formrequest-accessor

# インストール可能なPHPバージョン
PHP8 以上
Laravel 9以上


# インストール
```bash
composer require kanagama/laravel-add-formrequest-accessor:2.*
```

# 最新版にアップデートする場合
```bash
composer update -w kanagama/laravel-add-formrequest-accessor
```


# 使い方

ただの Trait なので、使いたい 自作 FormRequest で use して

```php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
// 追加
use Kanagama\FormRequestAccessor\FormRequestAccessor;

/**
 * @property-read string $last_name
 * @property-read string $first_name
 * @property-read string $full_name
 */
class BookingRequest extends FormRequest
{
    // 追加
    use FormRequestAccessor;
```

あとは、クラス内で getXXXXXXXXXAttribute() を作成するだけ。

```php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Kanagama\FormRequestAccessor\FormRequestAccessor;

/**
 * @property-read string $last_name
 * @property-read string $first_name
 * @property-read string $full_name
 */
class BookingRequest extends FormRequest
{
    use FormRequestAccessor;

    // アクセサメソッドを追加する
    /**
     * 氏名
     *
     * @return string
     */
    public function getFullNameAttribute(): string
    {
        // '山田'
        // $this->input('last_name') 

        // '太郎'
        // $this->input('first_name')

        return $this->input('last_name') . ' ' . $this->input('first_name');
    }
```

controller とかでアクセス

```php
class BookingController extends Controller
{
    /**
     * @param  BookingRequest  $request
     * @return View
     */
    public function index(BookingRequest $request): View
    {
        // full_name プロパティが追加されます
        // [
        //     'last_name' => '山田',
        //     'first_name' => '太郎',
        //     'full_name' => '山田 太郎',
        // ]
        dd($request->all());
        // '山田 太郎'
        dd($request->full_name);
    }
```

# オプション

## $immutable

true に設定した場合、[afterValidation()](https://qiita.com/kazumacchi/items/aebfe8dfccbfd28acaf4#aftervalidation%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89) 以降に merge(), replace(), offsetUnset(), offsetSet() ファンクションを呼び出すとImmutableException 例外が発生します（イミュータブルな Request クラスになります）

```php
/**
 * @property-read string $last_name
 * @property-read string $first_name
 * @property-read string $full_name
 */
class BookingRequest extends FormRequest
{
    use FormRequestAccessor;

    /**
     * @var bool
     */
    protected $immutable = true; 
}
```

```php
class BookingController extends Controller
{
    /**
     * @param  BookingRequest  $request
     * @return View
     */
    public function index(BookingRequest $request): View
    {
        // ImmutableException が発生
        $request->merge([
            'last_name'  => '山田',
            'first_name' => '太郎', 
        ]);
    }
}
```



## $guarded

all() メソッドで取得したくないプロパティは $guarded に記述することで除去できます。
プロパティとしてはアクセスできます。

```php
/**
 * @property-read string $last_name
 * @property-read string $first_name
 * @property-read string $full_name
 */
class BookingRequest extends FormRequest
{
    use FormRequestAccessor;

    /**
     * @var array
     */
    protected $guarded = [
        'last_name',
        'first_name',
    ];

    /**
     * 氏名
     *
     * @return string
     */
    public function getFullNameAttribute(): string
    {
        return $this->input('last_name') . ' ' . $this->input('first_name');
    }
}
```

```php
class BookingController extends Controller
{
    /**
     * @param  BookingRequest  $request
     * @return View
     */
    public function index(BookingRequest $request): View
    {
        // first_name, last_name が含まれない
        // [
        //     'full_name' => '山田 太郎',
        // ]
        dd($request->all());
        // '山田' が出力される
        dd($request->first_name);
    }
}
```


## $fill もしくは $fillable

配列指定

all() メソッドで取得したいプロパティだけを記述することで、そのプロパティだけが出力されます。
指定されていなくてもプロパティとしてはアクセス可能です。
※同時に指定した場合、$fillable の設定が優先されます。

```php
/**
 * @property-read string $last_name
 * @property-read string $first_name
 * @property-read string $full_name
 */
class BookingRequest extends FormRequest
{
    use FormRequestAccessor;

    /**
     * @var array
     */
    protected $fill = [
        'full_name',
    ];
    // もしくは
    protected $fillable = [
        'full_name',
    ];

    /**
     * 氏名
     *
     * @return string
     */
    public function getFullNameAttribute(): string
    {
        return $this->input('last_name') . ' ' . $this->input('first_name');
    }
}
```

```php
class BookingController extends Controller
{
    /**
     * @param  BookingRequest  $request
     * @return View
     */
    public function index(BookingRequest $request): View
    {
        // full_name のみ出力される
        // [
        //     'full_name' => '山田 太郎',
        // ]
        dd($request->all());
        // '山田' が出力される
        dd($request->first_name);
    }
}
```

## $casts

配列指定
型変換を行いたいプロパティを指定することで変換されます。
Model の $casts と同じです。

```php
/**
 * @property-read string $last_name
 * @property-read string $first_name
 * @property-read string $full_name
 */
class BookingRequest extends FormRequest
{
    use FormRequestAccessor;

    /**
     * @var array
     */
    protected $casts = [
        // どちらも int 型で 1 が格納されていると仮定
        'id'       => 'string',
        'view_flg' => 'bool',
    ];
}
```

```php
class BookingController extends Controller
{
    /**
     * @param  BookingRequest  $request
     * @return View
     */
    public function index(BookingRequest $request): View
    {
        // string 型で出力される
        // "1"
        dd($request->id);
        // bool 型で出力される
        // true
        dd($request->view_flg);
    }
}
```

## $null_disabled もしくは $nullDisabled

bool 値指定
アクセサの戻り値が null のものを all() で出力しないか設定出来ます。
※同時に指定した場合、$null_disabled の設定が優先されます。

```php
class BookingRequest extends FormRequest
{
    use FormRequestAccessor;

    // bool 値で指定
    // true の場合は null が戻り値のアクセサを出力しません
    // 未指定の場合は false 指定の挙動になります
    protected $nullDisabled = true;
    // protected $null_disabled = true

    /**
     * @return string|null
     */
    public function getNullPropertyAttribute(): ?string
    {
        return null;
    }
}
```

```php
class BookingController extends Controller
{
    /**
     * @param  BookingRequest  $request
     * @return View
     */
    public function index(BookingRequest $request): View
    {
        // null_property は未定義になり、出力されません
        // []
        dd($request->all());
        // 未定義のためエラー
        dd($request->null_property);
    }
}
```

## $empty_disabled もしくは $emptyDisabled

bool 値指定
戻り値が空 (※empty()チェックでtrue）のアクセサを all() で出力しないか設定出来ます。
$null_disabled と同時に指定した場合、こちらが優先されます。

同時に指定した場合、$empty_disabled の設定が優先されます。

```php
class BookingRequest extends FormRequest
{
    use FormRequestAccessor;

    // bool 値で指定
    // true の場合は戻り値が空のアクセサを出力しません
    // 未指定の場合は false 指定の挙動になります
    protected $emptyDisabled = true;
    // protected $empty_disabled = true;

    protected $nullDisabled = true;
    //protected $null_disabled = true;

    /**
     * @return string
     */
    public function getEmptyPropertyAttribute(): string
    {
        // 0 や null の場合も同様
        return '';
    }
}
```

```php
class BookingController extends Controller
{
    /**
     * @param  BookingRequest  $request
     */
    public function index(BookingRequest $request)
    {
        // $nullDisabled が指定されていますが、 $emptyDisabled が優先されます
        // empty_property は未定義になり、出力されません
        // []
        dd($request->all());
        // 未定義のためエラー
        dd($request->empty_property);
    }
}
```

## $disabled

配列指定
$disabled で指定されたプロパティは、Request クラスから削除されます。
all() でも出力されません。

```php
class BookingRequest extends FormRequest
{
    use FormRequestAccessor;

    // 配列で指定
    protected $disabled = [
        'disabled_property',
    ];

    /**
     * @return string
     */
    public function getDisabledPropertyAttribute(): string
    {
        return 'sample';
    }
}
```

```php
class BookingController extends Controller
{
    /**
     * @param  BookingRequest  $request
     */
    public function index(BookingRequest $request)
    {
        // disabled_property は未定義になり、出力されません
        // []
        dd($request->all());
        // 未定義のためエラー
        dd($request->disabled_property);
    }
}
```

## $enabled

配列指定
$enabled で指定されたプロパティ以外、Request クラスから削除されます。
all() でも出力されません。

$disabled と同時に指定した場合、こちらが優先されます。

```php
class BookingRequest extends FormRequest
{
    use FormRequestAccessor;

    // 配列で指定
    protected $enabled = [
        'enabled_property',
    ];

    protected $disabled = [
        'enabled_property',
    ];

    /**
     * @return string
     */
    public function getEnabledPropertyAttribute(): string
    {
        return 'sample';
    }
}
```

```php
class BookingController extends Controller
{
    /**
     * @param  BookingRequest  $request
     * @rerurn View
     */
    public function index(BookingRequest $request): View
    {
        // $disabled, $enabled どちらも指定している場合、$enabled が優先されます
        // enabled_property のみ出力される
        // [
        //     'enabled_property' => 'sample',
        // ]
        dd($request->all());
        // 'sample' が出力されます
        dd($request->enabled_property);
    }
}
```

## $validated_accessor もしくは $validatedAccessor

bool値指定
true を設定した場合、validated() を実行した際、返却値にアクセサの値が追加されます。
同時に指定した場合 $validated_accessor の設定が優先されます

```php
class BookingRequest extends FormRequest
{
    use FormRequestAccessor;

    /**
     * @var bool
     */
    protected $validatedAccessor = true;
    // protected $validated_accessor = true; 

    /**
     * @return string
     */
    public function getValidatedAccessorAttribute(): string
    {
        return 'sample';
    }
}
```

```php
class BookingController extends Controller
{
    /**
     * @param  BookingRequest  $request
     * @rerurn View
     */
    public function index(BookingRequest $request): View
    {
        // BookingRequest にバリデーションがないので本来は空配列ですが
        // $validated_accessor が true のためアクセサが実行される
        // [
        //      'validated_accessor' => 'sample',
        // ]
        dd($request->validated());
    }}
```

## settingsメソッド
Request アクセサの設定値を dd() を使って出力します。
dd() を使っているのでその時点で処理が停止します。


```php
$request->settings();
```


```php
{#1748 ▼
  +"settings": array:6 [▼
    "immutable" => null,
    "fill" => null,
    "guarded" => null,
    "casts" => null,
    "nullDisabled" => false,
    "emptyDisabled" => false,
    "validatedAccessor" => false,
  ]
  +"all": array:2 [▼
    "before_all" => {#1729}
    "after_all" => {#1718 ▼
      +"full_name": "山田 太郎"
    }
  ]
  +"accessor_methods": {#1737 ▼
    +"0": "getFullNameAttribute"
  }
}
```

## afterValidationメソッド

passedValidation() の後に動作するメソッドです。
passedValidation() の代替としてご利用下さい。

```php
class BookingRequest extends FormRequest
{
    use FormRequestAccessor;

    /**
     * passedValidation() 後に動作
     *
     * @return void
     */
    public function afterValidation(): void
    {

    }
}
```

## prepareForAccessorメソッド

アクセサを実行する前に動作するメソッド。
バリデーション実行後とアクセサ実行前の間に実行したい事がある場合にご利用下さい。
アクセサ実行前なので、アクセサにはアクセス出来ません。

```php
class BookingRequest extends FormRequest
{
    use FormRequestAccessor;

    /**
     * アクセサ実行前に動作
     *
     * @return void
     */
    public function prepareForAccessor(): void
    {
        
    }

    // prepareForAccessor() 実行後に動作します。
    /**
     * 氏名
     *
     * @return string
     */
    public function getFullNameAttribute(): string
    {
        return $this->input('last_name') . ' ' . $this->input('first_name');
    }
}
```

## beforeAllメソッド

アクセサ実行前の Request インスタンスで all() を実行します。

```php
// /booking?id=1 でアクセスしていると仮定
class BookingRequest extends FormRequest
{
    use FormRequestAccessor;

    /**
     * @return int
     */
    public function getIdAttribute(): int
    {
        // string 型の id が 戻り値の型 int に変換される
        return $this->input('id');
    }
}
```

```php
class BookingController extends Controller
{
    /**
     * @param  BookingRequest  $request
     * @return View
     */
    public function index(BookingRequest $request): View
    {
        // アクセサで変換される前の string "1" が出力される
        // [
        //      "id" => "1",
        // ]
        dd($request->beforeAll());
        // アクセサで変換された int 1 が出力される
        // [
        //      "id" => 1,
        // ]
        dd($request->all();
    }
}
```

## beforeメソッド
アクセサ実行前の Request インスタンスを取得できます。

```php
// /booking?id=1 でアクセスしていると仮定
class BookingRequest extends FormRequest
{
    use FormRequestAccessor;

    /**
     * @return int
     */
    public function getIdAttribute(): int
    {
        // string 型の id が 戻り値の型 int に変換される
        return $this->input('id');
    }
}
```

```php
class BookingController extends Controller
{
    /**
     * @param  BookingRequest  $request
     */
    public function index(BookingRequest $request): View
    {
        // before() で返却されるのがアクセサが実行される前の Request インスタンスのため
        // string "1" が出力される
        // [
        //      "id" => "1",
        // ]
        dd($request->before()->id);
    }
}
```

## getControllerメソッド
コントローラー名を取得します
cakephp はリクエストから controller 名を取得出来るのでパクりました。

```php
class BookingController extends Controller
{
    /**
     * @param  BookingRequest  $request
     * @return View
     */
    public function index(BookingRequest $request): View
    {
        // "BookingController" を出力
        dd($request->getController());
    }
```

## getActionメソッド
アクション名を取得します
cakephp はリクエストから action 名を取得出来るのでパクりました。


```php
class BookingController extends Controller
{
    /**
     * @param  BookingRequest  $request
     * @return View
     */
    public function index(BookingRequest $request): View
    {
        // "index" を出力
        dd($request->getAction());
    }
```

## 注意事項

1. Trait で passedValidation() をオーバーライドしているため、Request クラスで passedValidation() を利用出来ません。

2. passedValidation() で行う処理はアクセサみたいにプロパティ追加することがほとんどだと考えており、あまり問題視されないと思います（※願望）。
代替として [afterValidation()](#afterValidationメソッド) を用意したのでそちらをご利用下さい。

3. アクセサ処理は passedValidation() で実行しているため、passedValidation() より前の処理ではアクセサの値を取得出来ません。※validationData() など

4. 最新バージョンが安心です。

# 弊社での使い方

開始日時・終了日時を渡すページなどで、特に指定しなかった場合のデフォルト値を格納するとかしてます

なんちゃらリクエスト

```php
class SampleRequest extends FormRequest
{
    use FormRequestAccessor;

    // 省略
    
    /**
     * 開始日時
     *
     * @return string
     */
    public function getStartDateAttribute(): string
    {
        if ($this->input('start_date')) {
            return $this->input('start_date');
        }

        return Carbon::now()->addDay(7)->format('Y-m-d');
    }

    /**
     * 終了日時
     *
     * @return string
     */
    public function getEndDateAttribute(): string
    {
        if ($this->input('end_date')) {
            return $this->input('end_date');
        }

        return Carbon::now()->addDay(7)->format('Y-m-d');
    }
}
```

なんちゃらコントローラー
```php
    /**
     * @param  SampleRequest  $request
     */
    public function index(SampleRequest $request)
    {
        // start_date, end_date が空で来た場合、デフォルト値が格納される
        // yyyy-mm-dd
        $request->start_date;
        // yyyy-mm-dd
        $request->end_date;
    }
```

あと、ミュータブルな各プロパティを ValueObject に格納してイミュータブルにしたり

```php
/**
 * 空港クラス
 */
class AirportId
{
    private $value;

    /**
     * @param  int  $value
     */
    public function __construct(int $value)
    {
        if ($value < 1) {
            throw new InvalidArgumentException('AirportIdに範囲外の値が渡されました');
        }
        $this->value = $value;
    }

    /**
     * 那覇空港か判定する
     *
     * @return bool
     */
    public function isNahaAirport(): bool
    {
        // 中略
    }
}
```
```php
/**
 * @property-read AirportId $airport_id 空港idオブジェクト
 */
class SampleRequest extends FormRequest
{
    use FormRequestAccessor;

    // 省略
    
    /**
     * 開始日時
     *
     * @return AirportId
     */
    public function getAirportIdAttribute(): AirportId
    {
        return new AirportId($this->input('airport_id'));
    }
}

// aiport_idのパラメータが那覇空港かどうか
$request->airport_id->isNahaAirport();
```

# 更新履歴

## v2.0.0 - 2023/08/07
- 不具合を修正しました
- ちょっとだけ速くなりました。
- オプション名をキャメルケースでも指定可能にしました。
- property の持ち方を Model クラスに寄せました。