I'm using Kohana 3.3 and trying to output the pages to the menu, and all sub-menus. Sub-menus are stored in the same table as pages, called page. The only difference between a page an a sub-menu is a flag. This is the table structure of page and the example pages I've created to get this working.
| id | title | layout | content | is_menu | parent | position |
| 1 | Home | home.php | Lipsum... | 0 | 0 | 1 |
| 3 | Menu1 | none | | 1 | 0 | 2 |
| 2 | Expl1 | view.php | Lipsum... | 0 | 0 | 3 |
| 5 | MPge2 | view.php | Lipsum... | 0 | 3 | 1 |
| 4 | MPge1 | view.php | Lipsum... | 0 | 3 | 2 |
I load all of the pages as ORM objects into an array, ordering by parent and then position. I've ordered the table above as the query fetches them. And this is my PHP method to generate the HTML menu that I then pass the array of ORM pages into:
class Controller_Page extends Controller_Table {
/**
* Ourput the menu for editing purposes. Include add new page buttons.
*
* @return body
*/
public function action_acp_menu()
{
$view = View::factory('acp/layouts/pages/menu')
->bind('menu_pages', $menu_pages);
$pages = ORM::factory('Page')
->order_by('parent')
->order_by('position')
->find_all();
$menu_pages = $this->menu($pages);
$this->response->body($view->render());
}
/**
* Output the pages in a menu format, with optional add more buttons if
* we're in the ACP. This returns a string of LIs without a wrapping UL.
*
* @param ORM $pages ORM object of pages.
* @param integer $parent Output all children of this parent.
* @return string
*/
private function menu($pages, $parent = 0)
{
$html = '';
$is_acp = (strpos(Request::initial()->uri(), 'acp') !== FALSE);
echo "<br>Testing pages against parent: $parent<br>";
foreach ($pages as $page)
{
echo "Page: ".$page->id."; Parent: ".$page->parent."<br>";
if ($page->parent == $parent)
{
echo "Page belongs to the parent.<br>";
if ($page->is_menu)
{
echo "Page ".$page->id." is a menu. Loading children.<br>";
$children = $this->menu($pages, $page->id);
if ($children OR $is_acp)
{
echo "Children found (or we're in the ACP). Adding pages to the menu.<br>";
$html .= '<li class="dropdown" data-id="'.$page->id.'">'.($is_acp ? '
<span class="handle">::</span>' : '').'
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
'.$page->title.'
<b class="caret"></b>
</a>
<ul class="dropdown-menu'.($is_acp ? ' sortable-child' : '').'" data-id="'.$page->id.'">
'.$children.'
</ul>
</li>';
}
}
else
{
echo "Page ".$page->id." is not a menu. Outputting normal.<br>";
$html .= '<li data-id="'.$page->id.'">'.($is_acp ? '
<span class="handle">::</span>' : '').'
<a href="/acp/pages/edit'.$page->id.'">'.$page->title.'</a>
</li>';
}
}
echo "Finished processing page ".$page->id."<br>";
}
echo "Finished looping all pages<br>";
if ($is_acp)
{
$html .= '<li class="add-page">
<a href="/acp/pages/create?parent='.$page->id.'">+ Add page</a>
</li>';
}
echo "Finished testing against parent: $parent<br>";
return $html;
}
As you can see I've put a bunch of echo's in to try and figure out what's going on. This is the output from all the echo's:
Testing pages against parent: 0
Page: 1; Parent: 0
Page belongs to the parent.
Page 1 is not a menu. Outputting normal.
Finished processing page 1
Page: 3; Parent: 0
Page belongs to the parent.
Page 3 is a menu. Loading children.
Testing pages against parent: 3
Page: 1; Parent: 0
Finished processing page 1
Page: 3; Parent: 0
Finished processing page 3
Page: 2; Parent: 0
Finished processing page 2
Page: 5; Parent: 3
Page belongs to the parent.
Page 5 is not a menu. Outputting normal.
Finished processing page 5
Page: 4; Parent: 3
Page belongs to the parent.
Page 4 is not a menu. Outputting normal.
Finished processing page 4
Finished looping all pages
Finished testing against parent: 3
Children found (or we're in the ACP). Adding pages to the menu.
Finished processing page 3
Finished looping all pages
Finished testing against parent: 0
So after the second call to the menu method for the sub-menu (ID 3), the foreach loop completes ID 3 but doesn't continue on to the next page. I don't understand why? I've done foreach loops like this in the past that have worked, this is my first time doing it in Kohana though so think it's something Kohana related that I don't understand.
I've created an eval.in of this, showing the code works and that it must be something in Kohana doing this. This is the result of all the echo's from the eval.in:
Testing pages against parent: 0
Page: 1; Parent: 0
Page belongs to the parent.
Page 1 is not a menu. Outputting normal.
Finished processing page 1
Page: 3; Parent: 0
Page belongs to the parent.
Page 3 is a menu. Loading children.
Testing pages against parent: 3
Page: 1; Parent: 0
Finished processing page 1
Page: 3; Parent: 0
Finished processing page 3
Page: 2; Parent: 0
Finished processing page 2
Page: 5; Parent: 3
Page belongs to the parent.
Page 5 is not a menu. Outputting normal.
Finished processing page 5
Page: 4; Parent: 3
Page belongs to the parent.
Page 4 is not a menu. Outputting normal.
Finished processing page 4
Finished looping all pages
Finished testing against parent: 3
Children found (or we're in the ACP). Adding pages to the menu.<br>
Finished processing page 3
Page: 2; Parent: 0
Page belongs to the parent.
Page 2 is not a menu. Outputting normal.
Finished processing page 2
Page: 5; Parent: 3
Finished processing page 5
Page: 4; Parent: 3
Finished processing page 4
Finished looping all pages
Finished testing against parent: 0
As you can see unlike in Kohana, after page 3 finishes processing it continues on to page 2 as expected. Why doesn't Kohana do this?
I've found the problem and a solution. Kohana handles ORM objects by reference. I don't know why that stops my foreach loop completing, but it does.
The solution was to loop through the ORM objects once, storing the details of each page in an array of stdClass' instead. That way they weren't a reference, and so when I passed the new array in to my method it worked.
If anyone else can post a more complete answer explaining why an array of referenced objects has this issue I'd appreciate it.