среда, 29 октября 2008 г.

Symfony custom widget to display nested tree in listbox

Для формирования древовидной структуры достаточно ввести название для ветки и указать родительскую ветку.


Требования:
- Symfony 1.1.x
- Propel 1.3

/lib/myWidgetFormPropelSelectTree.class.php

<?php

/*
* widget displays nested tree in listbox
* based on symfony 1.1 and Propel 1.3
* 28-okt-2008 by Vit <228vit@gmail.com>
*/
class myWidgetFormPropelSelectTree extends sfWidgetFormSelect
{
/**
* @see sfWidget
*/
public function __construct($options = array(), $attributes = array())
{
$options['choices'] = new sfCallable(array($this, 'getChoices'));

parent::__construct($options, $attributes);
}

/**
* Constructor.
*
* Available options:
*
* * model: The model class (required)
* * add_empty: Whether to add a first empty value or not (false by default)
* If the option is not a Boolean, the value will be used as the text value
* * method: The method to use to display object values (__toString by default)
* * order_by: An array composed of two fields:
* * The column to order by the results (must be in the PhpName format)
* * asc or desc
* * criteria: A criteria to use when retrieving objects
* * connection: The Propel connection to use (null by default)
* * multiple: true if the select tag must allow multiple selections
*
* @see sfWidgetFormSelect
*/
protected function configure($options = array(), $attributes = array())
{
$this->addRequiredOption('model');
$this->addOption('add_empty', false);
$this->addOption('method', '__toString');
$this->addOption('order_by', null);//no needed, tree must be sorted
$this->addOption('criteria', null);
$this->addOption('connection', null);
$this->addOption('multiple', false);

parent::configure($options, $attributes);
}

/**
* Returns the choices associated to the model.
*
* @return array An array of choices
*/
public function getChoices()
{
$choices = array();
if (false !== $this->getOption('add_empty'))
{
$choices[''] = true === $this->getOption('add_empty') ? '' : $this->getOption('add_empty');
}

$class = $this->getOption('model').'Peer';
$criteria = is_null($this->getOption('criteria')) ? new Criteria() : clone $this->getOption('criteria');

$method = $this->getOption('method');

if (!method_exists($this->getOption('model'), $method))
{
throw new RuntimeException(sprintf('Class "%s" must implement a "%s" method to be rendered in a "%s" widget', $this->getOption('model'), $method, __CLASS__));
}
//get all SCOPEs from a tree
$cr = new Criteria;
$method2 = 'addAscendingOrderByColumn';
$cr->addAscendingOrderByColumn(
call_user_func(array($class, 'translateFieldName'),
'Scope',
BasePeer::TYPE_PHPNAME, BasePeer::TYPE_COLNAME)
);
$cr->addGroupByColumn(
call_user_func(array($class, 'translateFieldName'),
'Scope',
BasePeer::TYPE_PHPNAME, BasePeer::TYPE_COLNAME)
);
$scopes = call_user_func(array($class, 'doSelect'), $cr, $this->getOption('connection'));
$scopesCnt = count($scopes)-1;
//loop over scopes
foreach ($scopes as $currScope => $scope){
//retrieve tree for current scope
//TreePeer::retrieveTree($scope->getScope());
$tree = call_user_func(array($class, 'retrieveTree'), $scope->getScope(), $this->getOption('connection'));
//start iterator
$it = new myTreeListboxOptions($tree);
//loop over childs
foreach ($it as $m){
//store attributes in arrays, to build row for listbox
$siblings[$m->getLevel()] = intval($m->hasNextSibling());
$childs[$m->getLevel()] = intval($m->hasChildren());
$currLevel = $m->getLevel();
$connectors = "";//extra string which contains connectors:


В форме описываем как:

$this->setWidgets(array(
'id' => new sfWidgetFormInputHidden(),
'name' => new sfWidgetFormInput(),
...
'parent' => new myWidgetFormPropelSelectTree(
array(
'model' => 'Menu',
'add_empty' => 'root')),
));

$this->widgetSchema->setNameFormat('%s');
$this->validatorSchema->setOption('allow_extra_fields', true);
$this->validatorSchema->setOption('filter_extra_fields', false);


В пример для обработки в действии:

   if ($this->form->isValid()){
$tree = new Menu();
$tree->setName($request->getParameter('name'));
$parent = $request->getParameter('parent');

//get max scope
$nextScope = 0;
$c = new Criteria;
$c->clearSelectColumns()->addSelectColumn('MAX(' . MenuPeer::SCOPE . ')');
$stmt = MenuPeer::doSelectStmt($c);
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
$nextScope = $row[0] + 1;
}
$nextScope = (is_null($nextScope) ? 1 : $nextScope);

if (empty($parent)){
$tree->makeRoot();
$tree->setScopeIdValue($nextScope);
} else {
$root = MenuPeer::retrieveByPK($parent);
$tree->insertAsLastChildOf($root);
}
$tree->save();
}//if valid

3 комментария:

Yuriy Voziy комментирует...

Очень интересно сколько у вас заняло времени, чтобы написать подобное

Vit228 комментирует...

Затрудняюсь сказать, делал урывками в основном дома.

К тому же он еще не закончен, его еще надо в админку научиться встраивать, что бы отмечался выбранный родитель.

Yuriy Voziy комментирует...

Ну выбор родителя это мелочь. Просто подобное дерево, правда для Symfony 1.0 и не запаковано в Виджет у меня заняло практически полный рабочий день, а мой начальник сказал, что это 10 минут работы.