PEARでインストールしたDoctrine2を触ってみる

O/Rマッパーで何かいいのが無いかと調べてたらDoctrineというのがよさそうだったので使ってみよう!

…と思い立ったものの日本語の記事が少なく、しかもPEARから使ってるのがあんまありませんでした。
公式ドキュメントを見て色々苦労しながらチュートリアルってみたので備忘録を兼ねて記事にしてみようと思います。
インストール時のバージョンは2.3です。

インストール

インストールするパッケージは以下。

  • DoctrineCommon … 共通ライブラリ
  • DoctrineDBAL … 各DBMSの差分を吸収するレイヤー
  • DoctrineORM … O/Rマッパー

DoctrineDBALとDoctrineORMはsymfonyに依存しているようなのでチャンネルにpear.symfony.comも追加しておきます。

$ sudo pear channel-discover pear.doctrine-project.org
$ sudo pear channel-discover pear.symfony.com
$ sudo pear install pear.doctrine-project.org/DoctrineCommon
$ sudo pear install pear.doctrine-project.org/DoctrineDBAL
$ sudo pear install pear.doctrine-project.org/DoctrineORM

初期設定

ゼロベースからの初期設定(cakephpでいうところのbakeみたいなもん?)は

  • エンティティクラス+アノテーションで生成
  • エンティティクラス+XMLで生成
  • エンティティクラス+YAMLで生成

などが選べるようです。
XMLYAMLは別途設定ファイルを作成する必要があるため、コード内に直接記述できるアノテーションを選択しました。

まずはこんな感じのプロジェクト用ディレクトリを作成します。

project
|-- entities
|   |-- Company.php
|   `-- User.php
|-- bootstrap.php
|-- bootstrap_doctrine.php
`-- cli-config.php

まずエンティティを作成していきます。
CompanyクラスとUserクラスを定義します。
CompanyとUserが1対多な感じ。

<?php

/* Company.php */

/**                                                                                                                   
 * @Entity
 * @Table(name="companies")
 **/
class Company
{
    /** 
     * @Id
     * @Column(type="integer")
     * @GeneratedValue
     **/
    protected $id;

    /** 
     * @Column(type="string", length=50)
     **/
    protected $name;

    /** 
     * @OneToMany(targetEntity="User", mappedBy="company", cascade={"persist", "remove"})
     */
    private $users;
}

<?php

/* User.php */

/**
 * @Entity
 * @Table(name="users")
 **/
class User
{
    /** 
     * @Id
     * @Column(type="integer")
     * @GeneratedValue
     **/
    protected $id;

    /** 
     * @Column(type="string", length=50)
     **/
    protected $name;

    /** 
     * @ManyToOne(targetEntity="Company", inversedBy="users")
     * @JoinColumn(name="company_id", referencedColumnName="id")
     */
    private $company;
}   

アノテーションの意味は以下です。

  • @Entity … エンティティを表す
  • @Table(name="users") … テーブル情報を設定
  • @Id … 主キーを表す
  • @Column(type="XXXX", length=XX) … カラム情報を設定、null許可やunique制約をつけることも可能
  • @GeneratedValue … @Idと合わせて自動採番を指定、@GeneratedValue(strategy="XXXX")とすることで採番方法を選べる
  • @OneToMany(targetEntity="XXXX", mappedBy="XXXX") … 1対多のリレーションを貼る
  • @ManyToOne(targetEntity="XXXX", inversedBy="XXXX") … 多対1のリレーションを貼る
  • @JoinColumn(name="XXXX", referencedColumnName="XXXX") … 外部キーを設定

他にも便利なものがたくさんあり、ここでは書き切れませんので詳しい説明はリファレンスを参照して下さい。

続いて設定ファイルを用意します。

<?php

/* bootstrap_doctrine.php */

use Doctrine\ORM\Tools\Setup,
    Doctrine\ORM\EntityManager;

// PEARからインストールした場合のオートローダ(コマンドラインツール外から利用する場合に必要)
require_once 'Doctrine/ORM/Tools/Setup.php';
Setup::registerAutoloadPEAR();

// 開発モード
$isDevMode = true;

// DB接続設定
$conn = array(
    'driver'   => 'pdo_mysql',
    'host'     => '127.0.0.1',
    'dbname'   => 'dbname',
    'user'     => 'username',
    'password' => 'password'
);

// アノテーションでスキーマを定義する場合
$config = Setup::createAnnotationMetadataConfiguration(array(__DIR__ . '/entities'), $isDevMode);
// XMLでスキーマを定義する場合
// $config = Setup::createXMLMetadataConfiguration(array(__DIR__.'/toXmlDirectoryPath'), $isDevMode);
// YAMLでスキーマを定義する場合
// $config = Setup::createYAMLMetadataConfiguration(array(__DIR__.'/toYamlDirectory'), $isDevMode);

// エンティティマネージャを取得
$em = EntityManager::create($conn, $config);

<?php

/* bootstrap.php */

// 各エンティティを読み込む
require_once 'entities/Company.php';
require_once 'entities/User.php';

if (!class_exists('Doctrine\Common\Version', false)) {
    require_once 'bootstrap_doctrine.php';
}

<?php

/* cli-config.php */

require_once 'bootstrap.php';

// コンソール用設定
$helperSet = new \Symfony\Component\Console\Helper\HelperSet(array(
    'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($em)
));

コマンドラインツールではcli-config.phpを自動で読み込んでくれるようです。
通常のphpから叩く時はbootstrap.phpを明示的にrequireして下さい。

では、この状態でコマンドを叩いてDBを生成してみます。

$ doctrine orm:schema-tool:create
ATTENTION: This operation should not be executed in a production environment.

Creating database schema...
Database schema created successfully!

成功した模様。
DBを確認します。

mysql> desc companies;
+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+
| id    | int(11)     | NO   | PRI | NULL    | auto_increment |
| name  | varchar(50) | NO   |     | NULL    |                |
+-------+-------------+------+-----+---------+----------------+
2 rows in set (0.01 sec)

mysql> desc users;
+------------+-------------+------+-----+---------+----------------+
| Field      | Type        | Null | Key | Default | Extra          |
+------------+-------------+------+-----+---------+----------------+
| id         | int(11)     | NO   | PRI | NULL    | auto_increment |
| company_id | int(11)     | YES  | MUL | NULL    |                |
| name       | varchar(50) | NO   |     | NULL    |                |
+------------+-------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

無事作成できました。
これで初期設定は完了です。

DB操作をしてみる

では最後に簡単なINSERTとSELECTを行なっていきます。
更新、参照ではエンティティにセッター、ゲッターを追加する必要があります。
再度Company.phpとUser.phpを編集します。

<?php

/* Company.php */

// Userエンティティ用にArrayCollectionを利用
use Doctrine\Common\Collections\ArrayCollection;

/**
 * @Entity
 * @Table(name="companies")
 **/
class Company
{
    // ... (プロパティ設定)

    public function __construct() {
        $this->users = new \Doctrine\Common\Collections\ArrayCollection();
    }

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

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

    public function assignedToUser($user)
    {
        $this->users[] = $user;
    }

    public function getUsers()
    {
        return $this->users;
    }
}

<?php

/* User.php */

/**
 * @Entity
 * @Table(name="users")
 **/
class User
{
    // ... (プロパティ設定)

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

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

    public function setCompany($company)
    {
        // 双方向リレーションを実現
        $company->assignedToUser($this);
        $this->company = $company;
    }

    public function getCompany()
    {
        return $this->company;
    }
}

Companyは複数のUserをセットできるようArrayCollectionを利用します。
また、Doctrineでは双方向のリレーションが使用できます。
実現する為にUserへのCompanyセット時に自身をCompanyに対してセットしています。

続いて簡単なINSERT用スクリプトを用意。

<?php

/* insert.php */

require_once './bootstrap.php';

$company = new Company();
$company->setName('google');

$user1 = new User();
$user1->setName('shigeru');
$user1->setCompany($company);

$user2 = new User();
$user2->setName('hatsue');
$user2->setCompany($company);

$em->persist($company);
$em->flush();

echo "作成成功!\n";

インスタンスを生成してパラメータをセットした後、エンティティマネージャにpersist()で登録していきます。
アノテーションで「cascade={"persist", "remove"}」を記述している為、Userをpersist()する必要はありません。
またflush()を実行しないと更新は行われないので注意。

SELECT用スクリプトも用意します。

<?php

/* select.php */

require_once './bootstrap.php';                                                              

$dql = 'SELECT c FROM Company c';                                                            
    
$query = $em->createQuery($dql);                                                             
$companies = $query->getResult();                                                            

foreach($companies as $company) {                                                            
    echo "Company name is {$company->getName()}.\n";
    foreach($company->getUsers() as $user) {
        // CompanyからUserを取得
        echo "User name is {$user->getName()}.\n";
        // UserからCompanyを取得
        echo "{$user->getName()}'s company name is {$user->getCompany()->getName()}.\n";
    }
}

DoctrineではDQL(Doctrine Query Language)という専用のクエリ言語を用いて結果を取得していきます。
実行結果は以下です。

$ php insert.php
作成成功!
$ php select.php 
Company name is google.
User name is shigeru.
shigeru's company name is google.
User name is hatsue.
hatsue's company name is google.

良い感じですね!

感想

まだまだ上辺しか触っていませんが、なんとなく今までのシステムよりもスッキリしてる印象を受けます。
DQLを覚えつつ、Repository周りにDB処理を突っ込んで行けば更にスマートなDB操作ができるんじゃないでしょうか。
とりあえずもう少し深く触ってみようと思います。