PHP7 + Doctrine ODMでMongoDBを使う

[Symfony Advent Calendar 2016](http://qiita.com/advent-calendar/2016/symfony) 8日目の記事です。

---

SymfonyでMongoDBを扱う場合、Doctrine ODM(Object Document Mapper)を使うと、ORMと同様のAPIでMongoDBを扱うことができます。

## 必要なもの

- MongoDB
- PHP5系+[ext-mongo](http://php.net/manual/ja/book.mongo.php) または PHP7系+[ext-mongodb](http://php.net/manual/ja/class.mongodb.php)

```
$ mongo --version
MongoDB shell version v3.4.0

$ php -v
PHP 7.1.0 (cli) (built: Dec  2 2016 11:32:42) ( NTS )

$ php -m | grep mongo
mongodb
```

## インストール

Doctrine ODMは単独のライブラリとしても使えますが、ここでは[DoctrineMongoDBBundle](http://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html)を使って、Symfonyと一緒に使ってみます。

Symfonyのインストール方法については[公式サイト](https://symfony.com/doc/current/setup.html)を参照してください。
以下はSymfonyインストーラーを使った例です。

```
$ symfony new doctrine_odm
$ cd doctrine_odm
$ php bin/console --version
Symfony 3.2.0 (kernel: app, env: dev, debug: true)
```

次にDoctrineMongoDBBundleをインストールします。
PHP5の場合とPHP7の場合で手順が異なります。

### PHP5の場合

composer.jsonに以下の2行を追加します。

```
{
    "require": {
        "doctrine/mongodb-odm": "^1.1",
        "doctrine/mongodb-odm-bundle": "^3.2"
    }
}
```

次に、`composer update`を実行します。

### PHP7の場合

composer.jsonに以下の3行を追加します。

```
{
    "require": {
        "alcaeus/mongo-php-adapter": "^1.0",
        "doctrine/mongodb-odm": "^1.1",
        "doctrine/mongodb-odm-bundle": "^3.2"
    }
}
```

MongoDBのドライバは、PHP5とPHP7で異なります。
PHP5の[ext-mongo](http://php.net/manual/ja/book.mongo.php)は、PHP7では動作しません。
逆に、PHP7の[ext-mongodb](http://php.net/manual/ja/class.mongodb.php)もPHP5では使えません。

[alcaeus/mongo-php-adapter](https://github.com/alcaeus/mongo-php-adapter)は、PHP7のMongoDBドライバをPHP5のMongoDBドライバと同様のAPIで使えるようにするアダプタです。
Doctrine ODMはPHP5のMongoDBドライバを前提としているので、PHP7+Doctrine ODMの組み合わせで使うには、このアダプタが必要です。
本アダプタの注意点として、ネイティブの実装に比べてパフォーマンスが低いという問題が指摘されています([GitHub Issues](https://github.com/alcaeus/mongo-php-adapter/issues/124))。

インストールには`composer update　--ignore-platform-reqs`を実行します。
`--ignore-platform-reqs`は、PHPのバージョンやPHP拡張等の依存関係チェックを無視するオプションです。
doctrine/mongodbのcomposer.jsonにはext-mongo(PHP5のMongoDBドライバ)が依存ライブラリとして指定されているため、このライブラリを使えないPHP7では、上記オプションを使用する必要があります。

## Symfonyの設定

### オートローダー設定

```
// app/autoload.php
use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver;
AnnotationDriver::registerAnnotationClasses();
```

### AppKernelに登録

```
// app/AppKernel.php
public function registerBundles()
{
    $bundles = [
        // ...
        new Doctrine\Bundle\MongoDBBundle\DoctrineMongoDBBundle(),
    ];

    // ...
}
```

### 設定ファイル

```
# app/config/parameters.yml
parameters:
    mongodb_server: "mongodb://localhost:27017"
```

```
# app/config/config.yml
doctrine_mongodb:
    connections:
        default:
            server: "%mongodb_server%"
            options: {}
    default_database: test_database
    document_managers:
        default:
            auto_mapping: true
```

## ドキュメントクラスの追加

```
$ mkdir src/AppBundle/Document
$ touch src/AppBundle/Document/Product.php
```

Product.phpの中身は以下。Doctrine ORMと同様、アノテーションでマッピング情報を追加します。

```
<?php

// src/AppBundle/Document/Product.php
namespace AppBundle\Document;

use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;

/**
 * @MongoDB\Document
 */
class Product
{
    /**
     * @MongoDB\Id
     */
    protected $id;

    /**
     * @MongoDB\Field(type="string")
     */
    protected $name;

    /**
     * @MongoDB\Field(type="float")
     */
    protected $price;
}
```

以下のコマンドでgetter/setterを生成します。この辺もORMと同じですね。

```
$ php bin/console doctrine:mongodb:generate:documents AppBundle
```

## 実行テスト

src/AppBundle/Command/MongoDBTestCommand.php に、以下の内容のファイルを作成します。

```
<?php

namespace AppBundle\Command;

use AppBundle\Document\Product;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class MongoDBTestCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setName('app:mongo-test')
            ->setDescription('...')
            ->addArgument('argument', InputArgument::OPTIONAL, 'Argument description')
            ->addOption('option', null, InputOption::VALUE_NONE, 'Option description')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        // データの永続化
        $product = new Product();
        $product->setName('Foobar');
        $product->setPrice(19.99);
        
        $dm = $this->getContainer()->get('doctrine_mongodb')->getManager();
        $dm->persist($product);
        $dm->flush();
        
        // データ取得
        $product = $dm->getRepository('AppBundle:Product')
            ->findOneBy(['name' => 'Foobar']);
        
        if (!$product) {
            $output->writeln('Product not found');
        }
        
        var_dump($product->getName(), $product->getPrice());
    }
}
```

上記コマンドで実行確認できます。

```
$ php bin/console app:mongo-test
```

MongoDBクライアントでも結果を確認してみましょう。

```
$ mongo test_database
> db.Product.find();
{ "_id" : ObjectId("584559656adfe57b63216d51"), "name" : "Foobar", "price" : 19.99 }
```

上記手順で作成したプロジェクトは[GitHub](https://github.com/ryo-utsunomiya/doctrine_odm_with_php7)で公開しています。

## まとめ

以上、Doctrine ODMの基本的な使い方です。基本的なAPIはORMと似ていますが、MongoDB独特のAPIを扱うこともできます。詳細は[公式ドキュメント](http://docs.doctrine-project.org/projects/doctrine-mongodb-odm/en/latest/index.html)を参照してください。