I have an array such as:
$tree = array(
'folder_1' => array(
'folder_1_1',
'folder_1_2' => array(
'folder_1_2_1',
'folder_1_2_2'
),
'folder_1_3'
),
'folder_2' => array(
'folder_2_1' => array(
'folder_2_1_1' => array(
'folder_2_1_1_1',
'folder_2_1_1_2'
)
),
)
);
And I'm trying to build an array of paths:
$paths = array(
'folder_1',
'folder_1/folder_1_1',
'folder_1/folder_1_2',
'folder_1/folder_1_2/folder_1_2_1',
'folder_1/folder_1_2/folder_1_2_2',
'folder_2',
'folder_2/folder_2_1',
...
);
I can't seem to find a way to achieve this. The problem I encounter is that folder names can be array keys, but also array elements.
This is what I have done so far, but I'm not near a solution...
$paths = transform_tree_to_paths($trees);
function transform_tree_to_paths($trees, $current_path = '', $paths = array())
{
if (is_array($trees)) {
foreach ($trees as $tree => $children) {
$current_path .= $tree . '/';
return transform_tree_to_paths($children, $current_path, $paths);
}
$paths[] = $current_path;
$current_path = '';
} else {
$paths[] = $trees;
}
return $paths;
}
How about something like this?
function gen_path($tree, $parent=null) {
$paths = array();
//add trailing slash to parent if it is not null
if($parent !== null) {
$parent = $parent.'/';
}
//loop through the tree array
foreach($tree as $k => $v) {
if(is_array($v)) {
$currentPath = $parent.$k;
$paths[] = $currentPath;
$paths = array_merge($paths, gen_path($v, $currentPath));
} else {
$paths[] = $parent.$v;
}
}
return $paths;
}
You were headed in the right direction, but missed the mark a bit. The return statement before the recursive function call in your function caused everything after the foreach loop to never be called.
Here's another solution, utilizing RecursiveArrayIterator and RecursiveIteratorIterator:
function generatePaths( array $tree ) {
$result = array();
$currentPath = array();
$rii = new RecursiveIteratorIterator( new RecursiveArrayIterator( $tree ), RecursiveIteratorIterator::SELF_FIRST );
foreach( $rii as $key => $value ) {
if( ( $currentDepth = $rii->getDepth() ) < count( $currentPath ) ) {
array_splice( $currentPath, $currentDepth );
}
$currentPath[] = is_array( $value ) ? $key : $value;
$result[] = implode( '/', $currentPath );
}
return $result;
}
PS.: Baconics' solution appears to be about twice as fast as mine, though.
Related
I want to get an array of immediate sub-directories based on the current path from a given list of paths. Here is an example list of paths and the desired result:
$dirs = [
'/chris',
'/chris/usa',
'/david',
'/',
'/chris/canada/2022',
'/david/uk',
];
$current_path = "/chris";
// Required result:
$sub_dirs = ['/usa', '/canada'];
The current path could be anything from / to the last sub directory, in which case I would end up with an empty $sub_dirs.
My best attempt at it is this:
$dirs = [
'/chris',
'/chris/usa',
'/david',
'/',
'/chris/canada/2022',
'/david/uk',
];
$sub_dirs = [];
$current_path = "/";
foreach($dirs as $dir){
if(strstr($dir, $current_path)){
$sub_dirs[] = str_replace($current_path, '', $dir);
}
}
The above fails at two things. If the $current_path = "/" then the returned array is just paths without slashes since I strip them out with strstr() and if the $current_path = "/chris" then I still get the sub-directories.
How should I correctly solve it?
Thank you!
You can search for /chris/ to get only sub directories.
To get only the first directory of matching paths, you could use explode(), and get the first.
As of PHP8, you can use str_starts_with()
$dirs = ['/chris', '/chris/usa', '/david', '/', '/chris/canada/2022', '/david/uk'];
$current_path = "/";
$sub_dirs = [];
foreach($dirs as $dir)
{
// Ensure $dir starts with path + '/' (sub dir of $current_path)
if (!str_starts_with($dir, $current_path)) {
continue;
}
// Get the relative path from $current_path
$relPath = substr($dir, strlen($current_path));
// Get the first dir of this path
[$dir2] = explode('/', ltrim($relPath,'/'));
// skip same directory
if (empty($dir2)) {
continue;
}
// Store and add removed slash
$sub_dirs[] = '/' . $dir2;
}
var_export(array_unique($sub_dirs));
Output
array (
0 => '/usa',
1 => '/canada',
)
I like the approach of creating a tree out of the paths (adapted from this answer).
Then it's easier to get the children based on a path.
function buildTree($paths)
{
// Create a hierarchy where keys are the labels
$rootChildren = [];
foreach ($paths as $path) {
$branch = explode("/", $path);
$children = &$rootChildren;
foreach ($branch as $label) {
if ($label) {
if (!isset($children[$label])) {
$children[$label] = [];
}
$children = &$children[$label];
}
}
}
// Create target structure from that hierarchy
function recur($children)
{
$result = [];
foreach ($children as $label => $grandchildren) {
$result[$label] = recur($grandchildren);
}
return $result;
}
return recur($rootChildren);
}
function findChildren($tree, $path)
{
$branch = explode("/", $path);
$pointer = $tree;
foreach ($branch as $label) {
if ($label) {
$pointer = $pointer[$label];
}
}
return array_keys($pointer);
}
$dirs = [
'/chris',
'/chris/usa',
'/david',
'/',
'/chris/canada/2022',
'/david/uk',
];
$tree = buildTree($dirs);
print_r($tree);
$current_path = "/chris";
print_r(findChildren($tree, $current_path));
Output:
Array
(
[chris] => Array
(
[usa] => Array
(
)
[canada] => Array
(
[2022] => Array
(
)
)
)
[david] => Array
(
[uk] => Array
(
)
)
)
Array
(
[0] => usa
[1] => canada
)
I feel like I have a bit shorter answer than the previous ones, but not sure if it's the most efficient one though.
$dirs = [
'/chris',
'/chris/usa',
'/david',
'/',
'/chris/canada/2022',
'/david/uk',
];
$current_path = "/chris";
$sub_dirs = array_map(function($s) { return '/'.explode("/", $s)[2]; },array_filter($dirs, function($dir) use ($current_path) {
return substr($dir, 0, strlen($current_path) + 1) === $current_path . '/';
}));
print_r($sub_dirs); // Output: Array ( [1] => /usa [4] => /canada )
I'm trying to build a tree-map from categories.
I have the categories (I have a lot of categories and I want to remove duplicates and show them in a tree-map view)
$cat = array(
"Sneakers/Men",
"Sneakers/Women",
"Accessories/Jewellery/Men",
"Accessories/Jewellery/Women",
"Accessories/Jewellery/Men
");
...and I want them like this
$categories = array(
"Sneakers" => array(
"Men" => array(),
"Women" => array()
),
"Accessories" => array(
"Jewellery" => array(
"Men" => array(),
"Women" => array()
)
)
);
to print them like this
- Sneakers
-- Men
-- Women
- Accessories
-- Jewellery
--- Men
--- Women
Try this:
<?php
$cat = array(
"Sneakers/Men",
"Sneakers/Women",
"Accessories/Jewellery/Men",
"Accessories/Jewellery/Women",
"Accessories/Jewellery/Men
");
function buildTree($categories, $result = []){
$temp = [];
foreach($categories as $categoryString){
$catParts = explode('/',$categoryString);
if(count($catParts) > 1){
$temp[$catParts[0]][] = str_replace($catParts[0].'/','',$categoryString);
} else {
$temp[$catParts[0]] = [];
}
}
foreach($temp as $elemName => $elemVal){
$result[$elemName] = buildTree($elemVal);
}
return $result;
}
var_dump(buildTree($cat));
The most simple way is to use references, like this:
$out = [];
foreach ($cat as $str) {
$lookup =& $out;
foreach (explode("/", $str) as $part) {
$lookup =& $lookup[$part];
if (!isset($lookup)) {
$lookup = [];
}
}
}
$lookup initially refers to the whole expected result, then the reference is extended at each step to follow the path of nested members.
Note that each new member added looks like member-name => [], so that actually even final leaves are arrays: it may seem a bit weird, but is a pretty way to have a reduced code (each member is always ready to receive children).
And it's not a difficulty, though, to use the resulting array to then print it like the OP asked:
function nest_print($src, $level = 0) {
$prefix = '<br />' . str_repeat('- ', ++$level);
foreach ($src as $key => $val) {
echo $prefix . $key;
if ($val) {
nest_print($val, $level);
}
}
}
nest_print($out);
EDIT
Here is an alternate solution, including the count of final leaves, as asked by the OP in his comment:
$out = [];
foreach ($cat as $str) {
$lookup =& $out;
$parts = explode("/", $str);
foreach ($parts as $part) {
$lookup =& $lookup[$part];
if (!isset($lookup)) {
$lookup = [];
}
// when $part is a final leaf, count its occurrences
if ($part == end($parts)) {
$lookup = is_array($lookup) ? 1 : ++$lookup;
}
}
}
(might likely be improved in a more elegant way, though)
And here is how to modify the print-result snippet accordingly:
function nest_print($src, $level = 0) {
$prefix = '<br />' . str_repeat('- ', ++$level);
foreach ($src as $key => $val) {
echo $prefix . $key;
if (is_array($val)) {
nest_print($val, $level);
} else {
echo ': ' . $val;
}
}
}
nest_print($out);
I want create a nested array from a config file dynamically.
My config file structure is like this:
parameter1 value1;
parameter2 value2;
parameter3 value3;
block1{
parameter1-1 value1-1;
parameter1-2 value1-2;
block1-1{
parameter1-1-1 value1-1-1;
parameter1-1-2 value1-1-2;
block1-1-1{
parameter1-1-1-1 value1-1-1-1;
parameter1-1-1-2 value1-1-1-2;
}
block1-1-2{
parameter1-1-2-1 value1-1-2-1;
parameter1-1-2-2 value1-1-2-2;
}
}
block1-2{
parameter1-2-1 value1-2-1;
parameter1-2-2 value1-2-2;
block1-2-1{
parameter1-2-1-1 value1-2-1-1;
parameter1-2-1-2 value1-2-1-2;
}
block1-2-2{
parameter1-2-2-1 value1-2-2-1;
parameter1-2-2-2 value1-2-2-2;
}
}
}
block2{
parameter2-1 value2-1;
parameter2-2 value2-2;
block2-1{
parameter2-1-1 value2-1-1;
parameter2-1-2 value2-1-2;
block2-1-1{
parameter2-1-1-1 value2-1-1-1;
parameter2-1-1-2 value2-1-1-2;
}
block2-1-2{
parameter2-1-2-1 value2-1-2-1;
parameter2-1-2-2 value2-1-2-2;
}
}
block2-2{
parameter2-2-1 value2-2-1;
parameter2-2-2 value2-2-2;
block2-2-1{
parameter2-2-1-1 value2-2-1-1;
parameter2-2-1-2 value2-2-1-2;
}
block2-2-2{
parameter2-2-2-1 value2-2-2-1;
parameter2-2-2-2 value2-2-2-2;
}
}
}
and i want this array dynamically in php:
$blocks = array(
$parameter => $value,
$parameter => $value,
$block => array(
$parameter => $value,
$parameter => $value,
$block => array(
$parameter => $value,
$parameter => $value,
$block => array(
$parameter => $value,
$parameter => $value
...
)
)
)
);
How to create dynamically nested array in PHP
Thanks.
UPDATE:
I read file line by line to an array and i want create above structure for edit it and write it again to config file.
I means from "dynamic" is creating array inside "for" or other similar things.
Try this... (change "conf.txt" to "your conf file name")
<?php
$file = fopen("conf.txt","r");
$array = array();
$a = createArray($file, $array);
fclose($file);
print("<pre>".print_r($a,true)."</pre>");
function createArray($file, $array){
while(! feof($file)){
$line = fgets($file);
$line = trim($line);
if ($line == ""){
continue;
}
if (strpos($line,'{') !== false){
$line = trim(str_replace('{','',$line));
$array[$line] = array();
$array[$line] = createArray($file, $array[$line]);
} else if (strpos($line,'}') !== false) {
return $array;
} else {
$line = str_replace(';','',$line);
$key = strtok($line, ' ');
$value = strtok(' ');
$array[$key] = $value;
}
}
return $array;
}
?>
Creating array dynamically:
in all PHP versions:
$foo = array();
in PHP 5.4+ (aka "short syntax"):
$foo = [];
Creating nested array dynamically:
$foo = [];
$foo['bar'] = [];
$foo['bar']['zoo'] = [];
or
$foo = [];
$bar = ['zoo' => []];
$foo['bar'] = $bar;
And finally call
print_r($foo);
to see what you got created. Both cases are equal so you will see:
Array
(
[bar] => Array
(
[zoo] => Array
(
)
)
}
Please see docs on array: http://php.net/manua/en/language.types.array.php
I try to make a tree list in PHP from a hierarchy stored in a concatenated string in my mysql database
This my table :
and I'd like to reproduce something like this :
<ul>
<li>
<ul>
<li></li>
<li>
<ul>
<li></li>
<li></li>
</ul>
</li>
<li></li>
</ul>
</li>
<li></li>
<li></li>
</ul>
I know I have to use a recursive function I don't reach to do...
Maybe someone could help me
code without comments
see the usage and dataset section below to see what you need to pass and how to use these functions:
function items_to_tree( $items ){
$array = array();
foreach( $items as $item ) {
$parts = explode('.', $item['hierarchy']);
$last = array_pop( $parts );
$cursor = &$array;
foreach ( $parts as $part ) {
if ( !is_array($cursor[$part]) ) {
$cursor[$part] = array();
}
$cursor = &$cursor[$part];
}
$cursor[$last]['#item'] = $item;
}
return $array;
}
function tree_to_ul( $tree ){
$html = $children = '';
foreach( $tree as $key => $item ){
if ( substr($key,0,1) == '#' ) continue;
$children .= tree_to_ul( $item );
}
if ( isset($tree['#item']) ) {
$html .= '<li>' . PHP_EOL;
$html .= '<em>' . $tree['#item']['menu_text'] . '</em>' . PHP_EOL;
$html .= ( $children ? '<ul>' . $children . '</ul>' . PHP_EOL : '' );
$html .= '</li>' . PHP_EOL;
return $html;
}
else {
return $children;
}
}
code with comments and explanation
The code to convert your items to a tree structure:
function items_to_tree( $items ){
$array = array();
foreach( $items as $item ) {
/// split each hierarchy string into it's dot separated parts
$parts = explode('.', $item['hierarchy']);
/// pop off the last item of the array, we'll use this for assignment later
$last = array_pop( $parts );
/// create a reference to our position in the array we wish to fill out
$cursor = &$array;
/// step each hierarchy part and travel down the array structure,
/// just like you would if you typed an array path manually.
/// i.e. $array[$part][$part][...] and so on
foreach ( $parts as $part ) {
/// if at this point in the array, we don't have an array, make one.
if ( !is_array($cursor[$part]) ) {
$cursor[$part] = array();
}
/// ready for the next step, shift our reference to point to the next
/// $part in the array chain. e.g. if $cursor pointed to `$array[$part]`
/// before, after the next line of code the $cursor will point
/// to `$array[$oldpart][$part]`
$cursor = &$cursor[$part];
}
/// we popped the last item off the $parts array so we could easily
/// assign our final value to where the $cursor ends up pointing to.
/// starting with a hierarchy of '00001.00002.00003' would mean at this
/// point $cursor points to $array['00001']['00002'] and $last = '00003';
/// so finally we get $array['00001']['00002']['00003']['#item'] = $item;
$cursor[$last]['#item'] = $item;
/// use '#item' to keep our item's information separate from it's children.
}
/// return our built up array.
return $array;
}
The code to convert the tree structure to a UL:
function tree_to_ul( $tree ){
/// start with nothing
$html = $children = '';
/// step each item found in the current level of $tree
foreach( $tree as $key => $item ){
/// if the item's key starts with a # skip, these contain
/// our item's information and should not be treated as children
if ( substr($key,0,1) == '#' ) continue;
/// recurse this function so that we do the same for any child # any level.
$children .= tree_to_ul( $item );
}
/// if at this level a #item has been set, use this item information to
/// add a title to our level. You could change this to add whatever info
/// from your original database item that you'd like.
if ( isset($tree['#item']) ) {
$html .= '<li>' . PHP_EOL;
$html .= '<em>' . $tree['#item']['menu_text'] . '</em>' . PHP_EOL;
$html .= ( $children ? '<ul>' . $children . '</ul>' . PHP_EOL : '' );
$html .= '</li>' . PHP_EOL;
return $html;
}
/// if there wasn't an item, just return the traversed children.
else {
return $children;
}
}
dataset:
/// I simplified your dataset to an array, this could easily be generated
/// from a database query. You could also convert my code so that you
/// don't have to pre-generate an array, and instead could process after
/// each fetch from the database.
$items = array(
array('hierarchy' => '00001', 'menu_text' => 'One'),
array('hierarchy' => '00002', 'menu_text' => 'Two'),
array('hierarchy' => '00002.00001', 'menu_text' => 'Three'),
array('hierarchy' => '00002.00002', 'menu_text' => 'Four'),
array('hierarchy' => '00002.00003', 'menu_text' => 'Five'),
array('hierarchy' => '00002.00004', 'menu_text' => 'Six'),
array('hierarchy' => '00003', 'menu_text' => 'Seven'),
array('hierarchy' => '00003.00001', 'menu_text' => 'Eight'),
array('hierarchy' => '00003.00001.00001', 'menu_text' => 'Nine'),
array('hierarchy' => '00003.00001.00002', 'menu_text' => 'Ten'),
array('hierarchy' => '00003.00001.00003', 'menu_text' => 'Eleven'),
array('hierarchy' => '00003.00002', 'menu_text' => 'Twelve'),
);
usage:
/// Simple usage :) if a little complex explanation
$tree = items_to_tree( $items );
$html = tree_to_ul( $tree );
echo $html;
in the interests of codegolf ;)
The following could replace my items_to_tree function -- however it isn't advised.
$a = array();
foreach($items as $i){
eval('$a["'.str_replace('.','"]["',$i['hierarchy']).'"]=array("#item"=>$i);');
}
$refs = new stdClass();
//Assuming $data is the result array of your query to fetch the data.
foreach($data as $result)
{
$name = $result['hierarchy'];
$parent = substr($result['hierarchy'],0,strrpos($result['hierarchy'],'.'));
$thisref = &$refs->{$name};
foreach($result as $k => $v)
{
$thisref->{$k} = $v;
}
if ($parent == '') {
$tree->{$name} = &$thisref;
} else {
$refs->{$parent}->children->{$name} = &$thisref;
}
}
This will give you a nice object with every node's child in the property children.
function drawUL($level){
echo '<ul>';
foreach($level as $li){
echo '<li>'.$li->label;
if(isset($li->children))drawUl($li->children);
echo '</li>';
}
echo '</ul>';
}
drawUl($tree);
I am trying to retrieve all images in a directory, including all subdirectories. I am currently using
$images = glob("{images/portfolio/*.jpg,images/portfolio/*/*.jpg,images/portfolio/*/*/*.jpg,images/portfolio/*/*/*/*.jpg}",GLOB_BRACE);
This works, however the results are:
images/portfolio/1.jpg
images/portfolio/2.jpg
images/portfolio/subdirectory1/1.jpg
images/portfolio/subdirectory1/2.jpg
images/portfolio/subdirectory2/1.jpg
images/portfolio/subdirectory2/2.jpg
images/portfolio/subdirectory1/subdirectory1/1.jpg
images/portfolio/subdirectory1/subdirectory1/2.jpg
I want it to do a whole directory branch at a time so the results are:
images/portfolio/1.jpg
images/portfolio/2.jpg
images/portfolio/subdirectory1/1.jpg
images/portfolio/subdirectory1/2.jpg
images/portfolio/subdirectory1/subdirectory1/1.jpg
images/portfolio/subdirectory1/subdirectory1/2.jpg
images/portfolio/subdirectory2/1.jpg
images/portfolio/subdirectory2/2.jpg
Greatly appreciate any help, cheers!
P.S It would also be great if I could just get all subdirectories under portfolio without having to specifically state each directory with a wild card.
from glob example
if ( ! function_exists('glob_recursive'))
{
// Does not support flag GLOB_BRACE
function glob_recursive($pattern, $flags = 0)
{
$files = glob($pattern, $flags);
foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir)
{
$files = array_merge($files, glob_recursive($dir.'/'.basename($pattern), $flags));
}
return $files;
}
}
Solution:
<?php
$path = realpath('yourfolder/examplefolder');
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)) as $filename)
{
echo "$filename</br>";
}
?>
Here's a simpler approach:
Instead of using:
$path = realpath('yourfolder/examplefolder/*');
glob($path);
You'll have to use:
$path = realpath('yourfolder/examplefolder').'/{**/*,*}';
glob($path, GLOB_BRACE);
This last one will use bracing, and it is, in fact, a shorthand for this code:
$path = realpath('yourfolder/examplefolder');
$self_files = glob($path . '/*');
$recursive_files = glob($path . '/**/*');
$all_files = $self_files + $recursive_files; // That's the result you want
You may also want to filter directories from your result. glob() function has GLOB_ONLYDIR flag. Let's use it to diff out our result.
$path = realpath('yourfolder/examplefolder/') . '{**/*,*}';
$all_files = array_diff(
glob($path, GLOB_BRACE),
glob($path, GLOB_BRACE | GLOB_ONLYDIR)
);
This function supports GLOB_BRACE:
function rglob($pattern_in, $flags = 0) {
$patterns = array ();
if ($flags & GLOB_BRACE) {
$matches;
if (preg_match_all ( '#\{[^.\}]*\}#i', $pattern_in, $matches )) {
// Get all GLOB_BRACE entries.
$brace_entries = array ();
foreach ( $matches [0] as $index => $match ) {
$brace_entries [$index] = explode ( ',', substr ( $match, 1, - 1 ) );
}
// Create cartesian product.
// #source: https://stackoverflow.com/questions/6311779/finding-cartesian-product-with-php-associative-arrays
$cart = array (
array ()
);
foreach ( $brace_entries as $key => $values ) {
$append = array ();
foreach ( $cart as $product ) {
foreach ( $values as $item ) {
$product [$key] = $item;
$append [] = $product;
}
}
$cart = $append;
}
// Create multiple glob patterns based on the cartesian product.
foreach ( $cart as $vals ) {
$c_pattern = $pattern_in;
foreach ( $vals as $index => $val ) {
$c_pattern = preg_replace ( '/' . $matches [0] [$index] . '/', $val, $c_pattern, 1 );
}
$patterns [] = $c_pattern;
}
} else
$patterns [] = $pattern_in;
} else
$patterns [] = $pattern_in;
// #source: http://php.net/manual/en/function.glob.php#106595
$result = array ();
foreach ( $patterns as $pattern ) {
$files = glob ( $pattern, $flags );
foreach ( glob ( dirname ( $pattern ) . '/*', GLOB_ONLYDIR | GLOB_NOSORT ) as $dir ) {
$files = array_merge ( $files, rglob ( $dir . '/' . basename ( $pattern ), $flags ) );
}
$result = array_merge ( $result, $files );
}
return $result;
}
Simple class:
<?php
class AllFiles {
public $files = [];
function __construct($folder) {
$this->read($folder);
}
function read($folder) {
$folders = glob("$folder/*", GLOB_ONLYDIR);
foreach ($folders as $folder) {
$this->files[] = $folder . "/";
$this->read( $folder );
}
$files = array_filter(glob("$folder/*"), 'is_file');
foreach ($files as $file) {
$this->files[] = $file;
}
}
function __toString() {
return implode( "\n", $this->files );
}
};
$allfiles = new AllFiles("baseq3");
echo $allfiles;
Example output:
baseq3/gfx/
baseq3/gfx/2d/
baseq3/gfx/2d/numbers/
baseq3/gfx/2d/numbers/eight_32b.tga
baseq3/gfx/2d/numbers/five_32b.tga
baseq3/gfx/2d/numbers/four_32b.tga
baseq3/gfx/2d/numbers/minus_32b.tga
baseq3/gfx/2d/numbers/nine_32b.tga
If you don't want the folders in the list, just comment this line out:
$this->files[] = $folder . "/";