LionHeart SD BLOG

株式会社ライオンハート システムデザインの技術ブログ

インストールしたPropelを実際に利用してみる(その5:Behaviors編[3])

こんにちは、株式会社ライオンハートの鵜飼です。

その1その2その3その4に引き続き、Propelについてのメモその5です。

前回に引き続き、Behaviorsについてのメモを残していきます。公式ドキュメントはコチラですので、併せて参照ください。

なお、まだまだPropelについては面白そうな機能がありそうですが、一旦これでキリを付けておこうと思います。

NestedSet Behavior

利用するケースは少なそうですが、入れ子構造(ツリー構造)を持たせたデータを保存しておく場合の設定です。

<table name="section">
  <column name="id" required="true" primaryKey="true" autoIncrement="true" type="integer" />
  <column name="title" type="varchar" required="true" primaryString="true" />
  <behavior name="nested_set" />
</table>

データ挿入

実際に入れ子構造をもたせる場合の指定は、ちょっと複雑な記述となりますが下記のようになります。

<?php
// $s1 の作成
$s1 = new Section();
$s1->setTitle('Home');
$s1->makeRoot(); // 大本(ルート)のデータを作成
$s1->save();

// $s2 の作成
$s2 = new Section();
$s2->setTitle('World');
$s2->insertAsFirstChildOf($s1); // $s1の下に挿入
$s2->save();

// $s3 の作成
$s3 = new Section();
$s3->setTitle('Europe');
$s3->insertAsFirstChildOf($s2); // $s2の下に挿入
$s3->save();

// $s4 の作成
$s4 = new Section();
$s4->setTitle('Business');
$s4->insertAsNextSiblingOf($s2); // $s2の後ろに挿入
$s4->save();

/* 構造は下記のような形になります
    $s1:Home
    |       \
$s2:World  $s4:Business
    |
$s3:Europe
*/

入れ子構造に挿入する際に用意されているメソッドは、下記4つが準備されているようです。

insertAsFirstChildOf
指定したデータの子の先頭に挿入
insertAsLastChildOf
指定したデータの子の最後に挿入
insertAsPrevSiblingOf
指定したデータの同列で、自分の前に挿入
insertAsNextSiblingOf
指定したデータの同列で、自分の後に挿入

データ取得

取得する時もまたちょっとややこしいですが、こんな感じになります。

<?php
$rootNode = SectionQuery::create()->findRoot(); // $s1
$worldNode = $rootNode->getFirstChild();        // $s2
$businessNode = $worldNode->getNextSibling();   // $s4
$firstLevelSections = $rootNode->getChildren(); // array($s2, $s4)
$allSections = $rootNode->getDescendants();     // array($s2, $s3, $s4)

// チェーンメソッドで取得することも可能です
$europeNode = $rootNode->getLastChild()
                       ->getPrevSibling()
                       ->getFirstChild();       // $s3
$path = $europeNode->getAncestors();            // array($s1, $s2)

取得時のメソッドは、それぞれこんな役目のようです。

findRoot
大本(ルート)のデータを取得
getFirstChild
子のデータ群の内、先頭のデータを取得
getLastChild
子のデータ群の内、最後のデータを取得
getPrevSibling
同列のデータ群の内、自分の前のデータを取得
getNextSibling
同列のデータ群の内、自分の後のデータを取得
getAncestors
親のデータを再帰的に全て取得
getDescendants
子のデータを再帰的に全て取得

データ判定

構造自体が複雑になるので、入れ子構造内のデータについての判定フラグが幾つか存在しています。

<?php
echo $s2->isRoot();        // false : 大本(ルート)のデータか?
echo $s2->isLeaf();        // false : 一番子(葉)のデータか?
echo $s2->getLevel();      // 1 : 何階層目か?
echo $s2->hasChildren();   // true : 子はあるか?
echo $s2->countChildren(); // 1 : 子はいくつあるか?
echo $s2->hasSiblings();   // true : 同列のデータ(兄弟)はあるか?

データ移動

登録されているデータを移動させることも可能です。

<?php
/* データ挿入後の構造が存在している状態と仮定します
    $s1:Home
    |      \
$s2:World  $s4:Business
    |
$s3:Europe
*/

// $s2を$s4の子に移動させる
$s2->moveToFirstChildOf($s4);

/* 移動すると構造は下記のようになります
$s1:Home
  |
$s4:Business
  |
$s2:World
  |
$s3:Europe
*/

// $s3を$s4の後に移動する
$s3->moveToNextSiblingOf($s4);

/* 移動すると構造は下記のようになります
    $s1:Home
    |        \
$s4:Business $s3:Europe
    |
$s2:World
*/

データ削除

データの削除についても通常の削除処理に加えて便利なメソッドが追加されます。

<?php
/* データ移動後の構造が存在している状態と仮定します
    $s1:Home
    |        \
$s4:Business $s3:Europe
    |
$s2:World
*/

// $s4の子を全て削除
$s4->deleteDescendants();

/* 削除すると構造は下記のようになります
    $s1:Home
    |        \
$s4:Business $s3:Europe
*/

データ検索

検索条件に入れ子構造を利用することができます。

<?php
$children = SectionQuery::create()
  ->childrenOf($rootNode)
  ->orderByTitle()
  ->find();

また、前述の「データ取得」のメソッドに、条件を含めることも可能です。

<?php
$orderQuery = SectionQuery::create()->orderByTitle();
$children = $rootNode->getChildren($orderQuery);

Sortable Behavior

Autoincrementに似たような機能ですが、自動的に採番を行い順位付けを行うことができる設定です。

<table name="task">
  <column name="id" required="true" primaryKey="true" autoIncrement="true" type="integer" />
  <column name="title" type="varchar" required="true" primaryString="true" />
  <behavior name="sortable" />
</table>

sortable_rankというカラムが自動的に生成され、そこに採番が行われます。

<?php
// $t1 を生成
$t1 = new Task();
$t1->setTitle('Wash the dishes');
$t1->save();
echo $t1->getRank(); // 1、 0は利用されません

// $t2 を生成
$t2 = new Task();
$t2->setTitle('Do the laundry');
$t2->save();
echo $t2->getRank(); // 2

// $t3 を生成
$t3 = new Task();
$t3->setTitle('Rest a little');
$t3->save()
echo $t3->getRank(); // 3

データ取得

sortableを設定したテーブルのデータは順番に取得することができます。

<?php
$firstTask = TaskQuery::create()->findOneByRank(1); // $t1
$secondTask = $firstTask->getNext();      // $t2
$lastTask = $secondTask->getNext();       // $t3
$secondTask = $lastTask->getPrevious();   // $t2

$allTasks = TaskQuery::create()->findList();
// => collection($t1, $t2, $t3)
$allTasksInReverseOrder = TaskQuery::create()->orderByRank('desc')->find();
// => collection($t3, $t2, $t1)

データ判定

リスト内のデータについての判定用メソッドが用意されています。

<?php
echo $t2->isFirst(); // false : 最初のデータか?
echo $t2->isLast();  // false : 最後のデータか?
echo $t2->getRank(); // 2 : 何番目のデータか?

データ移動

リストの順番は下記のようなメソッドを利用することで変更することが可能です。

<?php
// 最初の順番 : $t1 - $t2 - $t3
// 下記はそれぞれ $t2 の位置を変更していきます

// 一番上に移動する : $t2 - $t1 - $t3
$t2->moveToTop();

// 一番下に移動する : $t1 - $t3 - $t2
$t2->moveToBottom();

// 一つ上に移動する : $t1 - $t2 - $t3
$t2->moveUp();

// 指定の要素と入れ替える : $t2 - $t1 - $t3
$t2->swapWith($t1);

// 指定位置に移動する : $t1 - $t3 - $t2
$t2->moveToRank(3);

// 指定位置に移動する : $t1 - $t2 - $t3
$t2->moveToRank(2);

データ挿入

特定位置に挿入することも可能です。

<?php
// 最初の順番 : $t1 - $t2 - $t3
$t4 = new Task();
$t4->setTitle('Clean windows');
$t4->insertAtRank(2);
$t4->save();
// 挿入後の順番 : $t1 - $t4 - $t2 - $t3

データ削除

データを削除すると、自動的に採番を直してくれます。

<?php
$t4->delete();
// 削除後の順番 : $t1 - $t2 - $t3

雑感

今回のBehaviorはちょっとややこしく出来ることが多い内容でしたが、実際に同じようなモノを作ろうとすると大変そうなので、便利な仕組みではあると感じますね。

その他、NestedSetSortableも、単一の条件の処理しかサンプルを残していませんでしたが、どちらも複数のキーを持たせることが出来るようです。設定自体はややこしくなりますが、使いこなせれば面白そうです。

また、前置き書かせていただきましたが、Propelについてのまとめは一旦これで一区切りをさせていただいて、まだ何にしようか考えていませんが、新しい何かを探してみようと思います。

よろしくお願いいたします。