Yii2 Materialized Path Behavior
Implementation of materialized path algorithm for storing the trees in DB tables., (*1)
, (*2)
Install
Install via Composer:, (*3)
composer require paulzi/yii2-materialized-path
or add, (*4)
"paulzi/yii2-materialized-path" : "^2.1"
to the require
section of your composer.json
file., (*5)
Migrations example
Single tree migration:, (*6)
class m150828_150000_single_tree extends Migration
{
public function up()
{
$tableOptions = null;
if ($this->db->driverName === 'mysql') {
// http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci
$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB';
}
$this->createTable('{{%single_tree}}', [
'id' => Schema::TYPE_PK,
'path' => Schema::TYPE_STRING . ' NULL',
'depth' => Schema::TYPE_INTEGER . ' NOT NULL',
'sort' => Schema::TYPE_INTEGER . ' NOT NULL',
'name' => Schema::TYPE_STRING . ' NOT NULL', // example field
], $tableOptions);
$this->createIndex('path', '{{%single_tree}}', ['path']);
}
}
Multiple tree migration:, (*7)
class m150828_150100_multiple_tree extends Migration
{
public function up()
{
$tableOptions = null;
if ($this->db->driverName === 'mysql') {
// http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci
$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB';
}
$this->createTable('{{%multiple_tree}}', [
'id' => Schema::TYPE_PK,
'tree' => Schema::TYPE_INTEGER . ' NULL',
'path' => Schema::TYPE_STRING . ' NULL',
'depth' => Schema::TYPE_INTEGER . ' NOT NULL',
'sort' => Schema::TYPE_INTEGER . ' NOT NULL',
'name' => Schema::TYPE_STRING . ' NOT NULL', // example field
], $tableOptions);
$this->createIndex('path', '{{%multiple_tree}}', ['tree', 'path']);
}
}
Configuring
use paulzi\materializedPath\MaterializedPathBehavior;
class Sample extends \yii\db\ActiveRecord
{
public function behaviors() {
return [
[
'class' => MaterializedPathBehavior::className(),
// 'treeAttribute' => 'tree',
],
];
}
public function transactions()
{
return [
self::SCENARIO_DEFAULT => self::OP_ALL,
];
}
}
Optional you can setup Query for finding roots:, (*8)
class Sample extends \yii\db\ActiveRecord
{
public static function find()
{
return new SampleQuery(get_called_class());
}
}
Query class:, (*9)
use paulzi\materializedPath\MaterializedPathQueryTrait;
class SampleQuery extends \yii\db\ActiveQuery
{
use MaterializedPathQueryTrait;
}
Sortable Behavior
This behavior attach SortableBehavior. You can use its methods (for example, reorder())., (*10)
Options
-
$pathAttribute = 'path'
- setup path attribute in table schema.
-
$depthAttribute = 'depth'
- setup depth attribute in table schema.
-
$itemAttribute = null
- setup item attribute in table schema for get path path, if the value is not set - using the primary key.
-
$treeAttribute = null
- setup tree attribute for multiple tree, when item attribute is not primary key.
-
$sortable = []
- SortableBehavior settings - see paulzi/yii2-sortable.
-
$delimiter = '/'
- delimiter of path items.
-
$rootDepthValue = 0
- setup value of $depthAttribute
for root nodes.
Usage
Selection
Getting the root nodes, (*11)
If you connect MaterializedPathQueryTrait
, you can get all the root nodes:, (*12)
$roots = Sample::find()->roots()->all();
Getting ancestors of a node, (*13)
To get ancestors of a node:, (*14)
$node11 = Sample::findOne(['name' => 'node 1.1']);
$parents = $node11->parents; // via relation
$parents = $node11->getParents()->all(); // via query
$parents = $node11->getParents(2)->all(); // get 2 levels of ancestors
To get parent of a node:, (*15)
$node11 = Sample::findOne(['name' => 'node 1.1']);
$parent = $node11->parent; // via relation
$parent = $node11->getParent()->one(); // via query
To get root of a node:, (*16)
$node11 = Sample::findOne(['name' => 'node 1.1']);
$root = $node11->root; // via relation
$root = $node11->getRoot()->one(); // via query
Getting descendants of a node, (*17)
To get all the descendants of a node:, (*18)
$node11 = Sample::findOne(['name' => 'node 1.1']);
$descendants = $node11->descendants; // via relation
$descendants = $node11->getDescendants()->all(); // via query
$descendants = $node11->getDescendants(2, true)->all(); // get 2 levels of descendants and self node
To populate children
relations for self and descendants of a node:, (*19)
$node11 = Sample::findOne(['name' => 'node 1.1']);
$tree = $node11->populateTree(); // populate all levels
$tree = $node11->populateTree(2); // populate 2 levels of descendants
To get the children of a node:, (*20)
$node11 = Sample::findOne(['name' => 'node 1.1']);
$children = $node11->children; // via relation
$children = $node11->getChildren()->all(); // via query
Getting the leaves nodes, (*21)
To get all the leaves of a node:, (*22)
$node11 = Sample::findOne(['name' => 'node 1.1']);
$leaves = $node11->leaves; // via relation
$leaves = $node11->getLeaves(2)->all(); // get 2 levels of leaves via query
Getting the neighbors nodes, (*23)
To get the next node:, (*24)
$node11 = Sample::findOne(['name' => 'node 1.1']);
$next = $node11->next; // via relation
$next = $node11->getNext()->one(); // via query
To get the previous node:, (*25)
$node11 = Sample::findOne(['name' => 'node 1.1']);
$prev = $node11->prev; // via relation
$prev = $node11->getPrev()->one(); // via query
Some checks
$node1 = Sample::findOne(['name' => 'node 1']);
$node11 = Sample::findOne(['name' => 'node 1.1']);
$node11->isRoot() - return true, if node is root
$node11->isLeaf() - return true, if node is leaf
$node11->isChildOf($node1) - return true, if node11 is child of $node1
Modifications
To make a root node:, (*26)
$node11 = new Sample();
$node11->name = 'node 1.1';
$node11->makeRoot()->save();
Note: if you allow multiple trees and attribute tree
is not set, it automatically takes the primary key value., (*27)
To prepend a node as the first child of another node:, (*28)
$node1 = Sample::findOne(['name' => 'node 1']);
$node11 = new Sample();
$node11->name = 'node 1.1';
$node11->prependTo($node1)->save(); // inserting new node
To append a node as the last child of another node:, (*29)
$node11 = Sample::findOne(['name' => 'node 1.1']);
$node12 = Sample::findOne(['name' => 'node 1.2']);
$node12->appendTo($node11)->save(); // move existing node
To insert a node before another node:, (*30)
$node13 = Sample::findOne(['name' => 'node 1.3']);
$node12 = new Sample();
$node12->name = 'node 1.2';
$node12->insertBefore($node13)->save(); // inserting new node
To insert a node after another node:, (*31)
$node13 = Sample::findOne(['name' => 'node 1.3']);
$node14 = Sample::findOne(['name' => 'node 1.4']);
$node14->insertAfter($node13)->save(); // move existing node
To delete a node with descendants:, (*32)
$node11 = Sample::findOne(['name' => 'node 1.1']);
$node11->delete(); // delete node, children come up to the parent
$node11->deleteWithChildren(); // delete node and all descendants
Reorder children:, (*33)
$model = Sample::findOne(1);
$model->reorderChildren(true); // reorder with center zero
$model = Sample::findOne(2);
$model->reorderChildren(false); // reorder from zero
Updating from 1.x to 2.x
- Move attributes
sortAttribute
, step
into sortable
attribute.
- Change namespace from
paulzi\materializedpath
to paulzi\materializedPath
.
- Include
paulzi\yii2-sortable
(composer update
).