【Laravel】データベースから Eloquent モデルのソースコードを生成する

 Laravel は PHP のフレームワークです。よく web サイトのサーバー側で使われます。この使用用途の中でよくあるのがDB(データベース)中へのデータをいい感じにCRUD(作成、照会、更新、削除をまとめた語)する使い方です。少なくとも弊社の業務の中でこの使い方は最も多く、異なる使い方をメインとしたサービスであっても十中八九どこかしらにこの DB の CRUD を行う使い方が出てきます。同じ形式で何度も使うとなるとテンプレートを作って楽したくなります。この記事ではこれを自動生成する方法のさわりを紹介します。

 この記事で用いた Laravel のバージョンは 9.4.1 です。

 まず、第一に主役となるのは DBAL の SchemaManager です。

DBAL Documentation – Doctrine Database Abstraction Layer (DBAL)
Schema-Manager – Doctrine Database Abstraction Layer (DBAL)#SchemaManeger

 これは DB の中身をいい感じにオブジェクトとして見れる様になるライブラリです。これを使えば Eloquent モデル(Laravel の DB のテーブルをマッピングしたクラス)を作らずとも処理を書けます。つまりこれを元に Eloquent モデルのコードを生成できます。
 方針としてはテーブルとカラムの情報を取得し、欲しい名前を型と方に対応する値をひたすら変数に入れ、Blade テンプレートでモデルのソースコードを生成します。簡易な実装例が次です。
 
 いささか手間ですがschemaManager->listTableForeignKeys($tableName)で外部キー制約を取得するとリレーションも自動生成できる様になります。

<?php

namespace App\Console\Commands\ForDevelop\ModelMaker;

use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Schema\Table;
use MatanYadaev\EloquentSpatial\Objects\Point;
use MatanYadaev\EloquentSpatial\Objects\Polygon;
use Str;
use View;

class Dumper
{
    /**
     * @param  string  $tableName
     * @return void
     * @throws Exception
     */
    public function dumpModel(string $tableName): void
    {
        /** @var Table $table */
        $table = \DB::getDoctrineSchemaManager()->listTableDetails($tableName);
        // 出力先のネームスペース
        $namespace  = 'App\\Models\\Eloquents';
        // 出力先のクラス名。テーブル名を単数形にし、キャメルケースにし、最初の一文字目を大文字にする
        $className  = ucfirst(Str::camel(Str::singular($table->getName())));
        // テーブル名
        $tableName  = $table->getName();
        // 主キー名。複合主キーを考慮しない場合は[0]の決め打ちで大丈夫
        $primaryKey = $table->getPrimaryKeyColumns()[0]->getName();
        // hiddenプロパティに入れるカラムらの名前。
        // テーブルインスタンスからカラムの名前だけを array_map で抽出し、
        // array_filter で隠したいカラムの名前である password, auth_token が存在したならば hidden それらを hidden とする
        $hidden     = array_filter(
            array_map(static fn ($c) => $c->getName(), $table->getColumns()),
            static fn (string $name) => in_array($name, ['password', 'auth_token'], true),
        );
        // gurdedプロパティに入れるカラムらの名前
        $guarded    = $this->getGuarded($table);
        // castsプロパティに入れるカラムらの名前
        $casts      = $this->getCasts($table);

        // Blade テンプレートに上で用意した情報を詰め込みます。
        $view = View::file(
            __DIR__.'/../CodeStab/Model.php.stub.blade.php',
            compact(
                'namespace', 'tableName', 'className', 'primaryKey', 'hidden', 'guarded', 'casts',
            )
        );
        // Blade テンプレートを描画し、描画時に PHP として実行されることを防ぐために入れていた %%php%% を <?php に置換
        // 結果できた文字列をファイルとして保存して。モデルのソースコード生成が完了
        file_put_contents(
            app_path('Models/Eloquents/'.$className.'.php'),
            str_replace('%%php%%', '<?php', $view->render())
        );
    }

    /**
     * 主キー、パスワード、登録日時、更新日時、削除日時といった
     * 外部から fill メソッドで入力すべきでないカラムを gurded プロパティとする。
     * hidden 同様に array_map, array_filter で存在するカラムのみを gurded に追加する
     * @param  Table     $table
     * @return string[]
     * @throws Exception
     */
    private function getGuarded(Table $table): array
    {
        return array_filter(
            array_map(static fn ($c) => $c->getName(), $table->getColumns()),
            static fn (string $name) => in_array($name,
                [
                    ...array_map(
                        static fn ($c) => $c->getName(),
                        $table->getPrimaryKey() === null
                            ? []
                            : $table->getPrimaryKeyColumns()
                    ),
                    'password',
                    'created_at',
                    'updated_at',
                    'deleted_at',
                ],
                true),
        );
    }

    /**
     * casts プロパティの配列を返します。カラムをキー、値をカラムの型に対応した文字列にしています。
     * @param  Table    $table
     * @return string[]
     */
    private function getCasts(Table $table): array
    {
        $casts = [];
        foreach ($table->getColumns() as $c) {
            $casts[$c->getName()] = self::DB_TYPE_TO_LARAVEL_CAST_MAP[$c->getType()->getName()] ?? null;
        }

        return $casts;
    }
    public const DB_TYPE_TO_LARAVEL_CAST_MAP = [
        'boolean'    => 'boolean',
        'tinyint'    => 'integer',
        'smallint'   => 'integer',
        'mediumint'  => 'integer',
        'int'        => 'integer',
        'integer'    => 'integer',
        'bigint'     => 'integer',
        'tinytext'   => 'string',
        'mediumtext' => 'string',
        'longtext'   => 'string',
        'text'       => 'string',
        'varchar'    => 'string',
        'string'     => 'string',
        'char'       => 'string',
        'date'       => 'date',
        'datetime'   => 'date',
        'timestamp'  => 'date',
        'time'       => 'date',
        'float'      => 'float',
        'double'     => 'float',
        'real'       => 'float',
        'decimal'    => 'float',
        'numeric'    => 'float',
        'year'       => 'integer',
        'longblob'   => 'string',
        'blob'       => 'string',
        'mediumblob' => 'string',
        'tinyblob'   => 'string',
        'binary'     => 'string',
        'varbinary'  => 'string',
        'set'        => 'array',
        'geometry'   => null,
        'point'      => Point::class,
        'polygon'    => Polygon::class,
    ];
}
%%php%%

namespace {{ $namespace }};

use App\Models\Eloquents\BaseEloquent as Model;

class {{ $className }} extends Model
{
    public $table = '{{ $tableName }}';
    protected $primaryKey = '{{ $primaryKey }}';

@if( !empty($hidden) )
    protected $hidden = [
@foreach( $hidden as $h)
        '{{ $h }}',
@endforeach
    ];
@endif

    public $guarded = [
@foreach( $guarded as $g)
        '{{ $g }}',
@endforeach
    ];

    protected $casts =[
@foreach( $casts as $k => $v)
@if( $v === null )
        // WARN: skip {{ $k }}
@else
        '{{ $k }}' => '{{ $v }}',
@endif
@endforeach
    ];

}

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG