EloquentModelのコンストラクタを正しく宣言するには

# はじめに

Modelのコンストラクタに必要な設定が施されていないがために、FactoryでModelを生成した際にModelの値が何も設定されていない、という現象に出くわしました。
コンストラクタ宣言時の注意点について備忘として残します。

## バージョン情報
PHP: 7.4
Laravel: 5.7 (古い。。。)
Vessel: 6.0

## 結論

先に結論を書きます。
Modelのコンストラクタを宣言する場合、親クラスのコンストラクタを呼び出す必要があります。
`parent::__construct($attributes)` で親クラスのコンストラクタを実行することで、Modelの初期化に必要な処理が実行されます。
↓の継承元の親クラス(Modelクラス)のコンストラクタを見てみると、いくつかの処理を実行することで初期化をしていることがわかります。
`parent::__construct($attributes)` を実行しないことで、HogeModelは知らぬ間にコンストラクタをオーバーライドしてしまっていたようです。


``` Hoge.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Hoge extends Model
{
    protected $table;

    protected $fillable = [
        'text',
    ];

    protected $dates = [
        'created_at',
        'updated_at'
    ];

    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);
        $this->table = config('database.connections.mysql.database') . '.hoges';
    }
}
```

``` Model.php
    /**
     * Create a new Eloquent model instance.
     *
     * @param  array  $attributes
     * @return void
     */
    public function __construct(array $attributes = [])
    {
        $this->bootIfNotBooted();

        $this->initializeTraits();

        $this->syncOriginal();

        $this->fill($attributes);
    }
```

冒頭で話したFactoryのエラーは完全にこいつが実行されていなかったせいですね、、
`$this->fill($attributes)` 

Hogeクラスで親クラス(Model)のコンストラクタを呼び出してあげることで解決です。

## 事象
続いて細かい事象についてですが

例えば`.env`に
`DB_DATABASE=hoge`と定義されていれば`hoge.hoges`
`DB_DATABASE=fuga`と定義されていれば`fuga.hoges`
といった具合に接続先スキーマとテーブル名を動的に設定するような処理がコンストラクタで実装されている場合

``` Hoge.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Hoge extends Model
{
    protected $table;

    protected $fillable = [
        'text',
    ];

    protected $dates = [
        'created_at',
        'updated_at'
    ];

    public function __construct()
    {
        $this->table = config('database.connections.mysql.database') . '.hoges';
    }
}
```

`database.php` はこう定義されているとして

``` database.php

    'connections' => [
        'mysql' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
            'engine' => null,
        ],
```

この状態でHogeモデルを用いてFactoryを使用すると

``` zsh
./vessel php artisan tinker
Psy Shell v0.9.12 (PHP 7.4.13 — cli) by Justin Hileman
>>> factory(\App\Hoge::class)->make()
=> App\Hoge {#4196}
>>> 
```

`factory(\App\Hoge::class)->make()`の結果、Hogeモデルの中身は何もありません。

# なぜHogeモデルの中身が無かったのか

Hogeモデルのコンストラクタは以下のように定義されています。

``` Hoge.php

    ~省略~

    public function __construct()
    {
        $this->table = config('database.connections.mysql.database') . '.hoges';
    }
```

これだと親クラスのコンストラクタが呼び出されていないため、子クラスでコンストラクタをオーバーライドしてしまっていることになります。
そのためModelの初期化に必要な処理が実行されず、Factoryで生成したModelが空の値を返すわけです。

また、親クラスのコンストラクタを見てみると

``` Model.php
    /**
     * Create a new Eloquent model instance.
     *
     * @param  array  $attributes
     * @return void
     */
    public function __construct(array $attributes = [])
    {
        $this->bootIfNotBooted();

        $this->initializeTraits();

        $this->syncOriginal();

        $this->fill($attributes);
    }

    ~省略~

    /**
     * Fill the model with an array of attributes.
     *
     * @param  array  $attributes
     * @return $this
     *
     * @throws \Illuminate\Database\Eloquent\MassAssignmentException
     */
    public function fill(array $attributes)
    {
        $totallyGuarded = $this->totallyGuarded();

        foreach ($this->fillableFromArray($attributes) as $key => $value) {
            $key = $this->removeTableFromKey($key);

            // The developers may choose to place some attributes in the "fillable" array
            // which means only those attributes may be set through mass assignment to
            // the model, and all others will just get ignored for security reasons.
            if ($this->isFillable($key)) {
                $this->setAttribute($key, $value);
            } elseif ($totallyGuarded) {
                throw new MassAssignmentException(sprintf(
                    'Add [%s] to fillable property to allow mass assignment on [%s].',
                    $key, get_class($this)
                ));
            }
        }

        return $this;
    }
```

コンストラクタ内部で`$this->fill($attributes)`が処理されていて
`$this->fill`関数を見に行くと、Modelの`$attributes`を設定していることが分かります。


ちなみに[PHP公式のコンストラクタの説明](https://www.php.net/manual/ja/language.oop5.decon.php)として以下のように記載があります。
> 注意: 子クラスがコンストラクタを有している場合、親クラスのコンストラクタが 暗黙の内にコールされることはありません。 親クラスのコンストラクタを実行するには、子クラスのコンストラクタの 中で parent::__construct() をコールすることが 必要です。 子クラスでコンストラクタを定義していない場合は、親クラスのコンストラクタを継承します (ただし、private 宣言されている場合は除く)。 これは、通常のクラスメソッドと同様です。

今回のケースは上記の注意事項に気付けず、発生してしまったようですね。


# 正しくコンストラクタを宣言するには 

もうお分かりですね。簡単です。継承元であるModelクラスのコンストラクタを呼び出すだけです。
上記のソースを修正します。

``` Hoge.php

    ~省略~

    public function __construct(array $attributes = []) // ここを追加
    {
        parent::__construct($attributes); // ここを追加
        $this->table = config('database.connections.mysql.database') . '.hoges';
    }
```

tinkerで確認してみます。

``` zsh
./vessel php artisan tinker
Psy Shell v0.9.12 (PHP 7.4.13 — cli) by Justin Hileman
>>> factory(\App\Hoge::class)->make()
=> App\Hoge {#4196
     text: "tenetur",
   }
>>> 
```

しっかりHogeの中身が設定されていますね。
これで事象は解決されました。

# 最後に

最後まで記事をお読みいただきありがとうございました。
Modelのコンストラクタの正しい宣言方法についてググっても、理由まで踏まえてまとめた記事が見受けられなかったので備忘録としても記事にしてみました。
(コントローラーのコンストラクタについての記事とかはあったんですけどね、、Modelのことは書かれてなかったりしたので)
似たような事で躓く人の助けになればいいなと思います。
ありがとうございました！