Использование одной модели и нескольких таблиц в Yii2 при построение дерева категорий по шаблону ClosureTable

Являюсь новичком в программировании. Чуть меньше года назад начал использовать первую версию фреймворка Yii для построения панели управления сайтом. Привлекла хорошая документация на русском языке и множество примеров кода через поисковики. С выходом Yii2, стал переписывать панель управления на нём, учитывая просчёты в проектировании прошлой панели. Для использования категорий (новостей, галереи, комментариев) решил использовать построение дерева категорий по шаблону проектирования ClosureTable. (Подробнее об этом шаблоне и сравнение с другими можете прочитать в публикации «Хранение деревьев в базе данных. Часть первая, теоретическая»).

Кратко об исходных данных при построении модуля новостей.

таблицы (приведу ключевые поля для данного шаблона проектирования):
news — таблица новостей. (поля: news_id — id-шник новости, category_id — id-шник категории новости и др.)
news_category — таблица категорий. (поля category_id — id-шник категории и др.)
news_category_tree — таблица дерева категорий. (поля ancestor — предок (или родитель), descendant — потомок (или дочерний), length — длина связи, других полей здесь нет)

При генерации кода Gii создал модуль со следующими моделями (namespace app\modules\news\models):
News — модель новостей
NewSearch — модель поиска новостей (в yii2 для поиска используется модель наследуемая от основной, т.е. NewsSearch extends News)
NewsCategory — модель категорий
NewsCategorySearch — модель поиска категорий
NewsCategoryTree — модель дерева.

При написании кода, использование шаблона ClosureTable действительно позволило легко управлять категориями, имею в виду создание новых, перемещение по дереву существующих и удаление вместе с дочерними категориями.

Приведу примеры кода модели NewsCategoryTree:

Для создания:

$connection = \Yii::$app->db;
				
$command = $connection->createCommand('
				INSERT INTO '.self::tableName().' (ancestor, descendant, length)
    		SELECT ancestor, :insert_category_id, length+1 FROM '.self::tableName().'
    		WHERE descendant = :ancestor_category
    		UNION ALL
    		SELECT :insert_category_id, :insert_category_id, 0												
		');
$command->bindValues([':insert_category_id'=>$insert_category_id,':ancestor_category'=>$ancestor_category]);
			
try {
	$command->execute();
} catch (Exception $e) {				
	throw new Exception(Yii::t('admin','Failed to update the category tree [{message}]',['message'=>$e->getMessage()]));
}


Для редактирования:

$connection = \Yii::$app->db;
				 
$command_del = $connection->createCommand('
						DELETE ct1 FROM '.self::tableName().' ct1 
						INNER JOIN '.self::tableName().' ct2 ON ct1.descendant=ct2.descendant
						LEFT JOIN '.self::tableName().' ct3 ON ct3.ancestor=ct2.ancestor
						AND ct3.descendant=ct1.ancestor
						WHERE ct2.ancestor=:update_category_id AND ct3.ancestor IS NULL
						');
$command_del->bindValue(':update_category_id', $update_category_id);
				
$command_ins = $connection->createCommand('
						INSERT INTO '.self::tableName().' (ancestor, descendant, length)
						SELECT ct1.ancestor, ct2.descendant, ct1.length+ct2.length+1
						FROM '.self::tableName().' ct1 JOIN '.self::tableName().' ct2
						WHERE ct2.ancestor=:update_category_id AND ct1.descendant=:new_ancestor
						');
$command_ins->bindValues([':update_category_id'=>$update_category_id,':new_ancestor'=>$new_ancestor]);
				
$transaction = $connection->beginTransaction();
try {
      $command_del->execute();
      $command_ins->execute();
      $transaction->commit();
} catch (Exception $e) {					
      $transaction->rollback();
      throw new Exception(Yii::t('admin','Failed to update the category tree [{message}]',['message'=>$e->getMessage()]));
}


Для удаления (этот код не обязателен, поскольку при удалении категории, удаляются связанные записи дерева категорий):

$connection = \Yii::$app->db;
			
$command = $connection->createCommand('
					DELETE ct1 FROM '.self::tableName().' ct1
					INNER JOIN '.self::tableName().' ct2 ON ct1.descendant=ct2.descendant
					WHERE ct2.ancestor= :delete_category_id
					');
$command->bindValue(':delete_category_id', $delete_category_id);
try {
       $command->execute();
} catch (Exception $e) {				
	throw new Exception(Yii::t('admin','Failed to update the category tree [{message}]',['message'=>$e->getMessage()]));
}


Возникшая проблема1:
Однако, при таком варианте для других модулей, которые будут использовать такой же принцип построения дерева по шаблону ClosureTable, необходимо будет создавать для каждого свою модель управления деревом (CatalogCategoryTree, CommentsTree и т.д.). Эти модели в узком смысле буду различаться только заданием используемой таблицы в базе данных.

Решение1:
Для того, чтобы не плодить кучу моделей с одинаковым кодом, весь код был перенесён в одну модель ClosureTree (namespace common\models). Common для того, чтобы её можно было использовать для нескольких приложений по типу advanced.

Возникшая проблема2:
Тут то и выяснилось, что изменять имя таблицы «на лету» не получается из-за того, что init() модели ClosureTree не позволяет передавать в него дополнительные атрибуты из модели NewsCategory (по сути менять таблицу одной модели из другой модели):

ClosureTree:

public function init($table_name,$category_class_name)
{
	parent::init();
        $this->_tableName=$table_name;
	$this->_categoryClassName=$category_class_name;
}


NewsCategory:
$classTree=new ClosureTree('{{%news_category_tree}}',self::className());


выводя ошибку «Declaration of common\models\ClosureTree::init() should be compatible with yii\db\BaseActiveRecord::init()». Которая говорит, что init() не должен принимать параметры, поскольку у родительского класса init() не принимает параметры.
И мало того, при использование статического метода запроса данных (ClosureTree::find()) не возможно было бы задать название таблиц.

Решение2:
Для обхода этого недоразумения, оказалось достаточно использовать статическое задание названия таблицы в модели ClosureTree и функции устанавливающей это статическое значение (дополнительно устанавливается класс, который отвечает за таблицу категорий NewsCategory для обеспечения связей):
ClosureTree:

public static $_tableName;   // название таблиц (news_category_tree, calalog_category_tree, comments_tree и т.д.)
public static $_categoryClassName; // класс категорий, который использует модель дерева (NewsCategory, CatalogCategory и т.д.)
	
/* Задаём таблицу с деревом и класс, который отвечает за эту таблицу */
public function setTableAndClass($table_name,$category_class_name)
{			
	self::$_tableName=$table_name;
	self::$_categoryClassName=$category_class_name;
}
		
public static function tableName()
{
       return self::$_tableName;
}


Для задания имени таблицы и класса категорий, используем функцию init() класса категорий:
NewsCategory:

public function init()
{			
	parent::init();
	/* настраиваем модель ClosureTree для работы с $this классом категорий и с таблицей news_category_tree */
	$classTree=new ClosureTree();
	$classTree->setTableAndClass('{{%news_category_tree}}',self::className());			
}


Отсюда имеем, что при вызове класса категорий (NewsCategory), автоматически настраиваем под него модель ClosureTree.
Теперь можно использовать ClosureTree::find(), он уже знает какую таблицу ему надо использовать.

Точно так же можно задать параметры используемой модели через контроллер. Например, при использовании разных таблиц для разных языков сайта.

Надеюсь, моя статья будет полезна новичкам.
Теги:
Yii2, связанные модели, Closure Table

Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.

Похожие публикации