TRAJOIN is an Application to Translate symfony documents Jointly.
home > 1.2/cookbook/en > sortable.txt
[1] Edit ↑TOP多くのウェブアプリケーションは項目を並び替えるインターフェイスを提供する必要があります。ウェブログのカテゴリー、CMSの記事、Eコマースウェブサイトのウィッシュ・・・を考えてみましょう。旧式の方法はリストで項目を一つ上げたり下げたりするための矢印を提供することです。AJAXによる方法はサーバーのサポートで直接ドラッグアンドドロップをできるようにすることです。この章では、オブジェクトモデルを強化する方法とCreoleを使った複雑なクエリをする方法の両方といくつかのティップを説明します。

For this article, the example used will be an undefined Item table - name it according to your needs. In order to be sortable, records need at least a rank field - no need for a heap here since the sorting will be done by the user, not by the computer. So the data structure (to be written in the schema.yml) is simply:
propel:
test_item:
_attributes: { phpName: Item }
id:
name: varchar(255)
rank: { type: integer, required: true }
定義されたデータ構造を一旦コマンドラインインターフェイスで入力して必ずビルドして下さい:
$ symfony propel-build-model
同じ構造を持つデータベースも一つ必要です。最速の方法は呼び出すことです:
$ symfony propel-build-sql
$ symfony propel-insert-sql
ユーザーインターフェイスを考える前に、ランクによって項目を取得する、ランクによって並び替えられた項目のリストを取得する、現在の最大ランクを取得する方法を次のメソッドをlib/model/ItemPeer.phpに追加することで確認して下さい:
static function retrieveByRank($rank = 1)
{
$c = new Criteria;
$c->add(self::RANK, $rank);
return self::doSelectOne($c);
}
static function getAllByRank()
{
$c = new Criteria;
$c->addAscendingOrderByColumn(self::RANK);
return self::doSelect($c);
}
static function getMaxRank()
{
$con = Propel::getConnection(self::DATABASE_NAME);
$sql = 'SELECT MAX('.self::RANK.') AS max FROM '.self::TABLE_NAME;
$stmt = $con->prepare($sql);
$stmt->execute();
$row = $stmt->fetch();
return $row['max'];
}
これらのメソッドはインターフェイスをソートするために大いに役立ちます。symfonyにおいてオブジェクトモデルがデータベースを取り扱う方法についてもっと詳しい情報が必要でしたら、Propelの基本的なCRUDの章を確認して下さい。
lib/model/Item.phpに追加されることが必要な2つ以上のメソッドがあります。それらはここでは必要ありませんが、テーブルに項目を追加と削除する実際の世界のアプリケーションではおそらく必要でしょう:
public function save(PropelPDO $con = null)
{
// New records need to be initialized with rank = maxRank +1
if(!$this->getId())
{
$con = Propel::getConnection(ItemPeer::DATABASE_NAME);
try
{
$con->beginTransaction();
$this->setRank(ItemPeer::getMaxRank()+1);
parent::save();
$con->commit();
}
catch (Exception $e)
{
$con->rollback();
throw $e;
}
}
else
{
parent::save();
}
}
public function delete(PropelPDO $con = null)
{
$con = Propel::getConnection(ItemPeer::DATABASE_NAME);
try
{
$con->beginTransaction();
// decrease all the ranks of the page records of the same category with higher rank
$sql = 'UPDATE '.ItemPeer::TABLE_NAME.' SET '.ItemPeer::RANK.' = '.ItemPeer::RANK.' - 1 WHERE '.ItemPeer::RANK.' > '.$this->getRank();
$con->exec($sql);
// delete the item
parent::delete();
$con->commit();
}
catch (Exception $e)
{
$con->rollback();
throw $e;
}
}
レコードの追加と削除はrankフィールドの整合性のために注意深く管理されなければなりません。save()とdelete()メソッドが特化されているのはそういうわけです。これらのメソッドは複雑な読み込み/書き込みの実行を行い、同時並行問題のリスクを作成しますので、これらの実行はトランザクションで行われます(symfonyにおけるトランザクションについての詳細な情報はPropelのドキュメントを参照して下さい)。
このチュートリアルで説明されたインタラクションはitemモジュールで行われます。次のコードを呼び出すことで初期化して下さい(frontendアプリケーションであると仮定します):
$ symfony init-module frontend item
好みのブラウザ経由で新しいモジュールへのアクセスをテストすることでウェブサーバー環境設定がうまくいっていることを確認して下さい。サンドボックスでこのチュートリアルに従った場合にチェックするURLは以下の通りです:
http://localhost/sf_sandbox/web/frontend_dev.php/item
Finally, if you want to test the ordering of items, you will need... items. Create a bunch of test items, either via a CRUD interface or a population file.
すべての準備が整いました。始めましょう。
古典的なソート可能なリストはそれぞれの項目が順番を変更するコントロールを持つためのリストです。最初、リストを表示するアクションとテンプレートを作成します:
// modules/item/actions/actions.class.phpに追加します
public function executeList()
{
$this->items = ItemPeer::getAllByRank();
$this->max_rank = ItemPeer::getMaxRank();
}
// modules/item/templates/にあるlistSuccess.phpテンプレートを作成します
<h1>Ordered list of items</h1>
<ul>
<?php foreach($items as $item): ?>
<li>
<?php
echo $item->getName().' ';
if($item->getRank() > 0):
echo link_to('Move up ', 'item/up?id='.$item->getId());
endif;
if($item->getRank() != $max_rank):
echo link_to('Move down', 'item/down?id='.$item->getId());
endif;
?>
</li>
<?php endforeach ?>
</ul>
項目を上げ下げするリンクは並び替え直すことが可能であるときのみ表示されます。このことは最初の項目はそれ以上上げることはできず、最後の項目はそれ以上下げることはできません。正しくページが表示されるか確認して下さい:
http://localhost/sf_sandbox/web/frontend_dev.php/item/list
では、item/upとitem/downアクションを見てましょう。upアクションはパラメータとして与えられるページのランクを減らし、以前のページのランクを増やします。downアクションは与えられたパラメータとしてのページのランクを増やし、次のページのランクを減らします。両方ともデータベースで2つの書き込み実行をし、これらのアクションはトランザクションを使用します。
2つのアクションはとても似たようなロジックを持ち、D.R.Yを保ちたい場合、コードを繰り返すことなく書くよりスマートな方法が見つかります。swapWith()メソッドをItem.phpモデルクラスに追加することで行われます:
public function swapWith($item)
{
$con = Propel::getConnection(ItemPeer::DATABASE_NAME);
try
{
$con->beginTransaction();
$rank = $this->getRank();
$this->setRank($item->getRank());
$this->save();
$item->setRank($rank);
$item->save();
$con->commit();
}
catch (Exception $e)
{
$con->rollback();
throw $e;
}
}
そして、upとdownアクションはとてもシンプルです:
public function executeUp()
{
$item = ItemPeer::retrieveByPk($this->getRequestParameter('id'));
$this->forward404Unless($item);
$previous_item = ItemPeer::retrieveByRank($item->getRank() - 1);
$this->forward404Unless($previous_item);
$item->swapWith($previous_item);
$this->redirect('item/list');
}
public function executeDown()
{
$item = ItemPeer::retrieveByPk($this->getRequestParameter('id'));
$this->forward404Unless($item);
$next_item = ItemPeer::retrieveByRank($item->getRank() + 1);
$this->forward404Unless($next_item);
$item->swapWith($next_item);
$this->redirect('item/list');
}
セキュリティのためにforward404Unless()へのコールによってチェックがなされない場合、これらのアクションはよりシンプルですが、間違ったリクエスト -URLを直接入力することで行われます- に対してアプリケーションを守らなければなりません。
リストはこれで十分に機能します。リストの項目を上げ下げしてみて下さい。
基本的なAJAXのソート可能なリストを開発することは古典的なものを開発するよりも難しくありません。大抵のジョブはsortable_element()と呼ばれる特別なJavaScriptヘルパーによって取り扱われます:
// modules/item/actions/actions.class.phpに追加します
public function executeAjaxList()
{
$this->items = ItemPeer::getAllByRank();
}
// ajaxListSuccess.phpテンプレートをmodules/item/templates/に作成します
<?php use_helper('Javascript') ?>
<style>
.sortable { cursor: move; }
</style>
<h1>Ordered list of items - AJAX enabled</h1>
<ul id="order">
<?php foreach($items as $item): ?>
<li id="item_<?php echo $item->getId() ?>" class="sortable">
<?php echo $item->getName() ?>
</li>
<?php endforeach ?>
</ul>
<div id="feedback"></div>
<?php echo sortable_element('order', array(
'url' => 'item/sort',
'update' => 'feedback',
)) ?>
次のURLを入力して結果を確認します:
http://localhost/sf_sandbox/web/frontend_dev.php/item/ajaxlist
sortable_element()JavaScriptヘルパーのマジックによって、<ul>要素はソート可能になります。このことはその子要素はドラッグドロップによって並び直すことが出来ることを意味します。ユーザーがリストを並び直すために項目をドラッグして放すたびに、AJAXリクエストは次のパラメータでなされます:
POST /sf_sandbox/web/frontend_dev.php/item/sort HTTP/1.1
order[]=1&order[]=3&order[]=2&order[]=4&order[]=5&order[]=6&_=
並び替えられたリストのすべては配列として渡されます(idプロパティのリスト要素にあるアンダースコア(_)の後で来たものに基づいて0と$idで始まるorder[$rank]=$id、$orderのフォーマットです)。ソート可能な要素(例ではorder)のidプロパティはパラメータの配列を命名するために使用されます。JavaScriptヘルパーはXMLHttpRequestをurlアクション(例ではitem/sort)にし、POSTモードで並べられたリストを私、id update(例ではfeedbackdiv)の要素を更新するアクションの結果を使用します
item/sortアクションを書き、項目のリストをどのように並び替えるのか見てみましょう:
// modules/item/actions/actions.class.phpに追加します
public function executeSort()
{
$order = $this->getRequestParameter('order');
$flag = ItemPeer::doSort($order);
return $flag ? sfView::SUCCESS : sfView::ERROR;
}
全体のリストを並び替える能力はモデルの部分です。ItemPeerクラスのスタティックメソッドが実行される理由はそういうわけです。繰り返しますが、このメソッドがitemテーブルの多くのレコードを更新するという事実によってデータベーストランザクションで更新を一緒にすることが必要になります。
static function doSort($order)
{
$con = Propel::getConnection(self::DATABASE_NAME);
try
{
$con->beginTransaction();
foreach ($order as $rank => $id)
{
$item = ItemPeer::retrieveByPk($id);
if($item->getRank() != $rank)
{
$item->setRank($rank);
$item->save();
}
}
$con->commit();
return true;
}
catch (Exception $e)
{
$con->rollback();
return false;
}
}
このメソッドによって返された値はアクションが表示するテンプレートを決定します。modules/item/templates/フォルダに次のテンプレートを追加して下さい:
// sortSuccess.php
Ok
// sortError.php
<strong>A problem occurred. Please refresh and try again.</strong>
リストを並び替えた後にF5を押してサーバーハンドリングをテストして下さい。サーバーが理解しAJAXリクエストが送ったことを正しく保存したのであれば、並び替えは変更されません。
sortable_element()オプションに焦点を当てるThe Javascript helpers chapter describes the generic options of remote function calls, but this example is a good opportunity to see the ones of the sortable_element() in detail.
hoverclassパラメータでリストホーバーされた要素に他の要素をドラッグしているとき異なる外見を定義することができます:
<?php use_helper('Javascript') ?>
<style>
.sortable { cursor: move; }
.hovered { font-weight: bold; }
</style>
...
<?php echo sortable_element('order', array(
'url' => 'item/sort',
'hoverclass' => 'hovered',
)) ?>
ソートできない要素をリストに追加しonlyパラメータによってのみ単独のクラスにドラッグアンドドロップの振る舞いを制限することが出来ます:
...
<ul id="order">
<?php foreach($items as $item): ?>
<li id="item_<?php echo $item->getId() ?>" class="sortable">
<?php echo $item->getName() ?>
</li>
<?php endforeach ?>
<li>This element is not part of the ordered list</li>
</ul>
<?php echo sortable_element('order', array(
'url' => 'item/sort',
'only' => 'sortable',
)) ?>
前の例においてリスト要素が上下に表示されない場合、overlapパラメータをhorizontalに設定しなければなりません:
<?php use_helper('Javascript') ?>
<style>
.sortable { cursor: move; float: left; }
</style>
...
<?php echo sortable_element('order', array(
'url' => 'item/sort',
'overlap' => 'horizontal',
)) ?>
並び替えるリストが<li>要素のセットではない場合、どのソート可能な要素の子要素がドラッグ可能であるか定義しなければなりません:
...
<div id="order">
<?php foreach($items as $item): ?>
<div id="item_<?php echo $item->getId() ?>" class="sortable">
<?php echo $item->getName() ?>
</div>
<?php endforeach ?>
<p>This cannot be dragged</p>
</div>
<?php echo sortable_element('order', array(
'url' => 'item/sort',
'tag' => 'div',
)) ?>
すべてのAJAXアクションのために、バックグラウンド活動とリクエストの成功の視覚的なフィードバックを持つことはよいことです:
<div id="feedback"></div>
<div id="indicator" style="display:none;"><img src="/images/activity_indicator.gif" style="display:none;"/></div>
<?php echo sortable_element('order', array(
'url' => 'item/sort',
'update' => 'feedback',
'loading' => "Element.show('indicator')",
'complete' => "Element.hide('indicator')",
'success' => visual_effect('highlight', 'feedback'),
)) ?>
これらのパラメータについてもっと詳細な内容とここでは説明していない他のことについては、script.aculo.us Sortable manualを参照して下さい。
リストを並び替えるために両方とも2つのメソッドが両方とも効果的ですが、制限と欠点があります。
項目の大きな配列のために、おそらくはパジネートされたリストが必要です。古典的なメソッドはページごとのリストで立派に動作しますが、AJAXのものは適応が必要で、それら自身のページの外側から要素を再び並び替えることは不可能です。AJAXの並び替えインターフェイスに加えて'move item to position'機能を提供するのはそういうわけです。
AJAXアクションは古典的なアクションと同様に間違ったリクエストに対して防御されません。データベースの矛盾のリスクを避けるために、validateSort()メソッドをitemActionsクラスに追加します。このメソッドはすべての項目のidをチェックし、それらのみが一度だけ現れ、受け取られた配列に収まります。
AJAXソートで使用されるItemPeer::doSort()メソッドの欠点の一つはリストを並び替えるときに必要なクエリの数です。n個の項目においてそれぞれの動きは少なくともデータベースにn+2のクエリを作成します。AJAXリストは長いリストに採用されないので、主要な問題ではないのかもしれませんが、パフォーマンスが問題である場合、たとえば、UPDATE table SET CASE/WHEN SQL構文を使用するといった一つのクエリだけでランクを更新するようにこのメソッドをリファクタしなければなりません。
AJAXインターフェイスはたしかにとりわけ長いタスクの並び替えといったことによりユーザーフレンドリです。 2つの動作の間にサーバーのリフレッシュは義務ではないからです。しかし、要素をドラッグする能力はウェブインターフェイスでは新しく、その機能を使っていないユーザーは驚くかもしれません。さらに、AJAXインターフェイスを選択する場合、ドラッグ可能な要素のサイズについて考えなければならない場合(それらは掴むのに十分な大きさが必要です)、それらの外見、運動の自由度・・・古典的メソッドで解決する必要がない多くの人間とコンピューター間のインタラクション問題があります。
ターゲットとしているユーザーがブラウザでJavaScriptsをオフにしている場合はAJAXインタラクションは常に問題です。JavaScriptインターフェイスのデザインに加えて、機能が優雅に退化するように(degraces gracefully)代替的なものとして古典的なインターフェイスを提供します。
すべてにおいて、AJAXバージョンは本当にルックアンドフィールが優れますが、少なくとも開発は2倍長くなります。
Documentation
| [php] public func ... | |
| brtRiver(2009-02-09 13:48:24) | |
| All in all, the AJAX vers ... | |
| brtRiver(2009-01-05 04:34:04) | |
| AJAX interactions are alw ... | |
| brtRiver(2009-01-05 04:33:54) | |
| The AJAX interface is def ... | |
| brtRiver(2009-01-05 04:33:46) | |
| One drawback of the `Item ... | |
| brtRiver(2009-01-05 04:33:31) | |
| The AJAX action is not as ... | |
| brtRiver(2009-01-05 04:33:20) | |
| For large arrays of items ... | |
| brtRiver(2009-01-05 04:33:09) | |
| The two methods are both ... | |
| brtRiver(2009-01-05 04:33:01) | |
| Comparison ---------- ... | |
| brtRiver(2009-01-05 04:32:53) | |
| For more details about th ... | |
| brtRiver(2009-01-05 04:32:39) | |
| [php] <div id= ... | |
| brtRiver(2009-01-05 04:32:32) | |
| For all AJAX actions, it ... | |
| brtRiver(2009-01-05 04:32:18) | |
| [php] ... < ... | |
| brtRiver(2009-01-05 04:32:09) | |
| If the list to order is n ... | |
| brtRiver(2009-01-05 04:31:59) | |
| [php] <?php us ... | |
| brtRiver(2009-01-05 04:31:51) | |
| If the list elements are ... | |
| brtRiver(2009-01-05 04:31:38) | |
| [php] ... < ... | |
| brtRiver(2009-01-05 04:31:30) | |
| You can **add non-sortabl ... | |
| brtRiver(2009-01-05 04:31:19) | |
| [php] <?php us ... | |
| brtRiver(2009-01-05 04:30:35) | |
| You can define a **differ ... | |
| brtRiver(2009-01-05 04:30:24) |