laravelでdoctrine


はじめに

https://github.com/laravel-doctrine/orm を使ってlaravelでDoctrineしてみた。

インストール

Laravelプロジェクトの作成。

curl -s "https://laravel.build/laravel-doctrine" | bash
cd laravel-doctrine

依存ライブラリのインストール。

sail composer req doctrine/inflector:^1.4 laravel-doctrine/orm laravel-doctrine/migrations
sail artisan vendor:publish --tag="config"

EntityとRepositoryの作成

Entity

larave-doctrineを使う時はEloquentのモデルを用いません。代わりにapp/Entities/以下にモデルの代わりのクラスであるエンティティを作成します。エンティティにはアノテーションでカラム情報などのメタデータを付与する必要がありますが、基本的にはPOPOとして定義できるのがEloquentとの大きな違いです。また、カラムとのマッピングはymlやxmlで定義することもでき、そのようにするとエンティティは純粋なPOPOになります。メタデータに関してはこちらを参照ください。

laravelのレスポンスに直接渡せるようにIlluminate\Contracts\Support\Arrayableをimplementsしておくと便利です。

app/Entities/User.php
<?php

namespace App\Entities;

use Doctrine\ORM\Mapping as ORM;
use Illuminate\Contracts\Support\Arrayable;

/**
 * @ORM\Entity
 */
class User implements Arrayable
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string")
     */
    protected $name;

    public function getId()
    {
        return $this->id;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setName($name): self
    {
        $this->name = $name;
        return $this;
    }

    public function toArray()
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
        ];
    }
}

Repository

Doctrineを使った設計では、基本的に一つのエンティティに対して一つのリポジトリを作ります。リポジトリクラスでは以下のようにDoctrine\ORM\EntityRepositoryを継承し、コンストラクタで親クラスにEntityManagerInterfaceとメタデータを渡します。

また、Doctrine\ORM\EntityRepositoryにはfind()findAll()findBy()findOneBy()count()などのよく使うメソッドがあらかじめ定義されているので、これらのメソッドは自分で実装する必要がありません。

app/Repository/UserRepository.php
<?php

namespace App\Repository;

use App\Entities\User;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;

class UserRepository extends EntityRepository
{
    public function __construct(EntityManagerInterface $em)
    {
        parent::__construct($em, $em->getClassMetadata(User::class));
    }
}

マイグレーションの作成と実行

Eloquentを使う時はマイグレーションファイルは自前で書く必要がありました。Doctrineでは、エンティティに付与したメタデータと実際のDBとの差分を見て自動でマイグレーションを作ってくれます(めちゃくちゃ便利)。

sail artisan doctrine:migrations:diff
sail artisan doctrine:migrations:migrate

簡単な例

コントローラーを作成し、ルートに登録します。

routes/api.php
<?php

use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;

Route::group(['auth:api'], function () {
    Route::resource('users', UserController::class)->only([
        'index', 'store', 'show', 'destroy'
    ]);;
});

コントローラーの各メソッドでは、先ほど作ったリポジトリをメソッドインジェクションできます。リポジトリにはfindXXX()系のメソッドが既に定義されているので、簡単な検索ならメソッドを追加するまでもありません。

リポジトリはデータソースとしての役割のみ持っていますので、「データの更新」や「データの削除」などDBへの書き込みはエンティティマネージャーを通して行う必要があります。エンティティマネージャーはlaravel-doctrineでFacadeとして提供されているのでEntityManager::persist($user)EntityManager::flush()のように使うことも可能です。

app/Http/Controllers/UserController.php
<?php

namespace App\Http\Controllers;

use App\Entities\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Illuminate\Http\JsonResponse;

class UserController extends Controller
{
    public function index(UserRepository $repository): JsonResponse
    {
        $users = $repository->findAll();

        return response()->json(collect($users));
    }

    public function store(EntityManagerInterface $entityManager): JsonResponse
    {
        $user = new User();
        $user->setName('test');

        $entityManager->persist($user);
        $entityManager->flush();

        return response()->json($user);
    }

    public function show($id, UserRepository $repository): JsonResponse
    {
        $user = $repository->find($id);

        return response()->json($user);
    }

    public function destroy($id, UserRepository $repository, EntityManagerInterface $entityManager): JsonResponse
    {
        $user = $repository->find($id);
        if (!$user) {
            return response()->json([], 404);
        }

        $entityManager->remove($user);
        $entityManager->flush();

        return response()->json();
    }
}

こんな感じに動きます。

$ curl http://localhost/api/users
[]

$ curl http://localhost/api/users -X POST
{"id":1,"name":"test"}

$ curl http://localhost/api/users -X POST
{"id":2,"name":"test"}

$ curl http://localhost/api/users -X POST
{"id":3,"name":"test"}

$ curl http://localhost/api/users/2
[{"id":2,"name":"test"}]

$ curl http://localhost/api/users/2 -X DELETE
[]

$ curl http://localhost/api/users
[{"id":1,"name":"test"},{"id":3,"name":"test"}]

感想

Eloquentを使うメリットはやはりLaravelとの親和性がとても高いところです。しかし、リポジトリパターンを使おうとすると、Eloquentの便利メソッド系はリポジトリ内でしか使えないという暗黙のルールができてしまいます。それなら最初からリポジトリを考慮した設計のORM使った方がよくない?と思い今回laravel-doctrineを触ってみました。

まだ本当に基本的なことしかしてないですが、ORMとしての機能はまったく問題なさそうな感じがします。また、エンティティにはIlluminate\Contracts\Support\Arrayableをはじめとするインターフェイス類をimplementsさせればLaravelとの親和性も思ったほど低くならないかも?とも思いました。

もう少し色んなパターンで触ってみます。

続編