Get Nested Level from Recursive Menu - php

I was looking at implementing the recursive function from this SO item: PHP: nested menu with a recursive function, expand only some nodes (not all the tree) to build a right click menu on my application and need to get the nested level. I am trying to think through the workflow here but can't think of where to put my increment by one and reset it to end up with variables such as:
$nestedLevel = 1; // main item
$nestedLevel = 2; // sub menu
$nestedLevel = 2; // sub-sub menu
I would like to use that to setup some if statements to I can handle each level differently. the Main level will be a simple list, Level 2 would be a horizontal button group, and the third will be simple lists assigned to tabs from the button group. Here is what I have:
function recursive($parent, $array) {
$has_children = false;
foreach($array as $key => $value) {
if ($value['menu_parent_id'] == $parent) {
if ($has_children === false && $parent) {
$has_children = true;
echo '<ul>' ."\n";
}
echo '<li>' . "\n";
echo '' . $value['name'] . '' . " \n";
echo "\n";
recursive($key, $array);
echo "</li>\n";
}
}
if ($has_children === true && $parent) echo "</ul>\n";
}
In the end it will look something like this (with some more styling of course):

Related

How to generate a recursive list of directories and files with hyperlinks to each one?

I'm at my wit's end here trying to generate a list of all directories and files in PHP. The idea was so that the navigation bar could be dynamically updated simply by adding more files.
An example of what the directory structure might look like is as follows:
Directory 1
Subdirectory 1
Category 1
Page 1
Page 2
Category 2
...
Directory 2
Subdirectory...
Directory 3
...
Each page would be linked to. For example, Directory 1 -> Subdirectory 1 -> Category 1 -> Page 1 would be /directory-1/subdirectory-1/category-1/page-1.
I found a few potential solutions but none that really fitted the bill. I chose to build off of this comment on the PHP website.
This is my code (first function is lifted directly from that linked comment):
function dirToArray($dir) {
$result = array();
$cdir = scandir($dir);
foreach ($cdir as $key => $value)
{
if (!in_array($value,array(".","..")))
{
if (is_dir($dir . DIRECTORY_SEPARATOR . $value))
{
$result[$value] = dirToArray($dir . DIRECTORY_SEPARATOR . $value);
}
else
{
$result[] = $value;
}
}
}
return $result;
}
$contentsArray = dirToArray("contents");
function listStuff($contentsArray, $contentsArrayArray, $parentDirectory) {
foreach ($contentsArrayArray as $key => $value) {
if (is_array($value)) {
if (!empty($GLOBALS["depthWorkaround"])) {
$parentDirectory = $GLOBALS["depthWorkaround"];
$GLOBALS["depthWorkaround"] = null;
}
$isDirectory = true;
$directoryName = explode("_", $key)[1];
$directoryURL = str_replace(" ", "-", strtolower($directoryName));
echo "<li>📁 $directoryName</li>";
if (!empty($contentsArrayArray[$key])) {
$parentDirectory = "$parentDirectory/" . $directoryURL;
echo "<ul>";
listStuff($contentsArray, $contentsArrayArray[$key], $parentDirectory);
echo "</ul>";
}
} else {
$isDirectory = false;
$directoryName = explode("_", $value)[1];
$directoryURL = str_replace(" ", "-", strtolower($directoryName));
echo "<li>🗎 $directoryName</li>";
}
}
$GLOBALS["depthWorkaround"] = explode("/", $parentDirectory)[1];
}
listStuff($contentsArray, $contentsArray, null);
I think it's best I try and explain what is going on here. The dirToArray function lists everything in a directory as a multidimensional array. It's really rather neat. In this case, my chosen directory is "contents".
Next up is listStuff. It does a number of things, mostly turning the array into a list, similar to shown above, and making them all hyperlinks, with the URLs being lowercase and no spaces.
If you are testing this yourself, note that all directories and files must have an underscore in them, or things will go wrong. This is because it's designed to have them listed like "00_File 1", "01_File 2" and so on to keep the items in the intended order. The numbers are then stripped from the resulting output, but I haven't done anything to make it handle the lack of an underscore yet, as it would be nice to get the essential parts working first, and it is only for personal use.
The issue is that I haven't been able to get the resulting URLs quite right. Here's an example of what it produces:
Directory: /directory
Subdirectory: /directory/subdirectory
Subsubdirectory 1: /directory/subdirectory/subsubdirectory-1
File: /directory/subdirectory/subsubdirectory/file
Subsubdirectory 2: /directory/subsubdirectory-2
File: /directory/subsubdirectory/file
Subsubdirectory 3: /directory/subsubdirectory-3
File: /directory/subsubdirectory-3/file
File: /directory/subdirectory/subsubdirectory/file
File: /directory/subdirectory/file
As you might be able to see, the URLs are completely out of whack, often pointing to an incorrect directory and one level deeper than they should after going up a level (or multiple levels).
I'm really not sure where to go from here to try and get out of this mess. Whatever I try either does not result in a successful recursive directory listing, or getting the appropriate hyperlinks in place is impossible.
I would rewrite to something like the following:
Create the directory structure, with DirectoryIterator
Loop over the structured array recursively to create the menu
Ignore ./node_modules/standard it's all I had at hand to test ;p change to suit.
<?php
function file_get_listing($path = '')
{
$return = [];
foreach (new IteratorIterator(new DirectoryIterator($path)) as $item) {
if ($item->isDot()) {
continue;
}
$info = [
'text' => $item->getFilename(),
'href' => str_replace('\\', '/', $item->getPathname())
];
if ($item->isDir()) {
$nodes = file_get_listing($item->getPathname());
if (!empty($nodes)) {
$info['nodes'] = $nodes;
}
}
$return[] = $info;
}
return $return;
}
function makeNav($item) {
$return = '<li>'.$item['text'].''.PHP_EOL;
if (isset($item['nodes']) && is_array($item['nodes']) && count($item['nodes']) > 0) {
$return .= '<ul>'.PHP_EOL;
foreach ($item['nodes'] as $node) {
$return .= makeNav($node);
}
$return .= '</ul>'.PHP_EOL;
} else {
$return .= '</li>'.PHP_EOL;
}
if (isset($item['nodes']) && is_array($item['nodes']) && count($item['nodes']) > 0) {
$return .= "</li>".PHP_EOL;
}
return $return;
}
$nav = '<ul>';
foreach (file_get_listing('./node_modules/standard') as $item) {
$nav .= makeNav($item);
}
echo $nav.'</ul>';
Result
<li>
standard
<ul>
<li>
docs
<ul>
<li>RULES-zhtw.md</li>
<li>RULES-kokr.md</li>
<li>README-iteu.md</li>
<li>RULES-zhcn.md</li>
<li>README-ptbr.md</li>
<li>README-zhtw.md</li>
<li>webstorm.md</li>
<li>RULES-iteu.md</li>
<li>RULES-esla.md</li>
<li>README-esla.md</li>
<li>RULES-ptbr.md</li>
<li>README-kokr.md</li>
<li>README-zhcn.md</li>
</ul>
</li>
<li>SECURITY.md</li>
<li>index.js</li>
<li>LICENSE</li>
<li>RULES.md</li>
<li>eslintrc.json</li>
<li>.travis.yml</li>
<li>AUTHORS.md</li>
<li>package.json</li>
<li>options.js</li>
<li>.editorconfig</li>
<li>
bin
<ul>
<li>cmd.js</li>
</ul>
</li>
<li>README.md</li>
<li>CHANGELOG.md</li>
</ul>
</li>

How to control order of elements in XML created with PHP?

I'm generating an XML document with PHP and the elements have to be in a specific order. Most of them work fine, with the exception of three. The child elements looks like this:
<p>
<co>ABC</co>
<nbr>123456</nbr>
<name>short product description</name>
<desc>long product description</desc>
<kw>searchable keywords</kw>
<i-std>relative/path/to/image</i-std>
<i-lg>relative/path/to/large/image</i-lg>
<i-thmb>relative/path/to/thumbnail</i-thmb>
<mfg>manufacturer</mfg>
<a-pckCont>package contents</a-pckCont>
</p>
The code I'm using works fine, but the three image elements are out of order, which makes the content processor that consumes them choke. What I've tried most lately is this:
$newStd = 0;
foreach ($items as $row => $innerArray) {
$p = $domTree->createElement('p');
$xmlRoot->appendChild($p);
foreach ($innerArray as $innerRow => $value) {
if ($innerRow != 'key') {
if ($value != '') {
echo $innerRow . ' : ' . $value . '<br />';
if ($innerRow == 'i-std') {
$newStd = $domTree->createElement($innerRow, htmlspecialchars($value));
} else {
$p->appendChild($domTree->createElement($innerRow, htmlspecialchars($value)));
}
}
}
if ($newStd != 0) {
$thmb = $p->getElementsByTagName('i-thmb')->item(0);
$p->insertBefore($newStd, $thmb);
}
}
}
My thought was to have it write out all child elements before I have it write the element in using InsertBefore to ensure it appears before the i-thmb element, but it didn't make a difference. No matter what I do, the output I get has them in the order i-thmb, i-std, i-lg. All other elements appear in the proper order, after rearranging some of the variables in the arrays used to build the XML document. I haven't attempted to control the i-lg element yet, since i-std isn't working.
Ultimately, this will be used to combine to XML documents together, but in testing to be sure the XML processor wasn't going to choke, I found the fundamental issue is that the order of elements largely determines if it will work or not (the system I'm working with is undocumented and support is poor, to say the least).
Edit to add: echoing as I do in the inner foreach loop shows them in the correct order but they're out of order in the output file.
I think a couple of changes would make the code more robust (I've added comments to the code for specifics). This mainly involves checking of data types and resetting the replacement field in each loop...
foreach ($items as $row => $innerArray) {
$p = $domTree->createElement('p');
$xmlRoot->appendChild($p);
foreach ($innerArray as $innerRow => $value) {
$newStd = 0; // Make sure this is set each time
if ($innerRow != 'key') {
if ($value != '') {
echo $innerRow . ' : ' . $value . '<br />';
if ($innerRow == 'i-std') {
$newStd = $domTree->createElement($innerRow, htmlspecialchars($value));
} else {
$p->appendChild($domTree->createElement($innerRow, htmlspecialchars($value)));
}
}
}
// Check if a replacement element, checking type of element
if ($newStd instanceof DOMElement) {
$thmb = $p->getElementsByTagName('i-thmb')->item(0);
$p->insertBefore($newStd, $thmb);
}
}
}

Restricting the number of times a function recurses in php codeigniter

I want to generate a multilevel hierarchical select option to select parent or any of its child categories in codeigniter. I want to limit the hierarchy level to 3. i.e the option must show parent, its first level child and its second level child with child category indented to the right of parent. I've no problem going through all the hierarchy level. But I can't generate the select option upto a particular hierarchy level. Three levels of hierarchy is what I need in my case. Can anyone please help me.
Here is what I tried so far:
<?php
$category = $this->get_category();
function get_category() {
$this->db->order_by('parent_id', 'DESC');
$this->db->select('id, name, parent_id');
return $this->db->get('tbl_job_category')->result_array();
}
/*
*$category contains the following json data obtained from DB
$category = [{"id":"20","name":"MVC","parent_id":"16"},
{"id":"16","name":"Spring","parent_id":"14"},
{"id":"12","name":"Car Driver","parent_id":"13"},
{"id":"6","name":"Electrical","parent_id":"5"},
{"id":"3","name":"Civil","parent_id":"5"},
{"id":"14","name":"java","parent_id":"2"},
{"id":"15","name":"javascript","parent_id":"2"},
{"id":"17","name":"Computer Operator","parent_id":"1"},
{"id":"2","name":"Programming","parent_id":"1"},
{"id":"4","name":"Networking","parent_id":"1"},
{"id":"11","name":"Hardware","parent_id":"1"},
{"id":"13","name":"Driver","parent_id":"0"},
{"id":"5","name":"Engineering","parent_id":"0"},
{"id":"19","name":"Economics","parent_id":"0"},
{"id":"1","name":"Information Technology","parent_id":"0"}];
*/
$category_options = $this->multilevel_select($category);
function multilevel_select($array,$parent_id = 0,$parents = array()) {
static $i=0;
if($parent_id==0)
{
foreach ($array as $element) {
if (($element['parent_id'] != 0) && !in_array($element['parent_id'],$parents)) {
$parents[] = $element['parent_id'];
}
}
}
$menu_html = '';
foreach($array as $element){
if($element['parent_id']==$parent_id){
$menu_html .= '<option>';
for($j=0; $j<$i; $j++) {
$menu_html .= '—';
}
$menu_html .= $element['name'].'</option>';
if(in_array($element['id'], $parents)){
$i++;
$menu_html .= $this->multilevel_select($array, $element['id'], $parents);
}
}
}
$i--;
return $menu_html;
}
echo $category_options;
Limiting the recursion involves three steps :
Initializing a counter variable passed as parameter to the recursive function.
Testing the counter value against the desired boundary prior to recursing
Passing an incremented counter value in case of recursive call.
In your case that would be the $level variable below :
function multilevel_select($array,$parent_id = 0,$parents = array(), $level=0) {
// ....
if(in_array($element['id'], $parents) && $level < 2){ // your boundary here, 2 for third nesting level from root
$i++;
$menu_html .= $this->multilevel_select($array, $element['id'], $parents, $level+1);
}
}
}
$i--;
return $menu_html;
}

Flattening array

I have a function which changes an array into the html list (ol/ul). The depth of the array is passed as an argument.
I wanted to do this in just a single function.
for($i = 0; $i < $depth; $i++) {
foreach($list_array as $li) {
if(! is_array($li))
{
$str .= '<li>' . $li . '</li>';
}
}
}
This code gives me the first dimension of the array. I'd like to flatten this array every time the $i increments.
Do you have any suggestions that could be helpful?
And yes, I'm aware of array_walk_recursive(), object iterators etc...I'd like to know if there's a simple way to do this task without using any ot those. I can't come up with anything.
And no, this is not any university project where I'm not allowed to use iterators etc.
EDIT:
print_list(array(
'some first element',
'some second element',
array(
'nested element',
'another nested element',
array(
'something else'
)
)
));
should output something like:
<ul>
<li>some first element</li>
<li>some second element</li>
<ul>
<li>nested element</li>
<li>another nested element</li> // etc
This is probably easiest to accomplish using recursion:
function print_list($array){
echo '<ul>';
// examine every value in the array
// (including values that may also be arrays)
for($array as $val){
if(is_array($val){
// when we discover the value is, in fact, an array
// print it as if it were the top-level array using
// this function
print_list($val);
}else{
// if this is a regular value, print it as a list item
echo '<li>'.$val.'</li>';
}
}
echo '</ul>';
}
If you want to do indentation, you could define a depth tracking parameter and a co-routine (print_list_internal($array, $depth)) or just add a default parameter (print_list($array,$depth=0)) and then print a number of spaces in front of anything depending on $depth.
function print_list($array) {
echo '<ul>';
// First print all top-level elements
foreach ($array as $val) {
if (!is_array($val)) {
echo '<li>'.$val.'</li>';
}
}
// Then recurse into all the sub-arrays
foreach ($array as $val) {
if (is_array($val)) {
print_list($val);
}
}
echo '</ul>';
}

How do I pick only the first few items in an array?

Here's something simple for someone to answer for me. I've tried searching but I don't know what I'm looking for really.
I have an array from a JSON string, in PHP, of cast and crew members for a movie.
Here I am pulling out only the people with the job name 'Actor'
foreach ($movies[0]->cast as $cast) {
if ($cast->job == 'Actor') {
echo '<p>' . $cast->name . ' - ' . $cast->character . '</p>';
}
}
The problem is, I would like to be able to limit how many people with the job name 'Actor' are pulled out. Say, the first 3.
So how would I pick only the first 3 of these people from this array?
OK - this is a bit of over-kill for this problem, but perhaps it serves some educational purposes. PHP comes with a set of iterators that may be used to abstract iteration over a given set of items.
class ActorIterator extends FilterIterator {
public function accept() {
return $this->current()->job == 'Actor';
}
}
$maxCount = 3;
$actors = new LimitIterator(
new ActorIterator(
new ArrayIterator($movies[0]->cast)
),
0,
$maxCount
);
foreach ($actors as $actor) {
echo /*... */;
}
By extending the abstract class FilterIterator we are able to define a filter that returns only the actors from the given list. LimitIterator allows you to limit the iteration to a given set and the ArrayIterator is a simple helper to make native arrays compatible with the Iterator interface. Iterators allow the developer to build chains that define the iteration process which makes them extremely flexible and powerful.
As I said in the introduction: the given problem can be solved easily without this Iterator stuff, but it provides the developer with some extended options and enables code-reuse. You could, for example, enhance the ActorIterator to some CastIterator that allows you to pass the cast type to filter for in the constructor.
Use a variable called $num_actors to track how many you've already counted, and break out of the loop once you get to 3.
$num_actors = 0;
foreach ( $movies[0]->cast as $cast ) {
if ( $cast->job == 'Actor' ) {
echo '...';
$num_actors += 1;
if ( $num_actors == 3 )
break;
}
}
$actors=array_filter($movies[0]->cast, function ($v) {
return $v->job == 'Actor';
});
$first3=array_slice($actors, 0, 3);
or even
$limit=3;
$actors=array_filter($movies[0]->cast, function ($v) use (&$limit) {
if ($limit>0 && $v->job == 'Actor') {
$limit--;
return true;
}
return false;
});
Add a counter and an if statement.
$count = 0;
foreach ($movies[0]->cast as $cast)
{
if ($cast->job == 'Actor')
{
echo '<p>' . $cast->name . ' - ' . $cast-character . '</p>';
if($count++ >= 3)
break;
}
}
$limit = 3;
$count = 0;
foreach ($movies[0]->cast as $cast) {
// You can move the code up here if all you're getting is Actors
if ($cast->job == 'Actor') {
if ($count == $limit) break;// stop the loop
if ($count == $limit) continue;// OR move to next item in loop
$count++;
echo '<p><a href="people.php?id='
. $cast->id
. '">'
. $cast->name
. ' - '
. $cast->character
. '</a></p>';
}
}

Categories