Doctrine2を使ってアプリケーションを開発していると、単なる一対多ではない階層構造(ツリー構造)を表すエンティティ(カテゴリや擬似フォルダなど)を使いたいケースが出てくることがあります。 そんな場合に便利なライブラリの一つ、 KnpDoctrineBehaviorsTree を紹介します。(他にも同様の機能を別の実装で実現しているライブラリがいくつかあります)

ツリー構造にするエンティティの定義方法

まず、 Category というエンティティを定義します。IDと名前だけを持つ、シンプルなエンティティです。

namespace Acme\DemoBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 *
 * @ORM\Entity(repositoryClass="Acme\DemoBundle\Entity\Repository\CategoryRepository")
 * @ORM\Table
 */
class Category
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;
}

Categoryをツリー構造で扱いたいので、ツリー構造で扱えるように定義を追加します。

use Doctrine\ORM\Mapping as ORM;
+use Knp\DoctrineBehaviors\Model\Tree\Node;
+use Knp\DoctrineBehaviors\Model\Tree\NodeInterface;

/**
 *
 * @ORM\Entity
 * @ORM\Table
 */
-class Category
+class Category implements NodeInterface
{
+    use Node;

    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;
}

これで準備完了です。

ツリー構造で保存する

一番上の親(ルート)になるCategoryを作ってみましょう。

$able = new Category();
$able->setName('able');
$em->persist($able);
$em->flush();

$able->isRootNode(); // true:何も親ノードを指定せずに保存すると新しいルートノードになる

いよいよ、ルートの $able$baker という子カテゴリを追加してみます。

$baker = new Category();
$baker->setName('baker');
$em->persist($baker);
$em->flush(); // 親ノードを指定するのにはprimary key(※厳密にはprimary keyでなくても良いが何か一意の値)が必要なので、$bakerの$idを自動生成させるために一度DBに保存する

$baker->isRootNode(); // true:まだ親を指定してないのでルートノード

$baker->setParentNode($able);
$em->flush();

$baker->isRootNode(); // false:親ノードを指定したのでルートノードじゃなくなった
$baker->isLeafNode(); // true 

ツリー構造で呼び出す

今度は、

  • able
    • baker
      • dog
    • charlie
      • easy
      • fox

のような able 配下のツリー構造を作って保存したものがあるとします。このツリーをDBから取り出してみましょう。 DBのレコードからツリー構造を作る機能は、EntityRepository用のトレイト Knp\DoctrineBehaviors\ORM\Tree\Tree に実装されています。 早速CategoryのEntityRepository定義を更新して、Treeを使えるようにしましょう。

<?php
namespace Acme\DemoBundle\Entity\Repository;

use Doctrine\ORM\EntityRepository;
+use Knp\DoctrineBehaviors\ORM\Tree\Tree; 

/**
 * class CategoryRepository
 */
class CategoryRepository extends EntityRepository
{
+    use Tree;
}

これで準備ができました。 では、 able をデータベースから読み出してみましょう。

$repository = $em->getRepository('AcmeDemoBundle:Category');
$able = $repository->findOneByName('able');

$able->getChildNodes(); // 空のArrayCollection(=要素数0の配列と同じ)

一見、 $able->getChildNodes() にはableの子要素が入っていても良さそうなものですが、実はここには 何も 入っていません。

ツリー構造を呼び出すには、通常のEntityRepository::find()ではない、別の呼び出し方が必要なのです。

// ableのIDは1とする
$repository = $em->getRepository('AcmeDemoBundle:Category');
$able = $repository->getTree('/1');

$able->getChildNodes(); // baker, charlie

まとめ

ドキュメントがほとんどなく、利用者も少ないのかあまり使い方が知られていませんが、Treeは使い方もシンプルで、使いこなせれば便利な機能だと思います。 機会があったら使ってみてください。