Laravel MySQL timestamp型 2038年問題に対応する

## 概要

MySQLのtimestamp型は2038年問題が発生します。
[MySQLのtimestamp型に起きる2038年問題](https://qiita.com/juve_534/items/a9f9f72d49f8817951e8)

timestamp型の寿命が先に来るか、アプリの寿命が先に来るか定かではないですが後から直すことを考えると予め対策しておいた方がベターなのかなと思います。

## 環境

- PHP 7.4.5
- Laravel 7.9.2
- MySQL 8.0.19

## デフォルトの users テーブル

```php:database/migrations/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();
        });
```

`timestamp()` と `timestamps()` の指定があります。

### timestamps() の実装

```php:vendor/laravel/framework/src/Illuminate/Database/Schema/Blueprint.php
    /**
     * Add nullable creation and update timestamps to the table.
     *
     * @param  int  $precision
     * @return void
     */
    public function timestamps($precision = 0)
    {
        $this->timestamp('created_at', $precision)->nullable();

        $this->timestamp('updated_at', $precision)->nullable();
    }
```

単純に `timestamp()` を呼び出しています。

## 問題

```
$ php artisan tinker

>>> factory(App\User::class)->create(['email_verified_at' => '2038-01-19 12:14:08'])

Illuminate/Database/QueryException with message 'SQLSTATE[22007]: Invalid datetime format: 1292 Incorrect datetime value: '2038-01-19 12:14:08' for column 'email_verified_at' at row 1 (SQL: insert into `users` (`name`, `email`, `email_verified_at`, `password`, `remember_token`, `updated_at`, `created_at`) values (Kelsie Renner DVM, elisha.simonis@example.org, 2038-01-19 12:14:08, $2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi, Y5cwkbHJGp, 2020-05-07 00:16:31, 2020-05-07 00:16:31))'
```

timestamp型に `2038-01-19 12:14:08` 以上の値を入れると `Invalid datetime format: 1292 Incorrect datetime value` エラーが発生します。

## 対策1 マイグレーションで dateTime 型を指定する

```php:database/migrations/2014_10_12_000000_create_users_table.php
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->dateTime('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->dateTime('created_at')->nullable();
            $table->dateTime('updated_at')->nullable();
        });
```

`timestamp` を `dateTime` に置き換えるだけです。

## 対策2 Blueprint クラスに dateTimes 関数を自作して追加

timestamp型だったら `$table->timestamps();` で簡単に登録できるのにな...🤔
公式に実装してもらいたいところですが、ないので仕方ありません。

`app/Database/Blueprint.php` を作成して `dateTimes` 関数を実装する。

```php:app/Database/Blueprint.php
<?php declare(strict_types=1);

namespace App\Database;

use Illuminate\Support\Facades\DB;
use Illuminate\Database\Schema\Blueprint as BaseBlueprint;

class Blueprint extends BaseBlueprint
{
    /**
     * Add automatic creation and update dateTimes to the table.
     *
     * @param  int  $precision
     */
    public function dateTimes($precision = 0): void
    {
        $this->dateTime('created_at', $precision)->nullable();
        $this->dateTime('updated_at', $precision)->nullable();
    }
}
```

`app/Facades/Schema.php` を作成して、 `Schema` ファサードを作成する。

```php:app/Facades/Schema.php
<?php declare(strict_types=1);

namespace App\Facades;

use App\Database\Blueprint;
use Illuminate\Database\Schema\Builder;
use Illuminate\Support\Facades\Schema as BaseSchema;

class Schema extends BaseSchema
{
    /**
     * Get a schema builder instance for a connection.
     *
     * @param  string|null  $name
     * @return Builder
     */
    public static function connection($name): Builder
    {
        /** @var \Illuminate\Database\Schema\Builder $builder */
        $builder = static::$app['db']->connection($name)->getSchemaBuilder();
        $builder->blueprintResolver(static function($table, $callback) {
            return new Blueprint($table, $callback);
        });
        return $builder;
    }

    /**
     * Get a schema builder instance for the default connection.
     *
     * @return Builder
     */
    protected static function getFacadeAccessor(): Builder
    {
        /** @var \Illuminate\Database\Schema\Builder $builder */
        $builder = static::$app['db']->connection()->getSchemaBuilder();
        $builder->blueprintResolver(static function($table, $callback) {
            return new Blueprint($table, $callback);
        });
        return $builder;
    }
}
```

`config/app.php` 一応、エイリアスも変更する。

```php:config/app.php
    'aliases' => [
        // 'Schema' => Illuminate\Support\Facades\Schema::class,
        'Schema' => App\Facades\Schema::class,
    ],
```

マイグレーションファイルを書き換える。
名前空間とtimestampのカラムを変更します。

```php:database/migrations/2014_10_12_000000_create_users_table.php
<?php

use App\Facades\Schema; // 変更
use App\Database\Blueprint; // 変更
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->dateTime('email_verified_at')->nullable(); // 変更
            $table->string('password');
            $table->rememberToken();
            $table->dateTimes(); // 変更
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}
```

`migrate:fresh` コマンドでテーブルを作り直します。

```shell-session
$ php artisan migrate:fresh
```

テーブル構成を確認します。

```sql
> desc users;
+-------------------+-----------------+------+-----+---------+----------------+
| Field             | Type            | Null | Key | Default | Extra          |
+-------------------+-----------------+------+-----+---------+----------------+
| id                | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| name              | varchar(255)    | NO   |     | NULL    |                |
| email             | varchar(255)    | NO   | UNI | NULL    |                |
| email_verified_at | datetime        | YES  |     | NULL    |                |
| password          | varchar(255)    | NO   |     | NULL    |                |
| remember_token    | varchar(100)    | YES  |     | NULL    |                |
| created_at        | datetime        | YES  |     | NULL    |                |
| updated_at        | datetime        | YES  |     | NULL    |                |
+-------------------+-----------------+------+-----+---------+----------------+
```

Typeが datetime 型になってます。
`$table->dateTimes();` で `created_at` と `updated_at` を登録できるようになりました。
しかし、カスタマイズしたクラスの名前空間指定が必要になったので、むしろ手間が増えたような...🤔

ちなみに元(timestamp)のテーブル構成はこちらです。

```sql
> desc users;
+-------------------+-----------------+------+-----+---------+----------------+
| Field             | Type            | Null | Key | Default | Extra          |
+-------------------+-----------------+------+-----+---------+----------------+
| id                | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| name              | varchar(255)    | NO   |     | NULL    |                |
| email             | varchar(255)    | NO   | UNI | NULL    |                |
| email_verified_at | timestamp       | YES  |     | NULL    |                |
| password          | varchar(255)    | NO   |     | NULL    |                |
| remember_token    | varchar(100)    | YES  |     | NULL    |                |
| created_at        | timestamp       | YES  |     | NULL    |                |
| updated_at        | timestamp       | YES  |     | NULL    |                |
+-------------------+-----------------+------+-----+---------+----------------+
```

公式で `dateTimes()` 関数用意してくれないかな...🤔

## 参考

- https://stackoverflow.com/questions/22444685/extend-blueprint-class
- https://stackoverflow.com/questions/49113407/extending-laravel-5-5-5-6-blueprint
- https://laracasts.com/discuss/channels/laravel/how-to-extend-blueprint