【Laravel】Eloquentで使われているテーブル間で共通するカラムを検出する

  • 2021年12月16日
  • PHP

 時折、データベースの中で共通する何かしらのカラムを見つけたい時があります。これは例えば、データベースがあまりに巨大な際に関連する部分を見つけて理解を深めたい時、正規化されていない部分を見つけたい時、実質外部キー制約がある関連するキーを見つけたい時があてはまります。自分の場合、カラムが170、160あり外部キー制約もなくER図も整備されていないデータベースを途中参加のプロジェクトで相手することになった際に役立ってくれました。
 これは先日紹介した Eloqeurnt クラス名の一覧を取得する方法と Laravel がラッピングしている Doctrine を用いることで解決できます。
【Laravel】プロジェクト内で使われている Eloquent クラスのクラス名一覧を取得する – 株式会社シーポイントラボ | 浜松のシステム・RTK-GNSS開発
Doctrine: PHP Open Source Project
Database Abstraction Layer – Doctrine: PHP Open Source Project
【Laravel】Eloquent から対応するテーブルのメタ情報を引っ張る – 株式会社シーポイントラボ | 浜松のシステム・RTK-GNSS開発
 実装は次です。このコマンドを app/Console/Commands 直下に配置して php artisan dev:show_column_list_with_tableと実行すればカラム一覧のファイルが app/Console/Commands 直下に出力されます。

<?php

namespace App\Console\Commands;

use App\Console\BaseCommand;
use App\Models\EloquentHelper;
use App\Models\Eloquents\BaseEloquent;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Schema\Column;
use Illuminate\Database\Eloquent\Model;
use Str;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\Yaml\Yaml;

class ShowColumnListWithTable extends BaseCommand
{
    protected $name = 'dev:show_column_list_with_table';

    protected $description = 'モデルを元に全カラムとそれを持つテーブルをまとめる';

    /**
     * @return int
     * @throws Exception
     */
    public function handle(): int
    {
        // Laravel でつなげているデータベースの接続から Doctrine の DB 操作用クラスインスタンスを取得
        $doctrine = \DB::connection()->getDoctrineSchemaManager();
        // Laravel 内で使っているモデルクラスのクラス名を取得
        $classNames = self::getEloquentClassNames();
        // 各クラスからテーブルの中で使われているカラムの情報を集める
        $columns = [];
        foreach($classNames as $className) {
            // Eloquent インスタンスからテーブル名を取得
            $tableName = (new $className())->getTable();
            // Doctrine から該当テーブルのカラムのリストを得る
            foreach($doctrine->listTableColumns($tableName) as $column) {
                // 初出のカラムについては初期化
                if(!isset($columns[$column->getName()])) {
                    $columns[$column->getName()] = [];
                }
                // カラムを主にしてそれを持つテーブルとそのテーブルに置けるコメントを格納
                $columns[$column->getName()][] = [
                    'table'   => $tableName,
                    'comment' => $column->getComment(),
                    // ここで $column->getDefault() 等してより多くの情報を集めることもできる
                ];
            }
        }
        // カラム名昇順で並び替え
        ksort($columns);

        // CSV出力
        $fp = fopen(__DIR__."/カラム一覧.csv", 'wb');
        foreach($columns as $name => $details) {
            foreach($details as $d) {
                // PHP 8.1 は連想配列でもスプレッド演算子で展開可能
                fputcsv($fp, [$name, ...$d]);
                // PHP 8.0 以前では array_merge を使う必要あり
                // fputcsv($fp, array_merge([$name], $d));
            }
        }
        fclose($fp);

        // YAML 出力.Symfony の Yaml コンポーネントを使用
        // @see https://symfony.com/doc/current/components/yaml.html
        file_put_contents(
            __DIR__ . '/カラム一覧.yaml',
            Yaml::dump($columns, 1e5, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE)
        );

        return static::SUCCESS;
    }

    /**
     * プロジェクト内で使われている Eloquent クラスの名前を全て返す
     * @return array
     */
    private static function getEloquentClassNames(): array
    {
        // プロジェクトの Eloquents を置いてあるディレクトリ以下の PHP ファイルを探索
        // この探索は再帰的に行われる
        // @see https://symfony.com/doc/current/components/finder.html
        $files = (new Finder())->in(app_path('Models/Eloquents'))
            ->files()
            ->name('*.php');
        // 見つかったファイルパスを元にクラス名一覧を作る
        $classNames = [];
        /** @var SplFileInfo $fileInfo */
        foreach($files->getIterator() as $fileInfo) {
            $classNames[] = str_replace(
            // ファイルパスを名前空間に入れ替え、拡張子を除去
                [app_path('Models/Eloquents'), '/', ".php"],
                ['App\\Models\\Eloquents', '\\', ''],
                $fileInfo->getRealPath()
            );
        }
        // クラス名の中から Eloquent を継承したクラスのみを抜き出す
        $eloquentClassNames = [];
        foreach($classNames as $className) {
            try {
                // クラス名からインスタンスを作成。 Eloquent を継承しているか確認
                $instance = new $className();
                if($instance instanceof \Illuminate\Database\Eloquent\Model) {
                    $eloquentClassNames[] = $className;
                }
            } catch(\Error $exception) {
                // インスタンス化できない対象について new を行った際のエラーを握りつぶす
                // abstract class や trait が引っかかりやすい
            }
        }

        return $eloquentClassNames;
    }
}

 上記コードの実行結果例が次です。外部キー制約がつけられていないにせよテーブルやカラムの命名で共通の語を持つことは多いのでそれなりにあてにできます。次図では member_groups がどこに関わっているのか名前から判別できます。

 例では Laravel プロジェクト内の Eloquent を総ざらいするコードにしましたが、Doctrine は Laravel 外でも使える汎用的なライブラリです。次の様に Doctrine のデータベースへの接続を直書きし、テーブル名も Eloquent とは別に用意することで Laravel 外でも同様にデータベースの中を調査するコードが書けます。

        $connectionParams = [
            'url' => 'mysql://user:secret@localhost/mydb',
        ];
        /** @var MySqlSchemaManager $doctrine */
        $doctrine = \Doctrine\DBAL\DriverManager::getConnection($connectionParams)->getSchemaManager();
        $tableNames = [
            'members',
            'member_groups',
        ];
        // 各クラスからテーブルの中で使われているカラムの情報を集める
        $columns = [];
        foreach($tableNames as $tableName) {
            // Doctrine から該当テーブルのカラムのリストを得る
            foreach($doctrine->listTableColumns($tableName) as $column) {
// 以下同上なので省略
>株式会社シーポイントラボ

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

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

CTR IMG