Giter VIP home page Giter VIP logo

doctrine2-nestedset's Introduction

Doctrine2 NestedSet

This Doctrine2 extension implements the nested set model (modified pre-order tree traversal algorithm) for Doctrine2. This allows storing hierarchical data, a collection of data where each item has a single parent and zero or more children, in the flat tables of a relational database. For more information on the nested set model, see:

https://en.wikipedia.org/wiki/Nested_set_model

Introduction

Nested Set is a solution for storing hierarchical data that provides very fast read access. However, updating nested set trees is more costly. Therefore this solution is best suited for hierarchies that are much more frequently read than written to. And because of the nature of the web, this is the case for most web applications.

Setting Up

To set up your model as a Nested Set, your entity classes must implement the DoctrineExtensions\NestedSet\Node interface. Each entity class must contain mapped fields for holding the Nested Set left and right values.

Here's an example using annotation mapping:

namespace Entity;

use DoctrineExtensions\NestedSet\Node;

/**
 * @Entity
 */
class Category implements Node
{
    /**
     * @Id @Column(type="integer")
     * @GeneratedValue
     */
    private $id;

    /**
     * @Column(type="integer")
     */
    private $lft;

    /**
     * @Column(type="integer")
     */
    private $rgt;

    /**
     * @Column(type="string", length="16")
     */
    private $name;


    public function getId() { return $this->id; }

    public function getLeftValue() { return $this->lft; }
    public function setLeftValue($lft) { $this->lft = $lft; }

    public function getRightValue() { return $this->rgt; }
    public function setRightValue($rgt) { $this->rgt = $rgt; }

    public function getName() { return $this->name; }
    public function setName($name) { $this->name = $name; }

    public function __toString() { return $this->name; }
}

Generally you do not need to, and should not, interact with the left and right fields. These are used internally to manage the tree structure.

Multiple Trees

The nested set implementation can be configured to allow your table to have multiple root nodes, and therefore multiple trees within the same table. This is done by implementing the DoctrineExtensions\NestedSet\MultipleRootNode interface (instead of DoctrineExtensions\NestedSet\Node) and mapping a root field.

Extending our annotation example:

/**
 * @Column(type="integer")
 */
private $root;

public function getRootValue() { return $this->root; }
public function setRootValue($root) { $this->root = $root; }

Like the left and right fields, you generally do not need to interact with the root value.

Working with Trees

After you successfully set up your model as a nested set you can start working with it. Working with Doctrine2's nested set implementation is all about two classes: Manager and NodeWrapper. NodeWrapper wraps your entity classes giving you access to the underlying tree structure. Manager provides methods for creating new trees and fetching existing trees.

To fetch an entire tree from the database:

$config = new Config($em, 'Entity\Category');
$nsm = new Manager($config);
$rootNode = $nsm->fetchTree(1);

In this example, $rootNode is an instance of NodeWrapper wrapping your model's root node. To get access to your model object:

$modelObject = $rootNode->getNode();

Creating a Root Node

$config = new Config($em, 'Entity\Category');
$nsm = new Manager($config);

$category = new Category();
$category->setName('Root Category 1');

$rootNode = $nsm->createRoot($category);

Inserting a Node

$child1 = new Category();
$child1->setName('Child Category 1');

$child2 = new Category();
$child2->setName('Child Category 2');

$rootNode->addChild($child1);
$rootNode->addChild($child2);

Deleting a Node

You must always delete a node using the NodeWrapper::delete() method instead of EntityManager's delete method. NodeWrapper::delete() takes care of updating the tree when deleting nodes:

$category = $em->getRepository('Entity\Category')->findOneByName('Child Category 1');
$node = $nsm->wrapNode($category);
$node->delete();

Deleting a node will also delete all descendants of that node. So make sure you move them elsewhere before you delete the node if you don't want to delete them.

Moving a Node

Moving a node is simple. NodeWrapper offers several methods for moving nodes around between trees:

  • moveAsLastChildOf($other)
  • moveAsFirstChildOf($other)
  • moveAsPrevSiblingOf($other)
  • moveAsNextSiblingOf($other)

Examining a Node

You can examine the nodes and what type of node they are by using some of the following functions:

$isLeaf = $node->isLeaf();
$isRoot = $node->isRoot();

Examining and Retrieving Siblings

You can easily check if a node has any next or previous siblings by using the following methods:

$hasNextSib = $node->hasNextSibling();
$hasPrevSib = $node->hasPrevSibling();

You can also retrieve the next or previous siblings if they exist with the following methods:

$nextSib = $node->getNextSibling();
$prevSib = $node->getPrevSibling();

If you want to retrieve an array of all the siblings you can simply use the getSiblings() method:

$siblings = $node->getSiblings();

Examining and Retrieving Descendants

You can check if a node has a parent or children by using the following methods:

$hasChildren = $node->hasChildren();
$hasParent = $node->hasParent();

You can retrieve a nodes first and last child by using the following methods:

$firstChild = $node->getFirstChild();
$lastChild = $node->getLastChild();

Or if you want to retrieve the parent of a node:

$parent = $node->getParent();

You can get the children of a node by using the following method:

$children = $node->getChildren();

The getChildren() method returns only the direct descendants. If you want all descendants, use the getDescendants() method.

You can get the descendants or ancestors of a node by using the following methods:

$descendants = $node->getDescendants();
$ancestors = $node->getAncestors();

Sometimes you may just want to get the number of children or descendants. You can use the following methods to accomplish this:

$numChildren = $node->getNumberChildren();
$numDescendants = $node->getNumberDescendants();

The getDescendants() method accepts a parameter that you can use to specify the depth of the resulting branch. For example getDescendants(1) retrieves only the direct descendants (the descendants that are 1 level below, that's the same as getChildren()).

Rendering a Simple Tree

$tree = $nsm->fetchTreeAsArray(1);

foreach ($tree as $node) {
    echo str_repeat('&nbsp;&nbsp;', $node->getLevel()) . $node . "<br>";
}

Advanced Usage

The previous sections have explained the basic usage of Doctrine's nested set implementation. This section will go one step further.

Fetching a Tree with Relations

If you're a demanding software developer this question may already have come into your mind: "How do I fetch a tree/branch with related data?". Simple example: You want to display a tree of categories, but you also want to display some related data of each category, let's say some details of the hottest product in that category. Fetching the tree as seen in the previous sections and simply accessing the relations while iterating over the tree is possible but produces a lot of unnecessary database queries. Luckily, Manager and some flexibility in the nested set implementation have come to your rescue. The nested set implementation uses QueryBuilder objects for all it's database work. By giving you access to the base query builder of the nested set implementation you can unleash the full power of QueryBuilder while using your nested set.

$qb = $em->createQueryBuilder();
$qb->select('c.name, p.name, m.name')
    ->from('Category', 'c')
    ->leftJoin('c.HottestProduct', 'p')
    ->leftJoin('p.Manufacturer', 'm');

Now we need to set the above query as the base query for the tree:

$nsm->getConfiguration()->setBaseQueryBuilder($qb);
$tree = $nsm->fetchTree(1);

There it is, the tree with all the related data you need, all in one query.

If you don't set your own base query then one will be automatically created for you internally.

When you are done it is a good idea to reset the base query back to normal:

$nsm->getConfiguration()->resetBaseQueryBuilder();

Transactions

When modifying a tree using methods from NodeWrapper, each method is executed immediately. This differs from working with normal Doctrine2 entities where changes are queued via the EntityManager and not executed until flush is called.

If you are making multiple changes, it is recommended to wrap these changes in a transaction:

$em->getConnection()->beginTransaction();
try {

    $root = $nsm->createRoot(new Category('Root'));
    $root->addChild(new Category('Child 1'));
    $root->addChild(new Category('Child 2'));

    $em->getConnection()->commit();
} catch (Exception $e) {
    $em->close();
    $em->getConnection()->rollback();
    throw $e;
}

Customizing left, right and root fields

NestedSet requires you include left, right and root fields in your entity class. By default, NestedSet expects these fields to be named lft, rgt and root respectively. You can customize the names of these fields using via the manager configuration:

$config = new Config($em, 'Entity\Category');
$config->setLeftFieldName('nsLeft');
$config->setRightFieldName('nsRight');
$config->setRootFieldName('nsRoot');
$nsm = new Manager($config);

Conclusion

NestedSet makes managing hierarchical data in Doctrine2 quick and easy.

doctrine2-nestedset's People

Contributors

blt04 avatar calumbrodie avatar fogs avatar jdodds avatar jsmitka avatar justinpfister avatar livsi avatar mdelanno avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

doctrine2-nestedset's Issues

Three level deep example?

Currently I'm trying to generate a three level deep nested set for a menu structure.
I would like to get the following structure:

  • Root
    • Home
    • Checklist
      • List
      • New
      • Statistics
    • User
      • Login
      • Register

I've written the following code

$perm_menu_default = $this->em->find('Entity\Permission', 1);
if($perm_menu_default === null)
{
    $perm_menu_default = new Permission();
    $perm_menu_default->setName('menu-list-default');
    $perm_menu_default->setDescription('Can view default menu.');
    $this->em->persist($perm_menu_default);
    $this->em->flush();
}

$config = new Config($this->em, 'Entity\Menu');
$nsm = new Manager($config);

$menu_root = new Menu();
$menu_root->setName('Default Menu');
$menu_root->setPermission($perm_menu_default);

$menu_home = new Menu();
$menu_home->setName('Home');
$menu_home->setPermission($perm_menu_default);

$menu_cl = new Menu();
$menu_cl->setName('Checklist');
$menu_cl->setPermission($perm_menu_default);

$menu_cl_list = new Menu();
$menu_cl_list->setName('List');
$menu_cl_list->setPermission($perm_menu_default);

$menu_cl_new = new Menu();
$menu_cl_new->setName('New');
$menu_cl_new->setPermission($perm_menu_default);

$menu_cl_stats = new Menu();
$menu_cl_stats->setName('Statistics');
$menu_cl_stats->setPermission($perm_menu_default);

$menu_user = new Menu();
$menu_user->setName('User');
$menu_user->setPermission($perm_menu_default);

$menu_user_login = new Menu();
$menu_user_login->setName('Login');
$menu_user_login->setPermission($perm_menu_default);

$menu_user_register = new Menu();
$menu_user_register->setName('Register');
$menu_user_register->setPermission($perm_menu_default);

$menu_user_forgot = new Menu();
$menu_user_forgot->setName('Forgot Password');
$menu_user_forgot->setPermission($perm_menu_default);

$node_root = $nsm->createRoot($menu_root);

$node_cl = $nsm->wrapNode($menu_cl);
$node_root->addChild($menu_cl);
$node_cl->addChild($menu_cl_list);
$node_cl->addChild($menu_cl_new);
$node_cl->addChild($menu_cl_stats);

$node_user = $nsm->wrapNode($menu_user);
$node_root->addChild($menu_user);
$node_user->addChild($menu_user_login);
$node_user->addChild($menu_user_register);
$node_user->addChild($menu_user_forgot);

$node_root->addChild($menu_home);

var_dump($node_root);

This results in the following (wrong) mysql entries

'id,'permission_id','lft','rgt','name','root','url'
'9', '1', '1', '8', 'Default Menu', '9', '/'
'10', '1', '8', '21', 'Checklist', '9', '/'
'11', '1', '15', '16', 'List', '9', '/'
'12', '1', '17', '18', 'New', '9', '/'
'13', '1', '19', '20', 'Statistics', '9', '/'
'14', '1', '8', '15', 'User', '9', '/'
'15', '1', '9', '10', 'Login', '9', '/'
'16', '1', '11', '12', 'Register', '9', '/'
'17', '1', '13', '14', 'Forgot Password', '9', '/'
'18', '1', '6', '7', 'Home', '9', '/'

As you can see, the root does not embrace all the sub entries. What am I doing wrong?

Produce a version set up as a Symfony2 bundle!

It would be super cool if I could just add a git repository to my .deps, load this library as a bundle and start using nestedset with a minimal amount of setup effort in Symfony2 projects.

Nesting level too deep โ€“ recursive dependency

hi,

Thanks for your job and sorry for my bad english.
A fatal error is thrown when you try to move a child node before or after one of its siblings because they both have the same parent.

Note that It only occurs with php > 5.1 ( i think php 5.2 has changed the way of comparing 2 objects )
You can fix it by replacing :

if( $node == $this )

with :

if( $node === $this )

in the NodeWrapper class.

Fetching a tree is pretty slow

On my development system (Core2 Duo 2.6Ghz) it takes almost 2 seconds to retrieve a tree with 160 nodes (ArrayCache used for development). Do you have an idea what the problem could be? If not, I will see if I find out something later today.

I remember that I had 0.2-0.3 seconds execution time in Doctrine1 nested set with 1k nodes - hopefully we can achieve that performance also!

When trying to delete multiple nodes, every other node does not get deleted

Say you fetch five entities from the DB, and want to delete each one. Iterate over the collection, wrapping each entity as a node, and then call $node->delete(). Only some of the nodes will be deleted, and the pattern seems to be that every other entity is not deleted. So, if you tried deleting the five entities, the second and fourth entities would not be deleted.

Looking at NodeWrapper::delete(), if you output the result of $qb->getQuery()->execute() (line 1182) you'll get back 0 for every other query.

PDOException on serialize/cache a NodeWrapper

I got the exception: PDOException: You cannot serialize or unserialize PDO instances when I tried to save a \DoctrineExtensions\NestedSet\NodeWrapper object in Doctrine Cache. To fix this bug I made the following changes in NodeWrapper class. Please, consider add this changes in next release.

    /**
     * To avoid PDO exception, the property 'manager' cannot be serialized
     * If you need the manager after unserialize the object, you MUST call the attachManager() method in the current wrapper
     * @return array
     */
    function __sleep() {
        return [
            'node',
            'parent',
            'ancestors',
            'descendants',
            'children',
            'level',
            'outlineNumbers',
        ];
    }

    /**
     * Attach a {@see \DoctrineExtensions\NestedSet\Manager} to the current wrapper
     * This method is useful after wakeup a serialized object
     * @param Manager $manager
     */
    public function attachManager(Manager $manager) {
        if (empty($this->manager)) {
            $this->manager = $manager;
        }
    }

Bad handling of unmanaged nodes in wrapper

If we create a node entity and wrap it in wrapper to then retrieve it's parent we get an error:

Undefined offset: 0
/DoctrineExtensions/NestedSet/NodeWrapper.php:253

We probably could just return null if node cannot be discovered.

Single Table Inheritance and JSON

Hi,
I created an abstract node class (Application_Model_Node) and 3 classes inherit from it.
This works quite well, but when I used JSON, then the method addChild doesn't update all right values.
I tried to debug and found that it has something to do with my Single Table Inheritance.
E.g. if I exchange in the method shiftRLRange of NodeWrapper the line
->update(get_class($this->getNode()), 'n')
by
->update('Application_Model_Node' , 'n')
then it works again.
Is this a bug on NestedSet or is NestedSet not supposed to work with the combination SingleTableInheritance/JSON or can I do something different?
Thanks,
Ben

Another moving issue

When moving nodes the LFT and RGT values get "holes" which breaks functionality like "->hasChildren"

I have a tree like this:
1 folder 8
2 folder_2 5
3 folder_3 4
6 folder_4 7

Now I do a folder_3->insertAsLastChildOf( folder_4 ) which leads to the following tree:

1 folder 8
2 folder_2 5
6 folder_4 9
7 folder_3 8

But now folder_2 incorrectly states that is has children, as RGT - LFT > 1, but actually it has no children...

Any suggestions how to fix this?

Moving methods are broken

All of the methods for moving nodes can break the nested set. For instance let's have structure like this:

LFT name RGT

1 root 6
2 section 5
3 subsection 4

If I call method moveAsLastChildOf (section -> subsection) it will break the hierarchy instead of throw an exception. (issued by mlueft). For this problem I have found a simple fix:

public function moveAsLastChildOf(NodeWrapper $node)
{
    // Added ancestor check
    if($this->isAncestorOf($node) || $this == $node)
    {
        throw new \InvalidArgumentException('Cannot move node as a child of itself');
    }
    ...

Same for moveAsFirstChildOf().

The siblings methods however are more complicated especially moveAsPrevSiblingOf(subsection->section) which should be possible to do.

Any solutions?

Intro Docs Suggestion, mention $nsm->reset().

Just a suggestion, you might want to include a section on bulk inserts and managing imports of large heirarchies with the Nested Set manager extension.

I just struggled with speed and page loads until I eventually found the $nsm->reset() method.

Eventually got a nice memory graph like http://chrisacky.com/images/phpGC.jpg which would import 4000 items in 50 seconds.

When I was stuck on something like this http://chrisacky.com/images/memoryLeakDoctrine.jpg which would take around 8 minutes for 1000 items importing.

Adding $nsm->reset() in addition to $em->clear() helped alleave this.

Node classes with inheritance not handled well

I have the following metadata for the class that implements the Node interface:

<entity name="OrganisationEntity" table="org_entity" inheritance-type="SINGLE_TABLE">
<discriminator-column name="type" column="type" type="string" />
    <discriminator-map>
      <discriminator-mapping value="company" class="Company" />
      <discriminator-mapping value="branch" class="Branch" />
      <discriminator-mapping value="store" class="Store" />
    </discriminator-map>

So the class OrganisationEntity implements the Node interface and the classes Company, Branch and Store are direct subclasses. When an entity is retrieved from the DB it is an instance of the appropriate class. Now when a branch has several stores as descendents and I want to delete the branch, it will NOT delete the stores. The reason is that delete() (and several other calls) pass the class: get_class($this->getNode()) to the querybuilder. Therefore, deleting a Branch will not delete the stores, because these are different classes.

I have made the same changes that were done to shiftRLRange in commit c84f0d9 (Little fix to make it works with Inheritance) to several other calls (delete, moveBetweenTrees, etc):

  1. Added line:

    $metadata = $em->getClassMetadata(get_class($this->getNode()));
    
  2. Replaced:

    get_class($this->getNode())
    

    with
    $metadata->rootEntityName
    So far it seems to work for delete. Haven't tested the other methods.

I can't create root node

Hey, I want to build a simple web file manager. Right now I got a code looking like this:

    $config = new \DoctrineExtensions\NestedSet\Config($entityManager, 'src\File');
    $nsm = new \DoctrineExtensions\NestedSet\Manager($config);

    $file = new \src\File();
    $file->setName('Plik');

    var_dump($nsm->createRoot($file));

But it doesn't work. var_dump() in last line returns me nothing, even "null"

Bulk inserting

Does anyone have any tips for inserting nodes in bulk? I have 1,000's to insert and my function starts of fast but gradually grinds to a halt.

logic allows circular references

Hi!

Thanks for this great tool. I am using it in my project and it saved me some time. While building and testing my frontend for editing I have seen the logic allows to make a node a child of itself or of one of it's children.

A fix is simple:

$newParent->getLeftValue() > $movedChild->getLeftValue()
&& $newParent->getRightValue() < $movedChild->getRightValue()

If this contition is true don't move the node.

regards,

michael

Fetching branches fails for single root implementation

First of all I would like to thank you for providing this useful extension! I discovered an issue refering to the single root implementation. If you try to fetch a branch by providing a pk and a level, the query which is built automatically by the extension also tries to add the root id (which does not exist) to the where clause of the query so that the query fails (semantic exception).

For example on line 195 in Manager.php:

 if($this->getConfiguration()->isRootFieldSupported())
 {
      $qb->andWhere("$alias.$rootField = :rootid")
           ->setParameter('rootid', $node->getRootValue());
 }

This happens because the method isRootFieldSupported only checks whether a label for the root field exists in the Config.php class and not whether a corresponding attribute actually exists in the node. You can work around this issues by overwriting the root field name each time you initialize a new NSM:

 $config = new Config($em, 'MyNodeClass');
 $config->setRootFieldName(null);
 $nsm = new Manager($config);

But I would suggest to use two interfaces for nodes instead. One interface 'INode' for nodes which have just one single root node (getId, get/setLeftValue, get/setRightValue). And in addition, a second interface 'IMultipleRootNode' extending the first one by adding a get/setRootValue method.

The advantages of this solution: no need to add the root value methods to nodes which do not need them (keep code clean) and you can simply check in isRootFieldSupported if the node implements the interface 'IMultipleRootNode' or not.

Comments are welcome!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.