Nested comments pagination will not work for children comments - php

Hard limit is working thanks to Luuk, but pagination is still not working.
Based on my previous question, I am currently building comments system for a blog and want to make it reddit like.
I am currently using the nested comments by http://www.jongales.com/blog/2009/01/27/php-class-for-threaded-comments/
class Threaded_comments
{
public $parents = array();
public $children = array();
/**
* #param array $comments
*/
function __construct($comments)
{
foreach ($comments as $comment)
{
if ($comment['parent_id'] === NULL)
{
$this->parents[$comment['id']][] = $comment;
}
else
{
$this->children[$comment['parent_id']][] = $comment;
}
}
}
/**
* #param array $comment
* #param int $depth
*/
private function format_comment($comment, $depth)
{
for ($depth; $depth > 0; $depth--)
{
echo "\t";
}
echo $comment['text'];
echo "\n";
}
/**
* #param array $comment
* #param int $depth
*/
private function print_parent($comment, $depth = 0)
{
foreach ($comment as $c)
{
$this->format_comment($c, $depth);
if (isset($this->children[$c['id']]))
{
$this->print_parent($this->children[$c['id']], $depth + 1);
}
}
}
public function print_comments()
{
foreach ($this->parents as $c)
{
$this->print_parent($c);
}
}
}
Here’s the example usage with the data provided as an array. Remember that if your data is in another format you’ll have to modify the class.
$comments = array( array('id'=>1, 'parent_id'=>NULL, 'text'=>'Parent'),
array('id'=>2, 'parent_id'=>1, 'text'=>'Child'),
array('id'=>3, 'parent_id'=>2, 'text'=>'Child Third level'),
array('id'=>4, 'parent_id'=>NULL, 'text'=>'Second Parent'),
array('id'=>5, 'parent_id'=>4, 'text'=>'Second Child')
);
$threaded_comments = new Threaded_comments($comments);
$threaded_comments->print_comments();
Example Output:
Parent
Child
Child Third level
Second Parent
Second Child
I have set the depth limit, created the html output and everything, but the problem is pagination. Whenever I try to do a pagination via offset limit, it disrupts the child comments and the structure because the next page does not have original parent where to attach children and such.
For example:
select count(*) as found_comments from comments where blog_post = 1 and parent_id is null;
result = 20; // lets say its 20 parent comments limit from query above.
$query = "select * from comments where blog_post = 1 LIMIT 0,20";
$comments = [];
foreach ($query as $row)
{
$comments[] = [
'id' => $row['id'],
'parent_id' => $row['parent_id'],
'text' = $row['text'],
'added' => $row['added']];
}
$threaded_comments = new Threaded_comments($comments);
$threaded_comments->print_comments();
This code will limit both parent and children comments, but I need to set it to limit only parents, so 20 parents per page.
If I set in count query WHERE blog_post = 1 AND parent_id = 0
it will count parents only, but will cut out children comments when 20 quota is reached in $comments query.
It's really headache to find a way to do this. Any help is appreciated.

An example of how to change your code to only print 2 parents:
public function print_comments($count =2)
{
foreach ($this->parents as $c)
{
$this->print_parent($c);
$count--;
if($count == 0) exit;
}
}

Related

CakePHP 3.8 convert array to Cake\ORM\Entity

I am currently involved in a CakePHP project and do not know how I can pass a modified query/array to a paginator.
Here is my controller:
public function index($fooElement = '')
{
$query = $this->Properties->find()->where(['fooElement' => $fooElement]);
//The fooFunction needs an array cause for an internal call of cakes HASH::NEST function
$data= $this->FooModel->_fooFunction($query->enableHydration(false)->toList();
//Error: Not a paginable object
$data = $this->paginate($data)
$this->set(compact('fooElement', 'data'));
$this->set('_serialize', ['data']);
if (empty($fooElement)) {
$this->render('otherView');
}
}
EDIT: Here is the fooFunction:
public function _fooFunction($data)
{
$out = [];
$cache = [];
$nested = Hash::nest($data, ['idPath' => '{n}.id', 'parentPath' => '{n}.parent_id']);
$out = $this->_setOrderAndLevel($nested);
return $out;
}
protected function _setOrderAndLevel($items, $level = 0, $number = 0)
{
$out = [];
$items = Hash::sort($items, '{n}.orderidx');
foreach ($items as $item) {
$item['level'] = $level;
if (!empty($item['children'])) {
$children = $item['children'];
unset($item['children']);
$out[] = $item;
$out = array_merge($out, $this->_setOrderAndLevel($children, $level + 1));
} else {
$out[] = $item;
}
}
return ($out);
}
The _fooFunction takes the casted database query, makes some adjustments, adds two new properties and returns a nested Array. It maps id with parent_id in order to get children and a level description. The level description will be used for indentations in the view to display a hierarchical order.
IMPORTANT NOTICE: I am already beware of TreeBehavior in CakePHP but the problem is that our database has no left/right fields and I am not able to add them. Within this project I have to choose this way.
However $data contains exactly what I want but I need to transform it into a compatible object for pagination.
EDIT: Thanks to ndm I could build a paginable object with the necessary constraints. The last problem I still have in front of me is to merge all children and possible sub-children. A parent can have nth children and also a children can sometimes have nth sub-children. Therefore I solved this with a recursive call of my _setOrderAndLevel function within the fooFunction.
This is the current structure:
array(
[0] = fooEntity(
id = 1,
orderidx = 1,
parentId = null,
level = 0,
children(
id = 2,
orderidx = 2,
parentId = 1,
level = 1
children(
id = 3,
orderidx = 3,
parentId = 2,
level = 2
........
But it should be this:
array(
[0] = fooEntity(
id = 1,
orderidx = 1,
parentId = null
level = 0
[1] = fooEntity(
id = 2,
orderidx = 2,
parentId = 1,
level = 1
[2] = fooEntity(
id = 3,
orderidx = 3,
parentId = 2,
level = 2
........
I tried to build a second result formatter but it does not work:
...
return $results
->nest('id', 'parent_id', 'children')
->map($decorate);
})
->formatResults(function (\Cake\Collection\CollectionInterface $results) {
return $results->map(function ($data) {
call_user_func_array('array_merge', $data);
});
});
Maybe a "combine->" call could be the solution but I am not sure.
Any help is welcome
Generally if you need to format the results in some way, you should most likely use a result formatter, in order to be able to keep query object intact, and rom looking at the resulting format that your function produces, that is what you should use in this case, a result formatter.
If you need the ordering you could do that on SQL level already, and for nesting the results you could use the result collection's nest() method, ie you could ditch using the Hash class:
$query = $this->Properties
->find()
->where(['fooElement' => $fooElement])
->order(['orderidx' => 'ASC'])
->formatResults(function (\Cake\Collection\CollectionInterface $results) {
$fold = function ($rows, $level = 0) use (&$fold) {
$folded = [];
foreach ($rows as $row) {
$row['level'] = $level;
$children = $row['children'] ?: null;
unset($row['children']);
$folded[] = $row;
if ($children) {
$folded = array_merge(
$folded,
$fold($children, $level ++)
);
}
}
return $folded;
};
$nested = $results->nest('id', 'parent_id', 'children');
$folded = $fold($nested);
return collection($folded);
});
Note that you must return an instance of \Cake\Collection\CollectionInterface from the result formatter. The docs say that returning an(y) iterator would be enough, but as soon as there are additional formatters appended that expect a collection, things would break.
See also
Cookbook > Database Access & ORM > Query Builder > Adding Calculated Fields
Cookbook > Collections > Working with Tree Data

foreach until find some value

I have a code that get categories from database but I don't know how to get all subcategories(parents).
This my php code :
function get_the_category($allCats,$filter_id = null) {
$re_struct_cat = array();
$filter_id = 10;
$ids = array();
$xx = array();
foreach($allCats as $cat_key=>$cat_val) {
$re_struct_cat[$cat_val["id"]] = array(
"title" => $cat_val["cat_title"],
"parent" => $cat_val["cat_parent"],
);
$ids = array_merge($ids,array($cat_val["id"]));
}
foreach($ids as $k=>$v) {
if($re_struct_cat[$v]["parent"]) {
$xx[] = $re_struct_cat[$re_struct_cat[$v]["parent"]];
}
}
return $xx;
//return $re_struct_cat;
//print_r($re_struct_cat);
}
What I want exactly
I have table with 3 columns [id,title,parent]
ID TITLE PARENT
1 Science 0
2 Math 1
3 Algebra 2
4 Analyse 2
5 Functions 4
So if variable filter_id = 10 I got cat_parent = 4
So I want to take that value and looking for it in array and if find another cat_parent do the same thing until find 0 or null value
It is not the most optimal solution, but you can use iterators.
Firstly, create the custom iterator, that can handle categories:
class AdjacencyListIterator extends RecursiveArrayIterator
{
private $adjacencyList;
public function __construct(
array $adjacencyList,
array $array = null,
$flags = 0
) {
$this->adjacencyList = $adjacencyList;
$array = !is_null($array)
? $array
: array_filter($adjacencyList, function ($node) {
return is_null($node['parent']);
});
parent::__construct($array, $flags);
}
private $children;
public function hasChildren()
{
$children = array_filter($this->adjacencyList, function ($node) {
return $node['parent'] === $this->current()['id'];
});
if (!empty($children)) {
$this->children = $children;
return true;
}
return false;
}
public function getChildren()
{
return new static($this->adjacencyList, $this->children);
}
}
It is taken from my another answer.
Then you can simply loop over this iterator until you find the needed id:
$id = 5;
$categories = [];
$result = null;
foreach ($iterator as $node) {
$depth = $iterator->getDepth();
$categories[$depth] = $node['categoryname'];
if ($node['id'] === $id) {
$result = array_slice($categories, 0, $depth + 1);
break;
}
}
Here is the demo.

separate html from php comments class

I found this class for threaded comment using php and MySQL:
<?php
class Threaded_comments
{
public $parents = array();
public $children = array();
/**
* #param array $comments
*/
function __construct($comments)
{
foreach ($comments as $comment)
{
if ($comment['parent_id'] === NULL)
{
$this->parents[$comment['id']][] = $comment;
}
else
{
$this->children[$comment['parent_id']][] = $comment;
}
}
}
/**
* #param array $comment
* #param int $depth
*/
private function format_comment($comment, $depth)
{
for ($depth; $depth > 0; $depth--)
{
echo "\t";
}
echo $comment['text'];
echo "\n";
}
/**
* #param array $comment
* #param int $depth
*/
private function print_parent($comment, $depth = 0)
{
foreach ($comment as $c)
{
$this->format_comment($c, $depth);
if (isset($this->children[$c['id']]))
{
$this->print_parent($this->children[$c['id']], $depth + 1);
}
}
}
public function print_comments()
{
foreach ($this->parents as $c)
{
$this->print_parent($c);
}
}
}
this worked with this array:
$comment = array( array('id'=>1, 'parent_id'=>NULL, 'text'=>'Parent'),
array('id'=>2, 'parent_id'=>1, 'text'=>'Child'),
array('id'=>3, 'parent_id'=>2, 'text'=>'Child Third level'),
array('id'=>4, 'parent_id'=>NULL, 'text'=>'Second Parent'),
array('id'=>5, 'parent_id'=>4, 'text'=>'Second Child')
);
and for result:
$tc = new Threaded_comments($comment);
$tc->print_comments();
and result is:
Parent
Child
Child Third level
Second Parent
Second Child
this worked true But for defign comment list(html) I need to add all html code into private function format_comment($comment, $depth). this is not good way and I think need separate html from php class.
EDIT:(this is my page for show/print threaded comment)
function _comments_($id,$type){
$DB_QUERY = mySqli::f("SELECT id,user,email,message,timestamp,parent_id,rfield_1,rfield_2,rfield_3,rfield_4,rfield_5 FROM " . NEWS_COMMENTS . " LEFT JOIN " . NEWS_REVIEWS . " ON " . NEWS_COMMENTS . ".id = " . NEWS_REVIEWS . ".cid WHERE
pid = ? AND type = ? AND approved = 1 ORDER BY timestamp DESC LIMIT 12", $id, $type);
foreach($DB_QUERY as $row){
$commentdata[] = $row;
}
return $commentdata;
}
$comments_list = _comments_('125','book');
foreach($comments_list as $row){
$comments[] = array(
'id' => $row['id'],
'parent_id' => $row['parent_id'],
'name' => $row['user'],
'text' => $row['message'],
'datetime' => $row['timestamp']
);
}
$tc = new Threaded_comments($comments);
$tc->print_comments();
In my page threaded comments show with format_comment and work true. in case i need to design output using separate html from php class.
how do we separate html code from class in this case?!
You're right.
1) Most simple solution is to pass your array to a template. This template doesn't contain logic, but only html and php (e.g. foreach of your your array).
2) For beter separation, use Model-View-Controller pattern:
With this layer construction, each layer has its own responsibilities. Never html in model and controller. Queries in the models only.
Frameworks as Laravel and CodeIgniter have an implementation of this pattern.

Parent child Relationship in mysql

I am having a table like the following,need to display as Parent and child format
--------------------------------------------------------
id role_name role_id parent_id
--------------------------------------------------------
1 NSM 1 0
2 MR 5 2
3 ASM 4 3
4 ZSM 3 4
5 RSM 2 1
---------------------------------------------------------
the result is like to be the following
NSM
---RSM
-----ZSM
-----NSM
-----MR
NSM->ROOT
RSM->FIRST CHILD
ZSM->SECOND CHILD
NSM->THIRD CHILD
MR->LEAF
// Fetch all the roles
$result = mysql_query("select * from roles");
$roles = array();
while( $role = mysql_fetch_assoc($result) ) {
$roles[] = $role;
}
// Function that builds a tree
function build_tree($roles, $parent_id=0) {
$tree = array();
foreach ($roles as $role) {
if ($role['parent_id'] == $parent_id) {
$tree[] = array(
'role' => $role,
'children' => build_tree($roles, $role['parent_id'])
);
}
}
return $tree;
}
// Function that walks and outputs the tree
function print_tree($tree) {
if (count($tree) > 0) {
print("<ul>");
foreach($node in $tree) {
print("<li>");
htmlspecialchars($node['role']['role_name']);
print_tree($node['children']);
print("</li>");
}
print("</ul>");
}
}
SQL Results are always flat - you'll not be able to return a hierarchy view of that data in a query.
Instead, I would suggest using whichever client components you are using to show that (is it a tree? what exactly?) that knows how to go thru a flat list and build a hierarchy out of that.
If you want to print a view like that in a console (why would you ever want to do that?), you could do like this:
$data = array();
$query = mysql_query("SELECT * FROM table ORDER BY parent_id");
while($array = mysql_fetch_assoc($query))
{
$data[$array['parent_id']][] = $array;
}
function output_hierarchy($id, $prepend)
{
$current = $data[$id];
foreach($current as $item)
{
print $prepend . " " . $item['role_name'];
if(count($data[$item['id']]) > 0)
{
output_hierarchy($item['id'], $prepend . "--");
}
}
}
output_hierarchy(0, '');
If you want to use this on your website, you can easily adapt it. Code should be self-explanatory.

how to loop over looped elements in Php?

I'm creating a tree-structure of categories with parentid's which can be called from children in such a way:
ID | Name | ParentID
1 1 0
2 2 1
3 3 2
4 4 1
Resulting in this:
1 = 1
2 = 1 -> 2
3 = 1 -> 2 -> 3
4 = 1 -> 4
which means 3 is a child of 2, which is a child of 1.
when trying to get this idea (with the -> to show what relations are set) I only get to the second grade (1 -> 2) but not to the third (1->2->3) because of the looping function I use for it.
//put all ID's in an array
while ($row2 = $connector->fetchArray($result2)){
$id = $row2['ID'];
$parents[$id] = $row2['name'];
}
// show the tree-structure
while ($row = $connector->fetchArray($result)){
if($row['parentid']!=0)echo $parents[$row['parentid']].' -> ';
echo $row['name'].' - ';
echo '<br>';
}
I'd like two things to change:
have the code automatically generate a tree sized as necessary.
in the while-loops i have to select the $result twice (once as $result, once as $result2) to make it work. these $result's have exactly the same database-query:SELECT ID,name,parentid FROM categories
to fetch results from. I'd like to only declare this once.
Thanks for all the good answers. I've gone with the easiest, less-code-to-implement approach:
$result = $connector->query('SELECT ID,name,parentid FROM categories');
// Get an array containing the results.
$parents = array();
while ($row = $connector->fetchArray($result)){
$id = $row['ID'];
$parents[$id] = array('ID' => $row['ID'],'name' => $row['name'],'parentid' => $row['parentid']);
}
foreach ($parents as $id => $row){
$pid=$id;
$arrTmp= array();
do { // iterate through all parents until top is reached
$arrTmp[]=$pid;
$pid = $parents[$pid]['parentid'];
}while ($pid != 0);
$arrTmp = array_reverse($arrTmp);
foreach($arrTmp as $id){
echo $parents[$id]['name'].' -> ';
}
echo '<br>';
}
Rather than have PHP organize the items into a tree, why not ask the database to do it for you? I found this article on hierarchical data to be very good and the examples are almost identical to yours.
EDIT
The SQL for getting the full tree using the Adjacency Model is not ideal. As the article explains it requires rather a lot of joins for even a small hierarchy. Is it not possible for you to use the Nested Set approach? The SQL stays the same regardless of the size of the hierarchy and INSERT and DELETE shouldn't be very difficult either.
If you really want to do hierachies with parent ids(suitable only for small number of items/hierachies)
I modified your code a little bit(I did not test it so there may be some syntax errors):
//put all recordsets in an array to save second query
while ($row2 = $connector->fetchArray($result2)){
$id = $row2['ID'];
$parents[$id] = array('name' => $row2['name'],'parent' => $row2['parentid']);
}
// show the tree-structure
foreach ($parents as $id => $row){
$pid = $row['parentid'];
while ($pid != 0){ // iterate through all parents until top is reached
echo $parents[$pid]['name'].' -> ';
$pid = $parents[$pid]['parentid'];
}
echo $parents[$id]['name'].' - ';
echo '<br>';
}
To answer your comment:
$parents = array();
$parents[2] = array('ID'=>2,'name'=>'General','parentid'=>0);
$parents[3] = array('ID'=>3,'name'=>'Gadgets','parentid'=>2);
$parents[4] = array('ID'=>4,'name'=>'iPhone','parentid'=>3);
foreach ($parents as $id => $row){
$pid=$id;
$arrTmp= array();
do { // iterate through all parents until top is reached
$arrTmp[]=$pid;
$pid = $parents[$pid]['parentid'];
}while ($pid != 0);
$arrTmp = array_reverse($arrTmp);
foreach($arrTmp as $id){
echo $parents[$id]['name'].' -> ';
}
echo '<br>';
}
Prints out:
General ->
General -> Gadgets ->
General -> Gadgets -> iPhone ->
Maybe easier with OOP. Just sort the query by parentId
Note: The listChildren method and the printout at the bottom is just there to show it is listed correctly. I did not interpret the question that the display was important.
class Element {
public $id;
public $name;
public $parent = null;
public $children = array();
public function __construct($id, $name)
{
$this->id = $id;
$this->name = $name;
}
public function addChild($element)
{
$this->children[$element->id] = $element;
$element->setParent($this);
}
public function setParent($element)
{
$this->parent = $element;
}
public function hasChildren()
{
return !empty($this->children);
}
public function listChildren()
{
if (empty($this->children)) {
return null;
}
$out = array();
foreach ($this->children as $child) {
$data = $child->id . ':' . $child->name;
$subChildren = $child->listChildren();
if ($subChildren !== null) {
$data .= '[' . $subChildren . ']';
}
$out[] = $data;
}
return implode(',', $out);
}
}
$elements = array();
$noParents = array();
while ($row = $connector->fetchArray($result)) {
$elements[$row['id']] = $element = new Element($row['id'], $row['name']);
if (isset($elements[$row['parent']])) {
$elements[$row['parent']]->addChild($element);
} else {
$noParents[] = $element;
}
}
foreach ($noParents as $element) {
if ($element->hasChildren()) {
echo "Element {$element->id} has children {$element->listChildren()}.\n";
} else {
echo "Element {$element->id} has no children.\n";
}
}
If you are using PostgreSQL as the database, you can use the connectby() function to create the record set:
SELECT *
FROM connectby('tableName', 'id', 'parent_id')
AS t(keyid text, parent_keyid text, level int);
I love this function, and use all the time in my code. It can do some very powerful things, very quickly, and you don't have maintain the left/right values like the (adjacency model).

Categories