がるの健忘録

エンジニアでゲーマーで講師で占い師なおいちゃんのブログです。

Laravelのミューテタを調べてみた

ちょっと故がありまして、ミューテタについてちょっと気になる事があったので調べてみました。
Laravelのバージョンは8で検証してますが、多分まぁどのバージョンでもさほどの差異はなかろうかなぁ、と(新しく生えたメソッドを除く)。

ミューテタについては例えば https://readouble.com/laravel/8.x/ja/eloquent-mutators.html あたりをベースに。
機能は色々あるのですが、一端、get*Attribute() とset*Attribute() を見ています。

基本機能についての基本情報は公式サイトを参考にしていただきつつ。
多分一つ「この辺は想定しているんだろうなぁ」というのが、「既存のカラムを加工合成してデータを取得する」あたりなんじゃなかろうか、と。
マニュアルでも「ユーザーのフルネームの取得」という風に書かれていますが、この辺はまぁ割と「あったら便利」って思う人も多いんじゃなかろうかと思います。
マニュアルだとこんな感じですね。

/**
 * ユーザーのフルネームの取得
 *
 * @return string
 */
public function getFullNameAttribute()
{
    return "{$this->first_name} {$this->last_name}";
}

これを書いておくと、マニュアルには書いてませんが、大体こんな感じで取得できます。

use App\Models\User;

$user = User::find(1);

$fullName = $user->full_name;

なんとなし便利だなぁ、というあたりなのですが。
ちょいと気になったのが「存在しないカラム」だけじゃなくて「存在するカラム」に対してもgetやsetを書く事ができます。
その辺で、いくつか実験してみたので、その辺を記録しておく、ってのがこの記事の目的です。

一端まず「姓と名、と後で使う文字列」用の、雑なテーブルを用意しました。

MariaDB [lara_test]> insert tests set first_name='aaa',last_name='bbb', val='val';
Query OK, 1 row affected (0.01 sec)

MariaDB [lara_test]> select * from tests;
+----+------------+-----------+-----+------------+------------+
| id | first_name | last_name | val | created_at | updated_at |
+----+------------+-----------+-----+------------+------------+
|  1 | aaa        | bbb       | val | NULL       | NULL       |
+----+------------+-----------+-----+------------+------------+
1 row in set (0.00 sec)

とりあえずまず、上述の通り「存在しないカラム名を指定して合成した値の取得」を、ざっくりと書いてみます。
「どのコードがどこに書かれているか」などは適宜推測してください(コメントとかで突っ込みが多かったら綺麗に推敲しますw)。

//
Route::get('/test', [TestController::class, 'index']);

resources/views/tests.blade.php

php artisan make:controller TestController

    public function getNameAttribute()
    {
echo "Trap getNameAttribute\n";
        return "{$this->first_name} {$this->last_name}";
    }
    public function index()
    {
        //
        $data = TestModel::find(1);
        var_dump($data->name);

まぁこんな感じで、取得ができます。

さて、ここから。

    public function getFirstNameAttribute()
    {
echo "Trap getFirstNameAttribute\n";
        return $this->first_name;
    }

これを書くとこんな風に言われます。

ErrorException: Undefined property: App\Models\Test::$first_name in file

これは、こんな風に書く必要があるようです。

    public function getFirstNameAttribute()
    {
echo "Trap getFirstNameAttribute\n";
        return $this->attributes['first_name'];
        //return $this->first_name;
    }

$attributesプロパティはマニュアルにも書いてあるので、基本的にミューテタ内では $attributes を使う、ってのがよろしい感じのようです。

    public function getAll()
    {
        return $this->attributes ;
    }

なんてのをModelに生やすと外部から「生データを一通り」取れるので、今回のような実験の時には便利か、と。

さて。
いやまぁgetも多少気になる事がないわけではないのですが、setのほうでより、ちょっと気になる事があって。

まずはこんなものを用意します。

    public function setValAttribute($v)
    {
        $this->attributes['val'] = strtoupper($v);
    }

これを書くと「valの値は全部大文字になる(事を期待している)」と思われるんですが、さてはて。
Modelにレコードを入れる時は、save()、create()、insert(単体)、insert(バルク)、Laravel8あたりからはupsert()なんてのがございます。
っつわけで、早速。

        //
        TestModel::create([
            'first_name' => 'create',
            'last_name' => 'create',
            'val' => 'aBcDe',
        ]);
        //
        $o = new TestModel();
        $o->fill([
            'first_name' => 'save',
            'last_name' => 'save',
            'val' => 'aBcDe',
        ]);
        $o->save();
        //
        TestModel::insert([
            'first_name' => 'insert',
            'last_name' => 'insert',
            'val' => 'aBcDe',
        ]);
        //
        TestModel::insert([
            [
                'first_name' => 'bulk insert',
                'last_name' => 'bulk insert',
                'val' => 'aBcDe',
            ],
            [
                'first_name' => 'bulk insert2',
                'last_name' => 'bulk insert2',
                'val' => 'aBcDe',
            ],
        ]);
        //
        TestModel::upsert([
            [
                'first_name' => 'bulk upsert',
                'last_name' => 'bulk upsert',
                'val' => 'aBcDe',
            ],
            [
                'first_name' => 'bulk upsert2',
                'last_name' => 'bulk upsert2',
                'val' => 'aBcDe',
            ],
        ], ['id']);
MariaDB [lara_test]> select * from tests;
+----+--------------+--------------+-------+---------------------+---------------------+
| id | first_name   | last_name    | val   | created_at          | updated_at          |
+----+--------------+--------------+-------+---------------------+---------------------+
|  1 | aaa          | bbb          | val   | NULL                | NULL                |
|  2 | create       | create       | ABCDE | 2022-03-25 05:16:54 | 2022-03-25 05:16:54 |
|  3 | save         | save         | ABCDE | 2022-03-25 05:16:54 | 2022-03-25 05:16:54 |
|  4 | insert       | insert       | aBcDe | NULL                | NULL                |
|  5 | bulk insert  | bulk insert  | aBcDe | NULL                | NULL                |
|  6 | bulk insert2 | bulk insert2 | aBcDe | NULL                | NULL                |
|  7 | bulk upsert  | bulk upsert  | aBcDe | 2022-03-25 05:16:54 | 2022-03-25 05:16:54 |
|  8 | bulk upsert2 | bulk upsert2 | aBcDe | 2022-03-25 05:16:54 | 2022-03-25 05:16:54 |
+----+--------------+--------------+-------+---------------------+---------------------+
8 rows in set (0.00 sec)

うんまぁ予想通りっちゃぁ予想通り。
「createとsave」はsetを通るようなのですが、insertとupsertは「setを通らない」。
多分、createとsaveは基本的に「Eloquentに所属している」のに対して、insertとupsertはおそらく「クエリビルダに所属している」んだろうなぁ、と。

あと「んじゃ、selectしてきた値をインスタンスに入れる時にはsetは通るのか?」ってのを一応確認。

    public function index()
    {
        //
        $data = TestModel::find(8);
        var_dump($data->name);
        var_dump($data->getAll());
        $data->sss('aBc');
        var_dump($data->getAll());
    }

    public function sss($v)
    {
        //$this->val = $v;
        $this->attributes['val'] = $v;
    }
Trap getNameAttribute
Trap getFirstNameAttribute
string(25) "bulk upsert2 bulk upsert2"
Trap getFirstNameAttribute
string(12) "bulk upsert2"
array(6) {
  ["id"]=>
  int(8)
  ["first_name"]=>
  string(12) "bulk upsert2"
  ["last_name"]=>
  string(12) "bulk upsert2"
  ["val"]=>
  string(5) "aBcDe"
  ["created_at"]=>
  string(19) "2022-03-25 05:16:54"
  ["updated_at"]=>
  string(19) "2022-03-25 05:16:54"
}
Trap getFirstNameAttribute
string(12) "bulk upsert2"
array(6) {
  ["id"]=>
  int(8)
  ["first_name"]=>
  string(12) "bulk upsert2"
  ["last_name"]=>
  string(12) "bulk upsert2"
  ["val"]=>
  string(3) "aBc"
  ["created_at"]=>
  string(19) "2022-03-25 05:16:54"
  ["updated_at"]=>
  string(19) "2022-03-25 05:16:54"
}

うんまぁだろうなぁ。

あとついでに。

    public function setValAttribute($v)
    {
        $this->val = 'hoge';
    }

これやると「エラーは出ないけど動かない」となります。
なんとなくなにが起きてるか? は想像できそうな感じではありますが。

さてここからおいちゃんの感想。

「存在しないカラム値のget」は、いやまぁ「それ、明示的にメソッド書いたほうがよくない?」とは思うんだけど、まぁ「ありっちゃぁアリと言えなくもない」かなぁ、と。
「存在するカラム値のget」は、そこそこ微妙。一方で「加工した値が欲しい」気持ちはわかるのですが、おいちゃんはEloquentを「ORM」と思っているので、だとすると「基本的には、生情報を返せ」って思うんですよねぇ。「加工した情報が欲しいなら、それ用のメソッドを生やせ」的な。ただまぁ「シームレスに取りたい」気持ちが全くわからないわけでもない、ので、微妙。

setについては、おいちゃんは基本「あんまりいい顔はしないなぁ」と。
一方で「便利」だったりするのはなんとなしわかるんだけど、もう一方で「隠蔽が多いと、その辺を"わかってない"人がやらかす」確率が増えて、ねぇ……………。
なのでまぁ「強弁をふるって止める」ってほどではないのですが、あんまり積極的に好印象ってわけでもないかなぁ、。

というわけで、あんまりポジティブな内容にはならんかったんですが、せっかく調べたんで、メモを兼ねて。