Sorting Silverstripe SiteTree by depth - php

I want to get all of the SiteTree pages of a Silverstripe website and then sort them by depth descending. By depth I mean the number of parents they have.
This is already done to some extent by the Google Sitemaps Module. Except it doesn't go past a depth of 10 and doesn't calculate for pages hidden from search: https://github.com/silverstripe-labs/silverstripe-googlesitemaps
Looking at the Google Sitemaps Module module it appears easy enough to count the number of parents of a page: ( /code/GoogleSitemapDecorator.php - line 78)
$parentStack = $this->owner->parentStack();
$numParents = is_array($parentStack) ? count($parentStack) - 1: 0;
But what is the best way to sort the SiteTree using this calculation?
I'm hoping there's an easier way than getting all of the SiteTree, appending the depth, and then resorting.

The function below will get all published pages from the database, checks they are viewable and not an error page, finds out how many ancestors they have and then returns a DataObjectSet of the pages ordered by depth (number of ancestors).
public function CustomPages() {
$filter = '';
$pages = Versioned::get_by_stage('SiteTree', 'Live', $filter);
$custom_pages = new DataObjectSet();
if($pages) {
foreach($pages as $page) {
if( $page->canView() && !($page instanceof ErrorPage) ) {
$parentStack = $page->parentStack();
$numParents = is_array($parentStack) ? count($parentStack) - 1 : 0;
$page->Depth = $numParents;
$custom_pages->push($page);
}
}
}
$custom_pages->sort('Depth', 'DESC');
return $custom_pages;
}

If you're using PostgreSQL you can also use a single database query. In our effort to speed up SilverStripe (2.4), we've replaced the getSiteTreeFor() with a single database query, see:
http://fvue.nl/wiki/SilverStripe:_Replace_getSiteTreeFor_with_single_query
The breadcrumb field contains an array of ID's on which you can sort.

Related

Create a multidimensional array by processing the same function over and over

I am trying to create a multidimensional array of my navigation. All my pages are created in a CMS and then stored in my DB. Each page will have the usual fields like title, url etc but also they will have a field which tells me if it has any children 'p_has_children' as well as a field which will tell me its parent page 'p_parent' (if it is a child of a page).
I thought I could write a function which I could call and send it an array of all the top level navigation items. Then as I loop through each item I would check if it had any children, get the children (and assign them to an array) and then send them through the same function like so....
function sideNavArray()
{
//Get the side navigation
$getSidenav = $this->crud_model->fetch_rows_where('pages', array('p_side_nav' => 'y', 'p_active' => 'y'));
//Call the navbuilder function
$sidenav = $this->sidenavBuilder($getSidenav, 'navitem', 'nav');
//return the generated nav
return $sidenav;
}
function sidenavBuilder($navItems, $itemClass, $navLevel, $i = 0, $sidenav = array())
{
//Loop over each nav item
foreach($navItems as $navItem){
//For each item I want to add th nav_title, slug, url and page type
$sidenav[$i]['p_nav_title'] = $navItem->p_nav_title;
$sidenav[$i]['p_slug'] = $navItem->p_slug;
$sidenav[$i]['p_url'] = $navItem->p_title;
$sidenav[$i]['p_type'] = $navItem->p_type;
//See if this page has any children
if($navItem->p_has_children == 'y'){
//If the page has children then I want to fetch them from the DB
$subnav = $this->crud_model->fetch_rows_where('pages', array('p_parent' => $navItem->id, 'p_active' => 'y', 'p_protected !=' => 'y'));
if(!empty($subnav)){
//Change the item class and level to subnavitem and subnav
$itemClass = 'sub' . $itemClass;
$navLevel = 'sub' . $navLevel;
//Assign the children to the same array as its parent in the "subnav" level and send it through the sitenavBuilder function
$sidenav[$i][$navLevel] = $this->sitenavBuilder($subnav, $itemClass, $navLevel);
}
}
$i++;
//End foreach loop
}
return $sidenav;
}
I am sure some of you are looking at this right now and saying well of course that won't work!
I currently have a navigation being generated but it is on the front end and every time I want to add an additional level I have to go into the code and add it manually. I want to avoid having to write each level of the navigation as I want it to be able to grow to as many levels as it can without me having to go in and code that extra level if it is needed every time.
I hope this all makes sense. Basically I want to know if there is a way to get this nav array to be built by running through the same loop over and over until it doesn't need to anymore.

PHP & Mysql tree navigation menu with unlimited sub categories

Im looking for a way to extract the data from the mysql database to create an unlimited subcategry menu.
The menu has about 1500 categories in it and they are stored into the database table (menu) in the following format :
category_id , category title , parent_id
i want to do 1 mysql query to put all the data into an array.
$menu_list =array();
$query = mysql_query("SELECT * FROM menu ORDER BY category_id ASC");
if(mysql_num_rows($query) != 0) {
while($row = mysql_fetch_array($query)){
$category_id = $row['category_id'];
$title = $row['category_title'];
$parent_id = $row[parent_id'];
$menu_list[] = array($category_id,$title,$parent_id);
}
}
That is the code that i am currently using to turn the data into the array.
I then need to loop through the array to build a menu up.
I will then hide all subcategories and expand with jquery ( I can do this bit no problem )
The problem i have is going through the array and displaying the data in the right order.
The menu has unlimited subcategories so it will probably need to be a recursive function to retrieve the data.
I have folllowed alot of the examples on here but then i get lost how to display the data..
i now need to end up with :
main item
sub cat
sub cat
sub cat
sub cat
main item
sub cat
main item
main item
sub cat
sub cat
sub cat
sub cat
sub cat
etc...
with each category in its own div
I will then need to be able to edit each category (ie title name and position if need be)
then if i change where a sub category is based a quick page refresh will reload the menu in the new order..
eventually i would like to make this into a drag and drop but that will be at a later date.
I hope this has explained it enough..
Thanks in advance..
So the problem has come back to haunt me again...
My method of ajax calling another mysql select was ok for the CMS section as it was only used from time to time.
But now we have got to the front end.
Each time the page is viewed the first 3 levels of the menu are pulled using numerous mysql requests.
As the categories are getting bigger this is now resulting in 1600+ categories being pulled numerous times each, and even at 1000 visits a day will result in more than a 1000000 sql requests a day.
So i think i will definately need to pull the categories into an array, and then recursively go through the array.
I have looked at the solutions above and they seem to work on paper but not in practise.
If anyone can attempt to give me another solution it would be appreciated..
just to recap in my db i have : id , category_name , parent_id
I am now using PDO with mysql and prepared statements for added security..
Thanks in advance..
Here is the code what you need
category_id AS id , category title AS kategori and parent_id AS kid
function countsubcat($pid)
{
$r=mysql_query("select count(kid) AS say from meskat where kid='$pid' limit 1");
$rw=mysql_fetch_array($r);
return $rw['say'];
}
function listmenu($pid = 0)
{
$res = mysql_query("select id,kategori,kid from meskat where kid='$pid'");
while($cat=mysql_fetch_array($res))
{
echo '<li>';
print''.$cat['kategori'].'';
if(countsubcat($cat['id'])>0)
{
print'<ul>';
listmenu($cat['id']);
print'</ul>';
}
echo '</li>';
}
}
echo '<ul>';
listmenu(0); //starting from base category
echo '</ul>';`
In MySQL, this will require many queries, one for each level plus one at a minimum.
you grab all the top-level categories, use them as root nodes for a forest (a group of trees).
You grab all of the categories that are children of any of them.
Add each of these categories as children to the parent node
If there are still children, go to step two, otherwise stop.
What you end up with a tree that you do a depth-first walk through to convert to some (presumably) html representation (e.g. nested lists).
There are some optimisations you can perform. If you sort the child levels by parent ID, then you can assume continuous blocks of children, meaning you don't have to do as many lookups to find the correct parent, just check for a parent id change.
If you maintain a list of the current lowest level of categories, indexed by ID, you can speed up forest creation.
If you overlap steps 2 and 4, then you only do n+1 queries (where n is the number of levels), so checking for more children is also grabbing the next level of children.
In terms of memory, this will use the memory required for the tree, plus lowest level lookup table and a current parent id.
The building algorithm it fairly fast, scaling linearly with the number of categories.
This method also successfully avoids corrupted sections of data, because it won't grab data that have cycles (no root node).
I also suggest using a prepared statement for the children query, since MySQL will compile and cache the query, it will speed up the operation by only sending new data across the wire.
If you don't understand some of the concepts, I suggest hitting up Wikipedia as I have tried to use standard terms for these.
Can you alter the table structure? In that case you could take a look at the Nested Set Model (link includes description and implementation details, scroll down to The Nested Set Model). Node insertion and removal becomes more complex, but allows you to retrieve the whole tree in just one query.
If you can assume that for any category category_id > parent_id is true, the following recursive functions to represent the menu as a multi-level array and render it as a nested HTML list would work:
$menu = array();
$query = mysql_query("SELECT category_id, category_title, parent_id FROM menu ORDER BY category_id ASC");
if(mysql_num_rows($query) != 0) {
while($row = mysql_fetch_assoc($query)) {
if(is_null($row['parent_id']))
$menu['children'][] = $row;
else
add_to_menu(&$menu,$row);
}
}
function add_to_menu($menu,$item) {
if(isset($menu['children'])) {
foreach($menu['children'] as &$child) {
if($item['parent_id'] == $child['category_id']) {
$child['children'][] = $item;
} else {
add_to_menu(&$child,$item);
}
}
}
}
function render_menu($menu) {
if(isset($menu['children'])) {
echo '<ul>';
foreach($menu['children'] as &$child) {
echo "<li>";
echo $child['category_id']." : ".$child['category_title'];
if(isset($child['children'])) {
render_menu(&$child);
}
echo "</li>";
}
echo '</ul>';
}
}
render_menu($menu);
So after various attempts at different solutions i actually have used jquery and a ajax load to retreive the next level.
With mine main categories all have a parent of 1 - shop i do a mysql scan for all categories with 1 as the parent id.
This will then display the main categories. Attached to each main category i have added a folder icon if they have any sub categories.
I have also inserted a blank div with an id of that category.
When the folder is clicked it does an ajax call to a php script to find all categories with that parent. These are appended to the parents_id div.
This inturn adds the category (with a css padding to indicate a sub category) and again a folder sign if it has sub categories as well as another blank div with the category_id.
You can continue this for as many categories and sub categories as the client requires.
I took it a stage further and added an edit icon to category/subcategory so categories can be moved or changed.
If you would like some sample code for this let me know...
Actually, you need very little code to build a tree.
You also only need one query.
Here is what I came up with (I use Drupal's database but it is explicit enough to understand):
$items = db_select('menu_items', 'mi')
->fields('mi')
->orderBy('position')
->execute()
->fetchAllAssoc('id', PDO::FETCH_ASSOC);
// Example of result. The ID as key is important for this to work.
$items = array(
3 => array('id' => 3, 'parent' => NULL, 'title' => 'Root', 'position' => 0),
4 => array('id' => 4, 'parent' => 3, 'title' => 'Sub', 'position' => 0),
5 => array('id' => 5, 'parent' => 4, 'title' => 'Sub sub', 'position' => 0),
6 => array('id' => 6, 'parent' => 4, 'title' => 'Sub sub', 'position' => 1),
);
// Create the nested structure. Note the & in front of $item.
foreach($items as &$item)
if($item['parent'])
$items[$item['parent']]['sub items'][$item['mid']] =& $item;
// Now remove the children from the root
foreach($items as $id => $item)
if($item['parent']) // This is a child
unset($items[$id])
At this point, all you need is a recursive function to display the menu:
function print_menu($items) {
echo '<ul>';
foreach($items as $item) {
echo '<li>';
echo '' . $item['title'] . '';
if(!empty($item['sub items']))
print_menu($item['sub items']);
echo '</li>';
}
echo '</ul>';
}

Need advice in algorithm implementation and creation in PHP based shop

For several days now I am trying to cope with the algorithm implementation at the online shop which I am writing in PHP. I do not know whether the problem is only the implementation, or perhaps bad algorithm design. Hovewer, for me it seems fine. I only haven`t checked its complexity of it, but it's such a problem.
After a long deliberation on the same algorithm, without thinking on implementation I came up with the use of binary search tree (bst) with additional data inserted into list consist of user defined info (later about it). The whole orders list would be displayed, or returned using inorder method.
I write it like that:
If the input object date is greater than current object go right
If the input object date is less than current object go left
If the dates are the same stay at place
If the field is blank check if the product is in stock
If it is put into place and finish
If there is not do nothing and exit
If the field is full
{Check if on the list is this user id
If yes than check order priority
If no do nothing and exit
Check if there is product on stock
If yes replace record and exit
If no do nothing and exit
}
{If there is not user id on the list check if product is on stock
If yes then put element on the end
If no do nothing and exit
}
Maybe it looks a little bad, but I was not able to do indentation.
Data is transferred into algorithm in a loop until the end of orders list. The list is unordered.
This is my implementation:
class BinaryTree {
private $predescor = array(
'd'=>array('data'=>0),
'p'=>null,
'r'=>null,
'l'=>null
);
private $ancestor = array(
'd'=>array('data'=>0),
'p'=>null,
'r'=>null,
'l'=>null
);
private $i = 0;
public function insertIntoTree(&$root,$element)
{
$this->predescor = $root;
$this->predescor;
while($this->predescor)
{
if($element['d']['data']==$this->predescor['d']['data'])
{
$this->inertIntoList($element,$this->predescor['d']);
return true;
}
$this->predescor = $this->predescor;
if($element['d']['data']<$this->predescor['d']['data'])
{
$this->predescor = $this->predescor['l'];
}
else
{
$this->predescor = $this->predescor['r'];
}
}
$element['p'] = $this->predescor;
if(!$this->predescor)
{
$root = $element;
}
else if($element['d']['data']<$this->predescor['d']['data'])
{
$this->predescor['l'] = $element;
}
else
{
$this->predescor['r'] = $element;
}
return true;
}
public function putList(&$list,$root)
{
if($root!=null)
{
$this->putList($list, $root['l']);
$lista[$this->i] = $root;
$this->i++;
$this->putList($list, $root['r']);
}
return;
}
private function insertIntoList($element,&$position)
{
if($position == null)
{
$position = $element;
return true;
}
foreach($position['user'] as &$key)
{
if($key == $element['d']['user'])
{
if($key['priority']<$element['d']['user']['priority'])
{
return false;
}
else if($key['priority']==$element['d']['user']['priority'])
{
return false;
}
else
{
if(Orders::checkOrder($element['d']['user']['order']))
{
$key['order'] = $element['d']['user']['order'];
return true;
}
else
{
return false;
}
}
}
}
//#todo add at the end
return true;
}
}
I would like to advise whether there is a simpler way than using bst consisting of a quite complex arrays, which would also be easier to implement? Because now I can not inplement it in PHP.
Thank you in advance.
I wouldn't start by coding this in php at all.
I'd start by building this into the database. ("Orders" implies a database.) I'd start by clarifying a couple of points. Assuming that one order can have many line items . . .
The number of days since the last order seems to clearly apply to the order,
not to individual products.
The user can have "only one request carried at a time". Request for
what? Doesn't seem to make sense for this to apply either to an
order or to an order's line item.
The order priority seems to clearly apply to the order, not to line
items. But a line-item priority might make more sense. (What products does the customer need first?)
Whether the product is in stock seems to apply to the line items, not
to the order as a whole.
I'd start by creating two views. (Not because you'll eventually need two views, but because some things are still unclear.)
One view, which has to do with "ranking" as applied to an order, would calculate or display three things.
Number of days since the last order.
Is this order the "one request carried at a time"?
The order priority.
If the numbers assigned to these three things are consistent in scale, you can just sort on those three columns. But that's not likely. You'll probably need to weight each factor, possibly by multiplying by a "weighting" factor. A calculation on the result should let you put these in a useful order. It's not yet clear whether the calculation is best done in the view or in a stored procedure.
The other view would have to do with whether a line item is in stock. It's not clear whether one line item out of stock means the whole order is incomplete, or whether one line item out of stock changes the calculation of a weighted number that scales along with the others above. (You can make a good argument for each of those approaches.)

Create a Dynamic Nav on the Fly

I'm looking for the best way to create a complex navigation element on the fly. I have all of the elements in a database (title, id, parentId) and I want to efficiently take them out of the DB and display them correctly. I also want to collapse all of the navigation elements that aren't active. So if I was browsing through "Sofas" I wouldn't see "Chandeliers" or any of the categories under lighting but I would see "Lighting".
This is what I want the final product to look like:
Furniture
Living Room
Sofas
Chairs
Ottomans
Bedroom
Beds
Nightstands
Lighting
Chandeliers
Floor Lamps
Sconces
Rugs & Textiles
Contemporary
Vintage
My current method is
write one SQL query that pulls down all of the category names, ids, and parent ids
Iterate through the categories and put into a sorted multi-dimensional array with child categories stored under their parents.
Iterate through the new array and add another entry to mark the appropriate categories as open (all categories are closed by default)
iterate through the array and write HTML
I'm trying to to this with as few interations as possible and I'm sure the code I have right now is inefficient. Especially step 2 I iterate through the array several times. There has to be a general solution to this (common?) problem.
Consider adding a new field to your database table: level.
Main-categories will have level 0.
Sub-categories will have level 1.
Sub-sub-categories will have level 2.
etc.
This trick will help you to know which sub-categories to disable without 2nd iteration of the array.
I believe this is the perfect place to generate your html code using recursion.
I used this function a while ago. It is working with a multi-dimensional array (tree)
function buildMenu($menu_array, $is_sub=FALSE) {
$attr = (!$is_sub) ? 'id="menu"' : 'class="submenu"';
$menu = "<ul $attr>\n";
foreach($menu_array as $id => $elements) {
foreach($elements as $key => $val) {
if(is_array($val)) {
$sub = buildMenu($val, TRUE);
}
else {
$sub = NULL;
$$key = $val;
}
}
if(!isset($url)) {
$url = $id;
}
$menu .= "<li>$display$sub</li>\n";
unset($url, $display, $sub);
}
return $menu . "</ul>\n";
}
echo buildMenu($menu_array);
This adds css properties too. If you wish to mark the currently active page you can use the strpos() function to find your current url. If you need some more functionality you can easily add them to buildMenu()
Using level as mentioned in the answer above will help too. If you were using the nested set model in your database I could also help you with my query which is a single select returning the whole menu data.

Recursing Properly Through Related Entities

I have a set of Organizations and their Board Members.
All organizations have board members and many board members are on the board of more than one organization.
I am using JIT Hypertree to illustrate their relationships. The JIT Hypertree schema requires that one item be the parent of all and is drawn based on a single JSON array.
I would love to have the re-centering event query and re-populate the graph based on the change. Then 2 levels would be fine but I have not been able to work out how to do that.
The code I have at present recurses manually for three levels from the starting organization but what I want is to re-curse through all related records.
So it would start with an Org and add Org's array of children (board members). Then fetch all of the boards (other than current Org) for each board member and add those as children of the board member.
This would continue until each trail dead ends - presumably at a board member who only belongs to one board.
Anyone have advice on how to create this array and avoid duplicates?
$board = $center->board();
$top['id'] = $center->ID;
$top['name'] = $center->Org;
$top['children'] = array();
if ($board) {
foreach ($board as $b) {
$child['id'] = $b->ID;
$child['name'] = (strlen(trim($b->Last)) > 0) ? $b->First . ' ' . $b->Last : 'Unknown';
$child['data']['orgname'] = $center->Org;
$child['data']['relation'] = $b->Role;
$child['data']['occupation'] = $b->Occupation;
$child['children'] = array();
$childboards = $b->boards();
if ($childboards) { foreach ($childboards as $cb) {
$gchild['id'] = $cb->ID;
$gchild['name'] = $cb->Org;
$gchild['data']['orgname'] = (strlen(trim($b->Last)) > 0) ? $b->First . ' ' . $b->Last : 'Unknown';
$gchild['children'] = array();
$childboardmembers = $cb->board();
if ($childboardmembers) { foreach ($childboardmembers as $cbm) {
$ggchild['id'] = $cbm->ID;
$ggchild['name'] = (strlen(trim($cbm->Last)) > 0) ? $cbm->First . ' ' . $cbm->Last : 'Unknown';
$ggchild['data']['orgname'] = $cb->Org;
$ggchild['data']['relation'] = $cbm->Role;
$ggchild['data']['occupation'] = $cbm->Occupation;
$ggchild['children'] = array();
$gchild['children'][]= $ggchild;
}}
$child['children'][]= $gchild;
}}
$top['children'][] = $child;
}
}
$top['data'] = array();
$top['data']['description'] = $center->Desc;
echo json_encode($top);
// Edit 2011.10.24 In Re hakre response
My data structure is a table of Organizations with unique IDs, a table of People with Unique IDs, and then a bridging table for the two specifying Organization (Entity) and Person and the Role the Person is playing in the Entity. A typical many-to-many. No sub-boards at all. I made an image of it which now seems kind of pointless but I'll add it at the bottom.
The JIT library data structure is a little nuts (to me) in that it goes like this in their band example:
Top: Nine Inch Nails
Child: Jerome Dillon
Child: Howlin Maggie (another band)
{all the bands' members and then all of their bands...}
So the organization (band) is treated as though it is a Person even though it is comprised of a number of Persons. And when I recurse using the code above I get (I think) terrible bloat but the JSON it makes works correctly despite bloat.
Example JSON and Example Visualization
// End Edit
Your question is hard to answer in the sense that your data-structure is mainly unknown.
For the graphical represenation you only need to provide simple relationships if I understand that correctly:
*- Parent
+- Child
+- Child
...
`- Child
Your data structure has a different format, I don't know specifically but it's something like:
Org <-- 1:n --> Board
Board <-- n:n --> Board # If you have sub-boards
Board <-- n:n --> Member
Whichever your data is represented, to map or transpose your data onto the required structure for the graphical representation, you need some functions that take care of that.
To do that you need to share classification/type between both and specific keys, so that you can look-up the needed data from the event to return the data. For example:
if (request_parent_is_org())
{
$id = request_parent_id();
$parent = data_get_board($id);
$parent->addChildrent(data_get_board_children($id));
}
else
{
... # All the other decisions you need to take based on request type
}
view_response_to_json($parent);
What you have with your many-to-many data model is a graph. JIT is designed for trees.
To put it another way, JIT will not correctly show the crossing lines that are represented in the data whenever a single person is connected to multiple organizations.
I'd recommend a proper network graph visualization - D3.js has a great implementation for modern browsers.
The JSON data format it uses is actually easier to implement given your table structure - for all the organizations and people, you define objects:
{
"name": "Mme.Hucheloup",
"group": 1
},
{
"name": "Afton School Board",
"group": 2
}
And for each association in your association table you define connection objects that wire them together:
{
"source": 1,
"target": 2
},
The fancy coding in D3 takes care of the rest. Good luck!
You can use function below:
function get_orgs_and_childs ($child_id, $found = array())
{
array_push ($found, $child['name']);
if($child['children'])){
$found[] = get_parents($child['id'], $found);
}
return $found;
}
Call it using:
$orgs = get_orgs_and_childs($child['id']);

Categories