Nested comments using iteration in PHP - php

I'm currently working on a comment system using PHP,and I'm using 'parent ID' solution as to connect one reply to another.The problem is I haven't figured out how to cast these 'parent ID' connected data stored in mysql to a PHP array and render them out.I've been searching for iteration solution but nothing found out.My database structure is as follow:
Parent_id 0 means top level comment.
comment_id content parent_id
1 xxx 0
2 xxx 0
3 xxx 1
4 xxx 3
5 xxx 4
6 xxx 3
... ... ...
Here is what I have done,I fetched all the comments out in an array,and the array looks like this:
$comment_list = array(0=>array('comment_id'=>1,'content'=>'xxx','parent_id'=>0),
0=>array('comment_id'=>2,'content'=>'xxx','parent_id'=>0),
0=>array('comment_id'=>3,'content'=>'xxx','parent_id'=>1),
0=>array('comment_id'=>4,'content'=>'xxx','parent_id'=>3),
...
)
I need to attach comment with parent_id 1 to comment with comment_id 1,and so on,depth should be unlimited,and working for hours still can't find a way to iterate properly,can someone give me some pointers on how to do this?I know a solution but it makes new request to the database upon every iteration,so I prefer to do it using PHP array once for all,thank you !

When faced with a complex structure like this, it is sometimes better to create an object oriented solution, and then use the objects to create the array that you require.
For example, based on your above, I might define the following class:
class Comment{
protected $id;
protected $children;
protected $content;
public function __construct( $id, $content ){
$this->id = $id;
$this->content = $content;
$this->children = array();
}
public function addChild( $child ){
$this->children[] = $child;
}
}
Now, we use this object to transfer your database into the working memory as follows:
$workingMemory = array(); //a place to store our objects
$unprocessedRows = array(); //a place to store unprocessed records
// here, add some code to fill $unproccessedRows with your database records
do{
$row = $unprocessedRows; //transfer unprocessed rows to a working array
$unprocessedRows = array(); //clear unprocessed rows to receive any rows that we need to process out of order.
foreach( $row as $record ){
$id = $record[0]; //assign your database value for comment id here.
$content = $record[1]; //assign your database value for content here.
$parentId = $record[2]; //assign your database value for parent id here
$comment = new Comment( $id, $content );
//for this example, we will refer to unlinked comments as
//having a parentId === null.
if( $parentId === null ){
//this is just a comment and does not need to be linked to anything, add it to working memory indexed by it's id.
$workingMemory[ $id ] = $comment;
}else if( isset( $workingMemory[ $parentId ] ) ){
//if we are in this code block, then we processed the parent earlier.
$parentComment = $workingMemory[ $parentId ];
$parentComment->addChild( $comment );
$workingMemory[ $id] = $comment;
}else{
//if we are in this code block, the parent has not yet been processed. Store the row for processing again later.
$unprocessedRows[] = $record;
}
}
}while( count( $unprocessedRows ) > 0 );
Once all the unprocessedRows are complete, you now have a representation of your comments entirely stored in the variable $workingMemory, and each cell of this array is a Comment object that has an $id, a $content, and links to all children $comments.
We can now iterate through this array and make whatever data arrays or tables we want. We must remember, that the way that we stored arrays, we have direct access to any comment directly from the $workingMemory array.
If I were using this to generate HTML for a website, I would loop through the workingMemory array and process only the parent comments. Each process would then iterate through the children. By starting with the parents and not the children, we would guarantee that we are not processing the same comment twice.
I would alter my Comment class to make this easier:
class Comment{
protected $id;
protected $children;
protected $content;
protected $isRoot;
public function __construct( $id, $content ){
$this->id = $id;
$this->content = $content;
$this->children = array();
$this->isRoot = true;
}
public function addChild( $child ){
$child->isRoot = false;
$this->children[] = $child;
}
public function getChildren(){ return $this->children; }
public function getId(){ return $this->id; }
public function getContent(){ return $this->content; }
}
After this change, I can create my HTML as follows:
function outputCommentToHTML( $aComment, $commentLevel = 0 ){
//I am using commentLevel here to set a special class, which I would use to indent the sub comments.
echo "<span class'comment {$commentLevel}' id='".($aComment->getId())."'>".($aComment->getContent())."</span>";
$children = $aComment->getChildren();
foreach( $children as $child ){
outputCommentToHTML( $child, $commentLevel + 1 );
}
}
foreach( $workingMemory as $aComment ){
if( $aComment->isRoot === true ){
outputCommentToHTML( $aComment );
}
}
This would convert database columns into the format you require. For example, if we had the following data:
comment_id content parent_id
1 xxx 0
2 xxx 0
3 xxx 1
4 xxx 3
5 xxx 4
6 xxx 3
... ... ...
It would output in HTML:
Comment_1
Comment_3
Comment_4
Comment_5
Comment_6
Comment_2
This is done recursively in the function, which processes Comment_1 fully before moving to Comment 2. It also processes Comment_3 fully before moving to Comment 2, which is how Comments 4, 5 and 6 all get output before Comment 2.
The above example will work for you, but if it were my personal project, I would not mix linear and Object oriented code, so I would create a code factory to convert Comments into HTML. A factory make data strings from source objects. You can create an Object that acts as a factory for HTML, and another factory that acts as a generator of SQL, and by layer objects with solutions like this, you can create an entirely Object Oriented solution, which is easier to understand to the average reader and sometimes even to non-coders to produce something like this:
//these definition files get hidden and tucked away for future use
//you use include, include_once, require, or require_once to load them
class CommentFactory{
/**** other Code *****/
public function createCommentArrayFromDatabaseRecords( $records ){
/*** add the data conversion here that we discussed above ****/
return $workingMemory;
}
}
class HTMLFactory{
public function makeCommentTableFromCommentArray( $array ){
$htmlString = "";
foreach( $array as $comment ){
if( $comment->isRoot ){
$htmlString .= $this->getHTMLStringForComment( $comment );
}
}
return $htmlString;
}
private function getHTMLStringForComment( $comment, $level=0 ){
/*** turn your comment and all it's children into HTML here (recursively) ****/
return $html;
}
}
Done properly, it can clean up your active code file so that it reads almost like a list of instructions like this:
//let database be a mysqli or other database connection
//let the query function be whatever method works for your database
// of choice.
//let the $fetch_comment_sql variable hold your SQL string to fetch the
// comments
$records = $database->query( $fetch_comment_sql )
$comFactory = new CommentFactory();
$commentArray = $comFactory->createCommentArrayFromDatabaseRecords( $records );
$htmlFactory = new HTMLFactory();
$htmlResult = $htmlFactory->makeCommentTableFromCommentArray( $commentArray );
echo $htmlResult;

Related

Assign Array to Property in PHP constructor function

Is it possible to store an array as an object property in PHP?
I am building an article class that pulls various information about a research article and stores them as a properties in an object. Since the number of authors vary per research article, I would like to store them as an array in an $authors property, rather than store each author as a separate property. In this code sample, I realize this problem results from working with a poorly designed table, but nonetheless, I would like to see how this code could be used to store an array as an object property.
<?php
Class Article {
public $id;
public $authors;
public $article_name;
public $journal;
public $volume_number;
public $issue_number;
public $article_location;
public function __construct($id, array $authors, $article_name, $journal,
$volume_number, $issue_number, $article_location)
{
$this->$id = $id;
$this->$authors = $authors;
$this->$article_name = $article_name;
$this->$journal = $journal;
$this->$volume_number = $volume_number;
$this->$issue_number = $issue_number;
$this->$article_location = $article_location;
}
}
//function to pull Article information from Articles Table
function getArticle($id){
try {
$query = "SELECT * FROM articles WHERE ID = :ID";
$db = Db::getInstance();
$results = $db->prepare($query);
$results->execute([':ID'=>$id]);
$row = $results->fetch();
$authors = array();
if(!empty($row['author'])){
$authors[] = $row['author'];
}
if(!empty($row['author2'])){
$authors[] = $row['author2'];
}
if(!empty($row['author3'])){
$authors[] = $row['author3'];
}
//This repeats for a while.
return new article($row['ID'],
$authorList,
$row['article_name'],
$row['journals'],
$row['volume_number'],
$row['issue_number'],
$row['article_location']);
} catch (PDOException $e) {
return "Unable to pull articles from the Articles table.";
echo $e->getMessage();
}
}
Yes, it is possible to store an array as a property.
The problem is that you use properties wrong.
$this->$authorList
Is wrong, you should use:
$this->authorList
Your code currently creates properties for your class based on the original property's value - if $article_name has the value of 'ABCD', $this->$article_name creates and fills the property 'ABCD' - being the equivalent of $this->ABCD = $article_name;, meaning you won't be able to access the value in the original property. It's the same with $this->$authors = $authors; - if you are passing an array as $authors, your code will try to store it as a string, making the situation even worse. Removing the $ before $authors solves this issue too.
Also, when you use $authorList[], you are pushing values into a local variable, not into the class property. It's not necessarily the wrong way to do it, as long as you copy the local variable's content into the property, but I would strongly suggest not to use variables named after properties. It makes your code harder to maintain, as it can confuse developers.

Assigning PHP object when object key unknown but sub key known in the object

Below lists the output of a mySQL query in PHP PDO. The object contains multiple columns from two tables that are then to be combined into a single object.
Some rows in the same table are children of others as identified by the column parent_ID. These children then need to be added to the object of the parent as do their children and so on and so on.
As much as I can achieve this simply for the first two levels of children I cannot see a way without performing another foreach to achieve this beyond the first to layers of the object.
This example should add clarity to the above:
foreach($components as $component){
if($component->parent_ID < 0){
$output->{$component->ID} = $component;
}
else if($output->{$content->parent_ID}){
$output->{$content->parent_ID}->child->{$component->ID} = $component;
}
else if($output->?->child->{$conent->parent_ID}){
$output->?->child->{$content->parent_ID}->child->{$component->ID} = $component;
}
}
Not on the third line there is an ? where there would normally be an ID. This is because we now do not know what that ID is going to be. In the first layer we did because it would be the parent_ID but this line is dealing with the children of children of a parent.
So, as I far I understood from comments and assuming that you don't have a lot of records in DB, it's seems to me the best way is to preload all rows from DB and then build a tree using this function
public function buildTree(array &$objects) {
/** thanks to tz-lom */
$index = array();
$relations = array();
foreach($objects as $key => $object) {
$index[$object->getId()] = $object->setChildren(array());
$relations[$object->getParentId()][] = $object;
if ($object->getParentId()) {
unset($objects[$key]);
}
}
foreach ($relations as $parent => $children) {
foreach ($children as $_children) {
if ($parent && isset($index[$parent])) {
$index[$parent]->addChildren($_children->setParent($index[$parent]));
}
}
}
return $this;
}
P.S. Really, I don't see other way without foreach in foreach. At least, it's not recursive

Get enum options in laravels eloquent

In my migration file, I gave my table pages a enum field with 2 possible values (as seen below). My question is, if it's possible to select these values with Laravels Eloquent?
$table->enum('status', array('draft','published'));
There are several Workarounds that I found, but there must be some "eloquent-native" way to handle this. My expected output would be this (that would be perfect!):
array('draft','published')
Thank you in advance!
Unfortunately, Laravel does not offer a solution for this. You will have to do it by yourself. I did some digging and found this answer
You can use that function and turn it into a method in your model class...
class Page extends Eloquent {
public static function getPossibleStatuses(){
$type = DB::select(DB::raw('SHOW COLUMNS FROM pages WHERE Field = "type"'))[0]->Type;
preg_match('/^enum\((.*)\)$/', $type, $matches);
$values = array();
foreach(explode(',', $matches[1]) as $value){
$values[] = trim($value, "'");
}
return $values;
}
}
And you use it like this
$options = Page::getPossibleStatuses();
If you want you can also make it a bit more universally accessible and generic.
First, create a BaseModel. All models should then extend from this class
class BaseModel extends Eloquent {}
After that, put this function in there
public static function getPossibleEnumValues($name){
$instance = new static; // create an instance of the model to be able to get the table name
$type = DB::select( DB::raw('SHOW COLUMNS FROM '.$instance->getTable().' WHERE Field = "'.$name.'"') )[0]->Type;
preg_match('/^enum\((.*)\)$/', $type, $matches);
$enum = array();
foreach(explode(',', $matches[1]) as $value){
$v = trim( $value, "'" );
$enum[] = $v;
}
return $enum;
}
You call this one like that
$options = Page::getPossibleEnumValues('status');
Made a small improvement to lukasgeiter's function. The foreach loop in his answer is parsing the string. You can update the regex to do that for you.
/**
* Retrieves the acceptable enum fields for a column
*
* #param string $column Column name
*
* #return array
*/
public static function getPossibleEnumValues ($column) {
// Create an instance of the model to be able to get the table name
$instance = new static;
// Pulls column string from DB
$enumStr = DB::select(DB::raw('SHOW COLUMNS FROM '.$instance->getTable().' WHERE Field = "'.$column.'"'))[0]->Type;
// Parse string
preg_match_all("/'([^']+)'/", $enumStr, $matches);
// Return matches
return isset($matches[1]) ? $matches[1] : [];
}
This throws an error if the column does not exist. So I added a small check in the code
public static function getPossibleEnumValues ($column) {
// Create an instance of the model to be able to get the table name
$instance = new static;
$arr = DB::select(DB::raw('SHOW COLUMNS FROM '.$instance->getTable().' WHERE Field = "'.$column.'"'));
if (count($arr) == 0){
return array();
}
// Pulls column string from DB
$enumStr = $arr[0]->Type;
// Parse string
preg_match_all("/'([^']+)'/", $enumStr, $matches);
// Return matches
return isset($matches[1]) ? $matches[1] : [];
}
As of L5.17 Eloquent does not include this functionality, instead you need to fall back to native QL. Here's an example that will work with SQL and in one line - returning an array like you asked.
In the spirit of one liner complexity ;)
I threw this in one of my view composers - it fetches the column from the table, explodes it and assembles the values in an array.
I iterate over that in my views using a foreach.
explode (
"','",
substr (
DB::select(" SHOW COLUMNS
FROM ".(new \Namespace\Model)->getTable()."
LIKE 'colName'"
)[0]->Type,
6,
-2
)
);

Handling items in the collection pattern by a data mapper

My question is related to the update section of #tereško's answer in "Who should handle the conditions in complex queries, the data mapper or the service layer?" Below is the code for reference and convenience.
$category = new Category;
$category->setTitle( 'privacy' );
$list = new ArticleCollection;
$list->setCondition( $category );
$list->setDateRange( mktime( 0, 0, 0, 12, 9, 2001) );
// it would make sense, if unset second value for range of dates
// would default to NOW() in mapper
$mapper = new ArticleCollectionMapper;
$mapper->fetch( $list );
foreach ( $list as $article )
{
$article->setFlag( Article::STATUS_REMOVED );
}
$mapper->store( $list );
In this code ArticleCollection is a collection of Domain Objects, let's call them Articles. The moment the ArticleCollectionMapper fetches data from the database, assigning it to $list, instances of Article need to be made (for each row). Would the instances of Article be added to our collection instance ($list) via a method like $list->addArticle($newArticle), should a Factory Object like ArticleFactory be used for that, or is there another option I haven't considered?
I wouldn't think to actually use a factory object to add the articles. You may see yourself using one to make the instance of Article (in the second example), though. What I went ahead and did was add an addArticles () method to the ArticleCollection instance. This way you can simply call the method on your instance of ArticleCollection from the mapper. ArticleCollectionMapper may look something like:
class ArticleCollectionMapper extends DataMapperAbstract
{
public function fetch ( ArticleCollection $articles )
{
$prepare = $this->connection->prepare( "SELECT ..." );
$prepare->execute();
// filter conditions
$articles->addArticles( $prepare->fetchAll() );
}
}
You'd need to do some filtering by getting the conditions from the ArticleCollection instance, which is excluded from the snippet above. Then our domain object's addArticles() implementation would look similar following:
class ArticleCollection extends DomainObjectAbstract
{
protected $collection = array();
public function addArticles ( Array $articles )
{
foreach ( $articles as $article )
{
$articleCollectionItem = new Article;
$articleCollectionItem->setParams( $article );
// however you prefer filling your list with `Article` instances
$this->collection[] = $articleCollectionItem;
}
}
}
You may also want to add an addArticle() method depending on your needs, and then just replacing what's within the foreach above with a call to addArticle(). Note that the above examples are extremely simplified and code will need to be adapted in order to meet your standards.

idiorm / paris has_many as_array result set

I'm having trouble getting the results of a has_many query using php idiorm/paris. Following the example from the paris site the has_many result for posts returns as an object.
That's great, and I can run through the object and access individual methods, but what I want to be able to do is pass the result set as an associative array off to my template engine for display.
Example:
class Post extends Model {
}
class User extends Model {
public function posts() {
return $this->has_many('Post'); // Note we use the model name literally - not a pluralised version
}
}
The api works this way:
// Select a particular user from the database
$user = Model::factory('User')->find_one($user_id);
// Find the posts associated with the user
$posts = $user->posts()->find_many();
I am able to access the posts object and print the result set like this:
// echo each post id
foreach ($posts as $post) {
echo $post->id;
}
What I'd really like to be to do though, is use as_array() to get the entire resultset as an associative array, limited by certain fields in the way as_array works for an individual row, e.g.
$post_list = $posts()->as_array(id,title,post,date);
This, or a call on something like $user->posts()->find_many()->as_array() don't work.
What is the correct way to access this type of result set using paris?
Adding this method to idiorm.php gives me the desired functionality.
public function find_array() {
if (func_num_args() === 0) {
return $this->_run();
}
$args = func_get_args();
$array = array();
foreach ($this->_run() as $r) {
$array[] = array_intersect_key($r, array_flip($args));
}
return $array;
}
Now I can call $post_list = $posts()->find_array(); or $posts()->find_array('id','title'); etc.
find_one returns a Model object, find_many returns an array of Models.
If you want to get the entire result set as an array of associative array, one solution should be to use array_map
function model_as_array($model) {
return $model->as_array();
}
$posts = $user->posts()->find_many();
$my_view->posts = array_map(model_as_array, $posts);
var_dump($my_view->posts);
or in php 5.3+ (not tested)
$aa_posts = array_map(function($model) {return $model->as_array();} , $posts);

Categories