Laravel 4 - Eloquent. Infinite children into usable array? - php

I've got a categories table. Each category can have an optional parent (Defaults to 0 if no parent).
What I want to do is build a simple html list tree with the levels of the categories.
Example date:
Foods
-- Fruit
---- Apple
---- Banana
---- Orange
-- Veg
---- Cucumber
---- Lettuce
Drinks
-- Alcoholic
---- Beer
---- Vodka
Misc
-- Household Objects
---- Kitchen
------ Electrical
-------- Cooking
---------- Stove
---------- Toaster
---------- Microwave
Note that this needs to work for around 10 'levels'. I'd love it to be infinite but I really dont want to be going down the route of using a nested set model as it'll cause huge delays on this project.
The docs on this for laravel are terrible, with no real reference as to where to even start. I've been playing with it for days trying to work out what to do and seem to be getting nowhere without a huge messy block of for each loops within each other 10 times.
I've got my tree of data using the following in my model:
<?php
class Item extends Eloquent {
public function parent()
{
return $this->hasOne('Item', 'id', 'parent_id');
}
public function children()
{
return $this->hasMany('Item', 'parent_id', 'id');
}
public function tree()
{
return static::with(implode('.', array_fill(0,10, 'children')))->where('parent_id', '=', '0')->get();
}
}
This gets all the parent and children up to a level of 10. This works fine, but you cant really then do anything with the child data without manually having 10 foreach loops within each other.
What am I doing wrong here? Surely this shouldn't be this hard/poorly executed? All I want do do is get a simple html list with the items in a tree structure.
I've put together a quick SQLFiddle example of the dummy data used above: http://sqlfiddle.com/#!2/e6d18/1

This was much more fun than my usual morning crossword puzzle. :)
Here is an ItemsHelper class that will do what you are looking for, and better yet will recurse as far down as you want.
app/models/ItemsHelper.php:
<?php
class ItemsHelper {
private $items;
public function __construct($items) {
$this->items = $items;
}
public function htmlList() {
return $this->htmlFromArray($this->itemArray());
}
private function itemArray() {
$result = array();
foreach($this->items as $item) {
if ($item->parent_id == 0) {
$result[$item->name] = $this->itemWithChildren($item);
}
}
return $result;
}
private function childrenOf($item) {
$result = array();
foreach($this->items as $i) {
if ($i->parent_id == $item->id) {
$result[] = $i;
}
}
return $result;
}
private function itemWithChildren($item) {
$result = array();
$children = $this->childrenOf($item);
foreach ($children as $child) {
$result[$child->name] = $this->itemWithChildren($child);
}
return $result;
}
private function htmlFromArray($array) {
$html = '';
foreach($array as $k=>$v) {
$html .= "<ul>";
$html .= "<li>".$k."</li>";
if(count($v) > 0) {
$html .= $this->htmlFromArray($v);
}
$html .= "</ul>";
}
return $html;
}
}
I just used a new installation of Laravel 4 and the basic hello.php view.
Here is my route in app/routes.php:
Route::get('/', function()
{
$items = Item::all();
$itemsHelper = new ItemsHelper($items);
return View::make('hello',compact('items','itemsHelper'));
});
Although my view doesn't use the items variable, I'm passing it here because you probably will want to do something else with them too.
And finally, my app/views/hello.php just has one line:
<?= $itemsHelper->htmlList(); ?>
The output looks like this:
FoodsFruitAppleBananaOrangeVegCucumberLettuceDrinksAlcoholicBeerVodkaMiscHousehold ObjectsKitchenElectricalCookingStoveToasterMicrowave
Note: your SQL Fiddle had 5 ("Orange") as the parent_id for Cucumber and Lettuce, I had to change it to 6 ("Veg").

I've expanded on the accepted answer by Mark Smith to allow generated lists to reference addition data that is passed into the class.
The class works in pretty much the same way, but I've packaged it up so hopefully it can be used easily.
Just reference the helper class in your controller:
use App\Helpers\CategoryHierarchy;
You can then either instantiate the class manually, or using Laravel 5's method injection things get even better:
$products = $product->getAllProducts();
$hierarchy->setupItems($products);
return $hierarchy->render();
This can output the following:
<ul class='simple-list'>
<li><input type="checkbox" name="hierarchy-checkboxes[]" value="1" >Home delivery</li>
<ul>
<li><input type="checkbox" name="hierarchy-checkboxes[]" value="2" >Italian</li>
<ul>
<li><input type="checkbox" name="hierarchy-checkboxes[]" value="3" >Pizza</li>
<li><input type="checkbox" name="hierarchy-checkboxes[]" value="4" >Pasta</li>
</ul>
<li><input type="checkbox" name="hierarchy-checkboxes[]" value="5" >Burgers</li>
</ul>
</ul>
A repo is available: https://github.com/amochohan/CategoryHierarchy which explains in some more detail.

I use this functions to make it work.
//Returns Root elements
public function scopeRoot($query) {
$all = $query->whereParent(0)->get();
$collection = $all->filter(function($single) {
if ($single->ModelFilter('GET')) {
return $single;
}
});
return $collection;
}
//Recursive call
public function traverse() {
self::_traverse($this->Children, $array, $this);
return $array;
}
//This function build a multidimensional array based on a collection of elements
private static function _traverse($collection, &$array, $object) {
$new_array = array();
foreach ($collection as $element) {
self::_traverse($element->Children, $new_array, $element);
}
$array[] = $object;
if (count($new_array) > 0) {
$array[] = $new_array;
}
}
First I get a collection of the root elements those are the ones that I pass to my views where I want to list the tree...
Then I do ...
<ul class="bg-info cat-list">
#foreach($categories as $category)
<?php
$array = $category->traverse();
list_view($array);
?>
#endforeach
</ul>
Using this function...
//Prints a multidimensional array as a nested HTML list
function list_view($element, $ul = true) {
foreach ($element as $value) {
if (!is_array($value)) {
echo "<li>";
echo $value->name;
} else {
echo ($ul) ? "<ul>" : "<ol>";
list_view($valuce, $ul);
echo "</li>";
echo ($ul) ? "</ul>" : "</ol>";
}
}
}
Hope it helps

Related

Find level or depth of child in child parent (Adjacency List Model) relation using PHP

There is a relation of employee and manager in mysql table which is stored as Adjacency List Model Employee has only one manager and manager have many employees if value of manager is empty that means employee without manager
relation
employee (1 to 1) manager
employee (many to 1) manager
employee manager
10 11
15 10
9 15
6 0
I want to depth of manager like for
11 depth is 3
for 10 depth is 2
for 15 depth is 1
for 6 depth is 0
......
......
.......
How can i achieve this using php below is my incomplete logic.
<?php
get_level(11) // 3
get_level(10) // 2
get_level(15) // 1
get_level(6) // 0
function get_level($level){
$this->db->get_where('manager_user', array('manager_no' => $level))
->result_array()
// logic
return no; //3 for 11
}
?>
Can some one help me in this. If any one provide me the solution with mysql function this will be also helpful for me data stored in mysql multilevel hierarchy.
Edit : I edit my question as suggested by #sintakonte.
Step taken by me to solve the issue - first i changed my table structure from The Adjacency List Model
to The Nested Set Model
after that this class helped me to achieve the desired result
Simple. Just use a switch statement:
function get_level($level){
$query = $this->db->get_where('manager_user', array('manager_no' => $level));
if ($query->num_rows !== 1) {
return false;
}
$manager_no = intval($query->row()->manager_no);
switch ($manager_no) {
default:
return false;
case 11:
return 3;
case 10:
return 2;
case 15
return 1;
case 6:
return 0;
}
}
Returns false if case doesn't exist or if query returns no rows. I'm also assuming that manager_no is unique.
Also think for 6 depth is 0 you said should be reversed but you can figure that part out.
Here is the solution for with dynamic data
function get_level($level){
$query = $this->db->query('SELECT count(*) as depth FROM manager_user WHERE manager_no > (SELECT manager_no FROM roles WHERE manager_no ='.$level.')');
$depth = $query->row_array()['depth'];
}
this is a pretty tough task i think - because one problem is the lack of information and the other is to build the tree
Anyway i tried something which should work (please study the code carefully) - i'm not sure if there are better concepts out there but i think this is a good one
Create a model called Manageruser_model.php like the following
class Manageruser_model extends CI_Model
{
private $arrFieldsWithKeys = [];
private function createTree()
{
$arrResult = $this->db->get_where('manager_user')->result();
//in case a manager is no employee - we've to add this one as an employee with manager id 0
$arrResult = $this->addManagersAsEmployee($arrResult);
$arrFieldsWithKeys = [];
foreach($arrResult AS $obj)
{
$arrFieldsWithKeys[$obj->employee] = $obj;
if (!isset($arrFieldsWithKeys[$obj->manager]) && $obj->manager != 0) $arrFieldsWithKeys[$obj->manager] = $obj;
}
$arrFoundChilds = [];
foreach($arrResult AS $obj)
{
if (isset($arrFieldsWithKeys[$obj->manager]))
{
if (!isset($arrFieldsWithKeys[$obj->manager]->childs)) $arrFieldsWithKeys[$obj->manager]->childs = [];
$arrFieldsWithKeys[$obj->manager]->childs[] = $obj;
$arrFoundChilds[] = $obj->employee;
}
}
$this->arrFieldsWithKeys = $arrFieldsWithKeys;
$arrRemovedChildsFromMasterNode =array_diff_key($arrFieldsWithKeys,array_flip($arrFoundChilds));
$this->setTreeDepth($arrRemovedChildsFromMasterNode);
}
private function addManagersAsEmployee($arrResult)
{
$employees = array_column($arrResult, 'employee');
$manager = array_column($arrResult, 'manager');
$arrMissingManagersAsEmployee = array_diff($manager, $employees);
foreach($arrMissingManagersAsEmployee AS $strId)
{
if ($strId > 0)
{
$obj = new stdClass();
$obj->employee = $strId;
$obj->manager = 0;
$arrResult[] = $obj;
}
}
return $arrResult;
}
private function setTreeDepth($arr)
{
$level = 0;
foreach($arr AS $obj)
{
if (isset($obj->childs))
{
$level = $this->setTreeDepth($obj->childs);
$obj->level = $level;
}
else
{
$obj->level = 0;
}
}
return $level + 1;
}
public function getDepth(int $id)
{
if (empty($this->arrFieldsWithKeys)) $this->createTree();
if (!isset($this->arrFieldsWithKeys[$id])) throw new Exception($id.' doesn\'t exist.');
return $this->arrFieldsWithKeys[$id]->level;
}
}
and now in your controller you can simply load the model and get a depth in return like
$this->load->model('Manageruser_model');
echo $this->Manageruser_model->getDepth(11);

Tree iteration without recursion

So, I was trying to print my class:
class category {
public $name;
public $id;
public $subCats = array();
public function __construct($name = "", $id = "") {
$this->name = $name;
$this->id = $id;
}
public function add_sub_cat($subCat) {
array_push($this->subCats, $subCat);
}
}
In two ways recursive and iterative, first one I did without any problems:
function recursive_print($category, $path) {
?>
<li><div id="category-name" ><p><? echo $category->name ?></p>
</div></li>
<?php foreach($category->subCats as $subCat) { ?>
<ul>
<?php recursive_print($subCat, $path.=$subCat->id) ?>
</ul>
<?php }
}
But now I got stuck on second part of this task.
Do I have to modify my class?
Is it even possible to print without recursion?
I have read this but it did not cleared anything.
Maybe someone have better tutorial or any advice?
Walking a tree without recursion is always an interesting problem.
The basic idea is you need to keep track of a stack by yourself. (Function calls are implemented by pushing temporary variables and a return address to a stack before the call, and popping that return address afterwards, so when you make a recursive function, you're avoiding having to do this yourself.)
Here's a non-recursive implementation:
function tree_print($root_category) {
$stack = array(array($root_category));
echo '<ul>'."\n";
while (count($stack)) {
$category = array_shift($stack[count($stack)-1]);
echo '<li><div id="category-name"><p>' . $category->name . '</p></div></li>'."\n";
echo '<ul>'."\n";
$stack[] = $category->subCats;
while (count($stack) && count($stack[count($stack)-1]) == 0) {
echo '</ul>'."\n";
array_pop($stack);
}
}
}
In each iteration round the main loop, we shift the first tree node from the array at the top of the stack, print it, open a <ul> tag, and push to the stack an array of all its child nodes. Then, we get rid of any empty arrays from the top of the stack, closing one <ul> tag for each such empty array.
See it running here: https://3v4l.org/umpvf

Check if category has parent

i'm in mid of creating my own cms . And now i want to show which one of the category has parent but i don't know how, so please help me.
my category table
idkategori | namakategori | parentid
1 Programming 0
2 PHP 1
Or i need relationship table for my categories?
My Controller so far.
function tampilmenu()
{
$sql = "select * from fc_kategori";
$data['kategori'] = $this->bymodel->tampildata($sql);
$sql1 = "select parentid from fc_kategori";
$data['parent'] = $this->bymodel->tampildata($sql1);
$id=array();
foreach ($data['parent'] as $paren)
{
$id[]=$paren->parentid;
}
foreach ($data['kategori'] as $cat)
if(in_array($cat->parentid,$id))
{
$have ='Yes';
}
else
{
$have ='No';
}
echo $cat->idkategori.$have;
}
}
my model
function tampildata ($sql)
{
$query = $this->db->query($sql);
return $query->result();
}
Please don't laugh on me.
Kindly follow:
1) Since you are using a MVC framework, never write queries inside the controller (queries should always be written in models).
2) Never use raw queries, since CI provides you what is called as Active Record.
3) Also never pass direct queries anywhere you'll possibly code in whichever language. Always pass data and make do that function to compute and query process.
4) Remember, in CI Models are only used for database functionalities, Views are only used for your HTML markups and Controllers acts as the mediator between models and views.
Your code:
Controller -
public function tampilmenu()
{
$categories = $this->bymodel->get_category_having_parent();
echo "<pre>"; print_r($categories);
// this will return object having all categories that are parents
}
Model -
public function get_category_having_parent()
{
$parent_ids = array();
$ps = $this->get("parentid");
foreach($ps as $p)
{
$parent_ids[] = $p->parentid;
}
$this->db->where_in("id", $parent_ids);
$query = $this->db->get("fc_kategori");
return $query->result();
}
public function get($column="*")
{
$this->db->select($column);
$query = $this->db->get("fc_kategori");
return $query->result();
}

cakePHP 2 afterFind for Model

I inherited code and in the Model the previous developer used the afterFind, but left it open when afterFind is executed in case of many to many relation to that table. So it works fine when getting one element from that Model, but using the relations break it.
public function afterFind($results, $primary = false) {
foreach ($results as $key => $val) {
if (isset($results[$key]['Pitch'])) {
if (isset($results[$key]['Pitch']['expiry_date'])) {
if($results[$key]['Pitch']['expiry_date'] > time()) {
$results[$key]['Pitch']['days_left'] = SOMETHINGHERE;
} else {
$results[$key]['Pitch']['days_left'] = 0;
}
} else {
$results[$key]['Pitch']['days_left'] = 0;
}
To solve this issue I added that code after the 2nd line.
// if (!isset($results[$key]['Pitch']['id'])) {
// return $results;
//
Is there a better way to solve that? I think afterFind is quite dangerous if not used properly.

Building a menu system with a PHP class

UPDATE 2 ::
This is the output I am getting from the function right now :
<nav>
<ul> //UL 1
<ul> //UL 2
menu link 1
<li>login</li>
<li>
<ul>sublink3</ul>
</li>
<li><a>menu link 3</a></li>
<li>
<ul>
sublink1
<li>sublink2</li>
</ul>
</li>
<li>menu link 4</li>
<li>menu link 5</li>
<li>
<ul>
sublink5a
<li>sublink5b</li>
<li>sublink5c</li>
<li>sublink5d</li>
</ul>
</li>
</ul> //End of UL 2
<ul></ul> //dont not why this is here?
</ul> //End of UL 1
</nav>
UPDATE
Ok I have re-move the construct, and this is how I am trying to display my menu :
<?php include('./includes/menu.php'); ?>
<h1>HEADER FILE TITLE TEST</h1>
<?php
$build = new Menu;
var_dump($build->DisplayMenu());
?>
I mainly only use CakePHP to build sites with, but I am trying to push my PHP skills up to the next level. So I am looking at other frameworks and OOP (which I have never used within PHP before). So I set myself a little task of building a menu system, controllable from a database e.g. titles and path links come form my db.
This works fine when I just built it has a function put all my menu system items within an array and then used a print call to display the menu, then I just called the function on the page I had required the file to.
But I thought that this was not the best way about doing it, so I wanted to make it a class, so I put a class call around my function, and then changed the print call to a return. What I got form the class/function was a NULL answer when I var dumped it. So I did some research, and re-read a lot about how to declare an array within the magic 'construct' function.
But now I am very confused, should this be inside my Menu function or outside? Just by adding the construct function, it started to display 'string(9)' - which I don't know why? Here is my code :
//Menu Include file
class Menu {
public $testforme = "dfdfdfdf"; //Just a test to see how to call somedata
public function DisplayMenu() {
$DBConn = getConnection(); //Set new database connection
$SQLMainMenu = "SELECT * FROM menu"; //Get Menu setting from database
//$MenuBuild[] = NULL; //Open array for menu to save data into
function __construct($MenuBuild = array()) {
//not sure what to put here???
}
try {
$MenuBuild[] = '<nav><ul>';
//Foreach loop for all main menu links
foreach ($DBConn->query($SQLMainMenu) as $MainMenu) {
$MainMenuID = $MainMenu['id'];
$MainMenuPath = $MainMenu['linkpath'];
$MainMenuSublinkCounts = $MainMenu['sublinks'];
$SQLSubMenu = "SELECT * FROM submenu WHERE menu_id = $MainMenuID";
if ($MainMenuPath == 'NULL') {
$MenuBuild[] = '<li><a>' .$MainMenu['name'] .'</a>';
} else {
$MenuBuild[] = '<li>' .$MainMenu['name'] .'';
}
if ($MainMenuSublinkCounts >=1) { $MenuBuild[] = '<ul>'; }
//Foreach loop to build all inner links for menu
foreach ($DBConn->query($SQLSubMenu) as $SubMenu) {
$SubLinkMenuIDs = $SubMenu['menu_id'];
$SubLinkTitles = $SubMenu['name'];
$SubLinkPaths = $SubMenu['linkpath'];
if ($SubLinkMenuIDs == $MainMenuID) {
$MenuBuild[] = '<li>'. $SubLinkTitles . '</li>'; }
} //End of inner foreach loop
if ($MainMenuSublinkCounts >=1) {
$MenuBuild[] = '</ul>';
}
$MenuBuild[] = '</li>';
} //End of foreach loop
$MenuBuild[] = '</ul></nav>';
//Print the Array that holds the menu.
foreach ($MenuBuild as $MenuKey => $MenuData) {
$MenuSystem = $MenuBuild[$MenuKey]; return $MenuSystem;
}
} catch(PDOException $e) {
echo $e->getMessage();
}
} //End of function DisplayMenu
} //end of class
Now I am connection to my db using a PDO, which is working fine, it within a other file that both are included on.
Please let me know of any good help sites and I have read lots of the questions / answers on here.
Also please go easy on me? this is the 1st time I am using OOP PHP.
If of I am not doing something right, with the way I am building the menu please point at any issues with that :).
Thanks Glenn.
Ok, this is for you, but you really need to do some more research before putting something like this up
my approach encapsulates some functionality but it should seem pretty clear where it's headed just by looking at it. Please feel free to make any questions
(REREedited) includes/menu.php
<?php
class Menu {
private $db;
private $main;
public function __construct($db=null) {
$this->db = $db ? $db : getConnection();
$q = 'SELECT id,name,linkpath,sublinks FROM menu';
$this->main = $this->db->query($q);
}
public function displayMenu() {
$items = array();
foreach ($this->main as $row) {
$path = $row['linkpath'];
$link = self::link($row['name'],$path==='NULL'?null:$path);
// if there is a submenu, concatenate to with the "parent" into the same "item"
if ($row['sublinks'] > 0) $link .= $this->subMenu($row['id']);
$items[] = $link;
}
// returns the whole nav menu.
return '<nav>'.self::lists(array_filter($items)).'</nav>';
}
private function subMenu($id) {
$q = 'SELECT name,linkpath FROM submenu WHERE menu_id = ';
$sub = array();
foreach ($this->db->query($q.$id) as $row) {
$sub[] = self::link($row['name'],$row['linkpath']);
}
return self::lists(array_filter($sub));
}
static function link($content, $href) {
return '<a '.($href?'href="'.$href.'"':'').'>' .$content.'</a>';
}
static function lists(array $items) {
if (!$items) return null;
return '<ul><li>'.implode('</li><li>',$items).'</li></ul>';
}
}
test.php
<?php include('./includes/menu.php'); ?>
<h1>HEADER FILE TITLE TEST</h1>
<?php
try {
$build = new Menu;
echo $build->displayMenu();
} catch (Exception $e) {
echo $e->getMessage().PHP_EOL.$e->getTraceAsString();
}
?>

Categories