🦔

Laravel + DoctrineでPrismaみたいに自動でマイグレーション生成してDB管理する

2021/11/04に公開

はじめに

これまで、ORMが嫌いだった僕は、業務でPrismaに出会ってどんな言語でもORMを使いたい人間になりました。(Eloquentが好きになれなかったからかも笑)

ORMと言っても特に感動した機能は、スキーマから差分をとってマイグレーション用のSQLを生成してくれる機能でした。

僕の知る限り、Laravel + Eloquent での開発では、Model側のファイルを更新した際に自動でマイグレーションファイルを生成してくれる機能はなかったと思います。(あったらコメントでご指摘ください🙏)

そこで、PHPでも自動で生成してくれるORMないかなーと思って調べたところDoctrineというORMに、定義ファイルとDBチェックして差分を出してくれる機能がありました。

この記事では、LaravelのプロジェクトにDoctrineを導入してDoctrineでDB管理する方法を紹介します。

この記事でわかること

  • LaravelにDoctrineを導入する
  • LaravelでDoctrineを動かすときのハマりポイント
  • 自動でマイグレーションを生成してDB更新する

前提

Laravel 7.x ← LTSサポートが終わっている!! 古くてすみませんm(_ _)m
laravel-doctrine/migrations: "^2.3"
laravel-doctrine/orm: "^1.6"
Mysql 5 ~

この記事では、できるだけ既存プロジェクトへの導入方法も意識して書いていきますが、やはり既存プロジェクトの環境やバージョンによっては色々問題出てくると思います。ハマったこととかあったらぜひコメントで聞いてください、分かる範囲で全力を使ってご回答します。

Doctrineについて

Doctrineについては、以下のリンク等をご参照ください。
https://www.doctrine-project.org/
https://engineering.otobank.co.jp/entry/2017/01/31/151121

SynfonyのデフォルトのORMみたいです。もちろんフレームワーク関係なくPHPには導入できます。
大きな特徴として、Doctrineには、アノテーションでPHPのクラスに対してスキーマを定義することができる構造となっています。

laravel-doctrineについて

LaravelにDoctrineを導入する際には、Laravelの他の機能やディレクトリ・ファイルの構造を揃えるために、composerのパッケージとして用意されている、laravel-doctrineを使うのがおすすめです。設定等をLaravelのスタイルに揃えることができます。

謝っておきたいこと

色々頑張ったんですが、アノテーション以外の方法でスキーマを定義することができませんでした。この記事では、アノテーションによるスキーマ定義のパターンしか紹介しないのでご了承ください🙏

Ⅰ. laravel-doctrineのインストール

まずは、Laravelプロジェクトにlaravel-doctrineをインストールしていきます。
以下のコマンドをプロジェクトのルートディレクトリで実行してください

プロジェクトのルートディレクトリ
composer require laravel-doctrine/orm:"^1.6" laravel-doctrine/migrations

このバージョン指定は、Laravel7.x系のバージョン指定です。自分の環境にあったバージョンでインストールしてください。

ハマりポイント

依存関係の問題で、ずっと以下のエラーに悩まされました。
僕は、composer.lockを削除してインストールを行うことで、解決することができました。
ただ、他の依存関係にも影響を及ぼす可能性があるので、自己責任でお願いします。
困っている方は、試してみてください。

ハマりエラー
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Conclusion: don't install laravel-doctrine/orm 1.6.1 (conflict analysis result)
    - doctrine/orm[v2.7.0, ..., v2.7.2] require doctrine/common ^2.11 -> satisfiable by doctrine/common[v2.11.0, ..., 2.13.x-dev].
    - doctrine/common[v2.11.0, ..., 2.13.x-dev] require doctrine/inflector ^1.0 -> found doctrine/inflector[v1.0, ..., 1.4.x-dev] but the package is fixed to 2.0.3 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.
    - doctrine/orm[v2.7.3, ..., 2.7.x-dev] require doctrine/inflector ^1.0 -> found doctrine/inflector[v1.0, ..., 1.4.x-dev] but the package is fixed to 2.0.3 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.
    - doctrine/orm[v2.6.0, ..., 2.6.x-dev] require symfony/console ~3.0|~4.0 -> found symfony/console[v3.0.0-BETA1, ..., 3.4.x-dev, v4.0.0-BETA1, ..., 4.4.x-dev] but the package is fixed to v5.2.4 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.
    - doctrine/orm[2.10.0, ..., 2.11.x-dev] require symfony/polyfill-php72 ^1.23 -> found symfony/polyfill-php72[dev-main, v1.23.0, 1.23.x-dev (alias of dev-main)] but the package is fixed to v1.22.1 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.
    - laravel-doctrine/orm[1.6.2, ..., 1.6.x-dev] require doctrine/inflector ^1.4 -> found doctrine/inflector[1.4.0, ..., 1.4.x-dev] but the package is fixed to 2.0.3 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.
    - laravel-doctrine/orm[1.7.0, ..., 1.7.x-dev] require illuminate/auth ^8.0 -> found illuminate/auth[v8.0.0, ..., 8.x-dev] but these were not loaded, likely because it conflicts with another require.
    - Root composer.json requires laravel-doctrine/orm ^1.6 -> satisfiable by laravel-doctrine/orm[1.6.0, ..., 1.7.x-dev].
    - Conclusion: don't install doctrine/persistence[2.2.1] | install one of doctrine/persistence[1.3.7, 1.3.8] (conflict analysis result)
    - Conclusion: don't install doctrine/persistence[2.2.0] | install one of doctrine/persistence[1.3.7, 1.3.8] (conflict analysis result)
    - Conclusion: don't install doctrine/persistence[2.3.x-dev] | install one of doctrine/persistence[1.3.7, 1.3.8] (conflict analysis result)
    - Conclusion: don't install doctrine/persistence[2.2.x-dev] | install one of doctrine/persistence[1.3.7, 1.3.8] (conflict analysis result)
    - Conclusion: don't install doctrine/persistence 1.3.7 (conflict analysis result)
    - Conclusion: don't install one of doctrine/orm[2.8.x-dev], doctrine/persistence[1.3.8] | install doctrine/persistence[2.2.3] (conflict analysis result)
    - Conclusion: don't install doctrine/persistence 2.2.3 (conflict analysis result)
    - laravel-doctrine/orm 1.6.0 requires doctrine/orm ^2.6|^2.7 -> satisfiable by doctrine/orm[v2.6.0, ..., 2.11.x-dev].
    - Conclusion: don't install doctrine/orm 2.9.x-dev (conflict analysis result)
    - Conclusion: don't install doctrine/orm 2.8.0 (conflict analysis result)
    - Conclusion: don't install doctrine/orm 2.8.1 (conflict analysis result)
    - Conclusion: don't install doctrine/orm 2.8.2 (conflict analysis result)
    - Conclusion: don't install doctrine/orm 2.8.3 (conflict analysis result)
    - Conclusion: don't install doctrine/orm 2.8.4 (conflict analysis result)
    - Conclusion: don't install doctrine/orm 2.8.5 (conflict analysis result)
    - Conclusion: don't install doctrine/orm 2.9.0 (conflict analysis result)
    - Conclusion: don't install doctrine/orm 2.9.1 (conflict analysis result)
    - Conclusion: don't install doctrine/orm 2.9.2 (conflict analysis result)
    - Conclusion: don't install doctrine/orm 2.9.3 (conflict analysis result)
    - Conclusion: don't install doctrine/orm 2.9.4 (conflict analysis result)
    - Conclusion: don't install doctrine/orm 2.9.5 (conflict analysis result)
    - Conclusion: don't install doctrine/orm 2.9.6 (conflict analysis result)
    - doctrine/orm 2.8.x-dev requires doctrine/persistence ^2.2 -> satisfiable by doctrine/persistence[2.2.0, ..., 2.3.x-dev].
    - Conclusion: don't install doctrine/persistence[2.2.2] | install one of doctrine/persistence[1.3.7, 1.3.8] (conflict analysis result)
    
Installation failed, reverting ./composer.json and ./composer.lock to their original content.

Ⅱ. laravel-doctrineの設定ファイルを作成

ここが、doctrineを直接入れるのではなく、laravel-doctrineを使うメリットの一つです。
以下のコマンドで、Laravelの設定ファイルディレクトリにlaravel-doctrine用の設定ファイルを生成してくれます。

プロジェクトディレクトリ
php artisan vendor:publish --tag="config" --provider="LaravelDoctrine\ORM\DoctrineServiceProvider"

アノテーションでエンティティを定義する場合、以下のように設定を編集してください。

doctrine.php
︙
︙
'default' => [
            'dev'           => env('APP_DEBUG', false),
            'meta'          => env('DOCTRINE_METADATA', 'annotations'),
            'connection'    => env('DB_CONNECTION', 'mysql'),
            'namespaces'    => [
            ],
            'paths'         => [
                base_path('app/Entities')
            ],
            'repository'    => Doctrine\ORM\EntityRepository::class,
            'proxies'       => [
                'namespace'     => false,
                'path'          => storage_path('proxies'),
                'auto_generate' => env('DOCTRINE_PROXY_AUTOGENERATE', false)
            ],
︙
︙

'paths' => [base_path('app/Entities')]は、エンティティファイルを配置する場所を指定してください。既存プロジェクトで、既に同名のディレクトリがある場合は、entities以外の指定が可能です。
例)既にEntitiesがあるからEntityに入れる etc...

Ⅲ. エンティティを作成する(エンティティ=スキーマでもある)

ここからは、エンティティの作成に入ります。
この記事では、アノテーションでスキーマを定義するので、エンティティ作成と同時にスキーマのベースを作成することになります。
また、各自のプロジェクトの状況によって、エンティティの作り方は、大きく2つに分かれます。

1. 既にデータベースが存在する場合

既にテーブルが存在するプロジェクトに、Doctrineを導入する場合は既存のDBから、アノテーションが定義されているエンティティファイルをテーブル単位で生成することができます。
ここにもハマリポイントがあります。

プロジェクトディレクトリ
# app/Entitiesディレクトリが存在しない場合
mkdir app/Entities

# DBからXMLファイルにスキーマを生成
php artisan doctrine:convert:mapping xml "app/Entities/" --from-database --namespace="App/Entities/"

# XMLファイルからアノテーション付きエンティティファイルを生成
php artisan doctrine:convert:mapping annotation "app/Entities/"  --namespace="App/Entities/"

# app/Entities/App/Entities に作成されたXMLファイルは不要なので削除
rm app/Entities/App/Entities/*.xml

# ディレクトリ構成がおかしいので修正する
mv app/Entities/App/Entities/* app/Entities/

ハマりポイント

① DBからアノテーション付きエンティティファイルを直接生成できない
直接生成しようとすると、namespaceがバグります。したがって、アノテーション以外のスキーマファイルを一旦作成して、そこからアノテーション付きエンティティファイルに変換をする必要があります。

③ 生成されるディレクトリがおかしい
namespaceとディレクトリを指定して、octrine:convert:mappingを実行すると、指定したディレクトリの更に下にnamespaceの構成でディレクトリが作成され各ファイルが生成されます。これはおそらく、Laravel特有のディレクトリ構成が邪魔していると思われます。各ファイルの生成後、自分でファイルの位置の修正を行う必要があります。

2. まだ、DBが存在していない場合

まだ、テーブルが存在していない場合は、自作でエンティティファイルを作成し、アノテーションを記述していく必要があります。基本的には以下の公式ドキュメントに則って記述をしていけばよいですが、せっかくなので記述例を掲載しておきます。

app/Entities/User.php
<?php

namespace App\Entities;

use Doctrine\ORM\Mapping as ORM;

/**
 * User
 *
 * @ORM\Table(name="user")
 * @ORM\Entity
 */
class User
{
    /**
     * @var int
     *
     * @ORM\Column(name="user_id", type="integer", nullable=false, options={"comment"="ユーザID"})
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $userId;

    /**
     * @var string
     *
     * @ORM\Column(name="user_number", type="string", length=15, nullable=false, options={"fixed"=true,"comment"="会員番号"})
     */
    private $userNumber;

    /**
     * @var string
     *
     * @ORM\Column(name="user_name", type="string", length=255, nullable=false, options={"comment"="ユーザ名"})
     */
    private $userName;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="created_at", type="datetime", nullable=false, options={"comment"="作成日"})
     */
    private $createdAt;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="updated_at", type="datetime", nullable=false, options={"comment"="更新日"})
     */
    private $updatedAt;
}

"fixed"=trueを指定することで、Mysql上で、char指定になります。逆に指定していない、"type"="string"は、varchar指定になります。

Ⅳ. スキーマからマイグレーションの作成 & 実行

最後に、マイグレーションの作成と実行に入ります。ここまで時間をかけて作ったエンティティファイルが大活躍です。

ルートディレクトリ
# エンティティファイルとDBの差分からマイグレーション生成
php artisan doctrine:migrations:diff
# 結果例
Generated new migration class to "Versionhogehoge" from schema differences.

# マイグレーション実行!!
php artisan doctrine:migrations:migrate

マイグレーションは、database/migrationsディレクトリの直下に作成されます。
作成されたマイグレーションは以下の構成です。

Versionhogehoge.php
<?php

namespace Database\Migrations;

use Doctrine\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema as Schema;

class Version20211103183613 extends AbstractMigration
{
    /**
     * @param Schema $schema
     */
    public function up(Schema $schema): void
    {
        $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
	︙
	︙
	// このブロック内に、マイグレーションバージョンアップ時に実行する操作が記述されます。
	︙
	︙
    }

    /**
     * @param Schema $schema
     */
    public function down(Schema $schema): void
    {
        $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
	︙
	︙
	// このブロック内に、マイグレーションバージョンダウン時に実行する操作が記述されます。
	︙
	︙
    }
}

マイグレーションを自動で生成したあと、直接マイグレーションを修正することも可能ですが、今後自動生成を運用することを考えると、できるだけDoctrineのスキーマ記述で頑張ってスキーマ定義をしたほうが良いと思います。

これで、エンティティファイルのスキーマ等を修正するだけで、DBに対して様々な変更をかけるようになることができるようになりました!

まとめ

思ったより内容が多くなってしまいましたが、Laravel 7.x + Doctrineで、DBの管理をしていくための手法をご紹介させていただきました。
Prismaを触ってLaravelでも、自動生成をしたい!と思って既存プロジェクトに導入するまでに、かなり日本語ドキュメントが少なく苦労しました。
ぜひ、皆さんのお力になれればなと思っています。また、Laravel + Doctrine構成が流行ってほしいなぁと思っています笑

最後まで読んでいただき、ありがとうございましたm(_ _)m

Discussion