p2p.wrox.com Forums

Need to download code?

View our list of code downloads.


  Return to Index  

beginning_php thread: reading dir structure ... not consistent.


Message #1 by "Dirk Bruins" <dbruins@i...> on Wed, 26 Feb 2003 20:16:26
I'm creating a super simple flat file database and am placing products 
into categories, which are created using directories.

The user is able to add, delete and change the names of categories (saved 
as directories on the server).

The problem is when I use a script to read the directory structure to 
display the categories and products, it returns inconsistent results.

To try to get around this problem, all category directories are prepeneded 
with the letter "x" then 3 numbers.  In theory, x001Tables, then 
x002Chairs and so forth should be read in sequence.  But when the user 
changes the name of a category/directory, it is read in an order 
irregardless of the prepended x###.

For example, go to the link below and change the Chairs category to 
Chairs2, then go back to the Main Menu.  Chairs2 will shift below the 
Silver Wear category even though it is still x002.

To change the name of the category, click on the pencil on the far left.  
(One strange thing is you have to click the Submit button with your mouse, 
pressing enter on the keyboard doesn't work ... but that's another 
problem.)

http://www.intellectease.com/test/products4/database/products_editor.php

You can view the directory structure by viewing the database dir.


Thanks for your time!

Dirk Bruins.
Message #2 by "Nikolai Devereaux" <yomama@u...> on Wed, 26 Feb 2003 12:20:08 -0800
> I'm creating a super simple flat file database and am placing products
> into categories, which are created using directories.

How are you reading in the directory?  Can you show your code?

read_dir() doesn't guarantee any sort of sorting on the directory/file names
that it returns.  It returns items in the order that the filesystem gives.
It seems that this ordering is affected by modifications to a file
(including it's name).

If you need to guarantee alphabetical order of your categories, then sort
the array before using it.


Take care,

Nik


Message #3 by "Dirk Bruins" <dbruins@i...> on Wed, 26 Feb 2003 22:04:34
Nik,

I'm using a modified version of the nav_dir.php scipt found in ch 10 of 
the book.  Here is the code as it appears in the book (I just made it look 
like the link above with extra code):

<?php
//nav_dir.php
$default_dir = "/home/james";
function traverse_dir($dir) {
   echo "Traversing $dir....<BR>";
   chdir($dir);
   if(!($dp = opendir($dir))) die("Can't open $dir.");

   while($file = readdir($dp)) {
      if(is_dir($file)) {
         if($file != '.' && $file != '..') {
            echo "/$file<BR>";
            traverse_dir("$dir/$file");
            chdir($dir);
         }
      }
      else echo "$file<BR>";
      
   }
   
   closedir($dp);
}

traverse_dir($default_dir);

?>





Problem is there are no arrays to alphabetize, it echos as it traverses 
the directories.  I'm thinking some sort of multidimensional array can be 
generated, but I can't figure it out.

Thanks,
Dirk
Message #4 by "Nikolai Devereaux" <yomama@u...> on Wed, 26 Feb 2003 14:33:11 -0800

> Problem is there are no arrays to alphabetize, it echos as it traverses
> the directories.  I'm thinking some sort of multidimensional array can be
> generated, but I can't figure it out.

Have the function return the array.  When you encounter a directory from
within traverse_dir, the nested array is created with a recursive call to
traverse_dir.  The return value of the recursive call is assigned to the
$dir index of the result array.

The first thing you need to do is decide how you want to structure your dir
array.

Here's one way.  Consider this dir structure:

/
+ - subdir
| +-- filename.a
| +-- filename.b
| +-- filename.c
|
+-- filename.1
+-- filename.2
+-- filename.3


The resulting array might look like this:

dir_array: Array
(
   [subdir]  =>   Array
   (
       [0]  =>  "filename.a"
       [1]  =>  "filename.b"
       [2]  =>  "filename.c"
   )
   [0]  =>   "filename.1"
   [1]  =>   "filename.2"
   [2]  =>   "filename.3"
)


There is a gotcha with this is, though -- what happens when you have a
directory who's name is an integer?


It's really no big deal; a subdirectory key/value pair in the dir_array is
not one who's key is non-numeric, it's one who's value is an array.



Lemme rewrite your code:

<?php  //nav_dir.php

$default_dir = "/home/james";

function traverse_dir($dir)
{
   $dir_array = array();

   if($dp = opendir($dir))
   {
      while(false !== ($file = readdir($dp)))
      {
         if(is_dir($file) && ($file != '.') && ($file != '..'))
         {
             echo "/$file<BR>";
             $dir_array[$file] = traverse_dir("$dir/$file");
         }
         else
         {
             $dir_array[] = $file;
         }
      }

      closedir($dp);
   }

   return $dir_array;
}

$dirs = traverse_dir($default_dir);

echo '$dirs is: ';
print_r($dirs);

?>



Also note that this isn't tested, though I see no reason why it shouldn't
work. =)


Hope this helps,

Nik

Message #5 by "Nikolai Devereaux" <yomama@u...> on Wed, 26 Feb 2003 15:08:49 -0800

> Also note that this isn't tested, though I see no reason why it shouldn't
> work. =)

Actually, i take that back.  More thorough testing has showed me that your
code was buggy, and so was mine.

I didn't think you needed the call to chdir(), since you were building up
the path string by recursively calling traverse_dir("$dir/$file").

HOWever, you did chdir($dir), which means when you opendir($dir), the $dir
in the $opendir is relative to $dir (since $dir is the new current working
directory, or "cwd").

This doesn't matter much with absolute paths, but consider passing ".." to
the function.  You'll chdir(".."), then opendir("..").  Which means you're
effectively running the function for "../..", which is wrong.


I rewrote everything to perform some more robust cwd handling and included
enough comments that you should be able to follow things along.

I also tacked on a form so you can submit whatever directories you want to
run the function on.

Keep in mind that this function does _NOT_ handle recursive directory
structures (via symlinks to dirs higher up in the structure).


That said, here's everything you need to play with it:


<?php  // nav_dir.php

function print_r($var, $desc = '')
{
   echo "<pre>\n";
   if($desc != '') echo "$desc: ";
   print_r($var);
   echo "</pre>\n";
}


function traverse_dir($dir)
{
   $cwd = getcwd();  // need to chdir() back to this dir later to avoid
                     // side-effects during call (especially when called
   chdir($dir);

   $dir_array = array();

   if($dp = opendir(getcwd())) // don't opendir($dir), because $dir is
relative
                               // to cwd, which you changed in chdir($dir).
   {
      while(false !== ($file = readdir($dp))) // see
http://www.php.net/readdir
                                              // for more info about false
!==
      {
         if(is_dir($file))
         {
             if(($file != '.') && ($file != '..'))
             {
                $dir_array[$file] = traverse_dir("$file"); // build nested
array
             }
         }
         else
         {
             $dir_array[] = $file;                        // normal scalar
value
         }
      }

      closedir($dp);
   }

   chdir($cwd);         // revert to original cwd state before function
call.

   return $dir_array;
}

$form_dir = '';
if(isset($_GET['dir']))
{
    $form_dir = $_GET['dir'];
    $dirs = traverse_dir($_GET['dir']);
    printr($dirs, "\$dirs when dir is $_GET[dir] (cwd is " . getcwd() .
')');
    echo "<hr />\n";
}

echo <<<EOF
<form method="get" action="$_SERVER[PHP_SELF]">
  <input type="text" name="dir" value="$form_dir" />
  <input type="submit" value="Go" />
</form>

EOF;
?>




hth,

nik

Message #6 by "Nikolai Devereaux" <yomama@u...> on Wed, 26 Feb 2003 15:20:56 -0800
A crap, i messed up again.

> function print_r($var, $desc = '')

should be

function printr($var, $desc = '') // no _ in func name.


This function is defined in a debug library that I require_once(), but since
you don't have my debug library I just typed the gist of the function
instead of the require_once() call, and fudged it.


take care,

Nik

Message #7 by "Dirk Bruins" <dbruins@i...> on Thu, 27 Feb 2003 21:01:14
YA!  You rule Nik!

Thanks so much ... I'm back on the road.

Just in case anyone cares, I'm echoing the multidimensional with this code:



echocat($dirs);
function echocat($cat)
 {
  while(list($entry) = each($cat))
   {
    $start = substr($entry, 0, 1);   // Make sure the entry starts with a 
y for categories or an x for products.
    $belongs = substr($entry, 1, 3);  // Make sure the category or product 
is numbered.
    $ext = substr($entry, 4);  // Make sure the products file has a .php 
extension.
// These three checks will ensure that no other files are inlcuded on 
accident.
    if($start==y && $belongs>0 && $belongs<999)
     {
      echo "<br>Category: $entry";
      echocat($cat[$entry]);    // Recursive echo call to nav the 
multidimensional array.
     }
    if($start==x && $ext==".php" && $belongs>0 && $belongs<999)
     {
      echo "<br>Product: $entry";
     }
   }
 }

Message #8 by "Nikolai Devereaux" <yomama@u...> on Thu, 27 Feb 2003 13:01:37 -0800
If I may offer a couple suggestions:

Writing some utilty functions would be extremely beneficial here.  First of
all, should you ever decide to change how a category or product are listed,
you'll only have to change the body of these functions, and not anywhere
else they're used.. second, the code is much more readable.

Check it out:

function isCategory($name)
{
    return ($name[0] == 'y');
}

function isProduct($name)
{
    return ($name[0] == 'x');
}

function extensionOf($filename)
{
   return strrchr($filename, '.');
}



Now your code looks like this:

Message #9 by "Nikolai Devereaux" <yomama@u...> on Thu, 27 Feb 2003 13:07:35 -0800
Oops!  sent prematurely.  Lemme try again, from the top:



If I may offer a couple suggestions:

Writing some utilty functions would be extremely beneficial here.  First of
all, should you ever decide to change how a category or product are listed,
you'll only have to change the body of these functions, and not anywhere
else they're used.. second, the code is much more readable.

Check it out:

function isCategory($name)
{
    return ($name[0] == 'y');
}

function isProduct($name)
{
    return ($name[0] == 'x');
}

function numberOf($name)
{
    return (int)substr($name, 1, 3);
}

function extensionOf($filename)
{
   return strrchr($filename, '.');
}



Now your code looks like this:

function echocat($cat)
{
    while(list($entry) = each($cat))
    {
        if (isCategory($cat)         &&
            (numberOf($cat) > 0)     &&
            (numberOf($cat) < 999))
        {
            echo "<br>Category: $entry";
            echocat($cat[$entry]);    // Recursive call to nav the
                                      // multidimensional array.
        }
        else if (isProduct($cat)              &&
                (extensionOf($cat) == ".php") &&
                (numberOf($cat) > 0)          &&
                (numberOf($cat) < 999))
        {
            echo "<br>Product: $entry";
        }
    }
}


Take care,

Nik

Message #10 by "Dirk Bruins" <dbruins@i...> on Fri, 28 Feb 2003 03:11:16
YES!  This helps too ... I'm not used to looking too far in the future 
yet ... a skill I'm working on.

I did make one change though:

function extensionOf($filename)
{
   return (strrchr($filename, '.')==".php");
}

This way I can change the extension in one place too!

THANKS!

Dirk
Message #11 by "Nikolai Devereaux" <yomama@u...> on Thu, 27 Feb 2003 19:54:06 -0800
> YES!  This helps too ... I'm not used to looking too far in the future
> yet ... a skill I'm working on.
>
> I did make one change though:
>
> function extensionOf($filename)
> {
>    return (strrchr($filename, '.')==".php");
> }
>
> This way I can change the extension in one place too!


Well... not quite.

See, the function is called "extensionOf()", so you'd think that it would
return the extension of the file.

By changing what the function DOES, you're invalidating the name of the
function, which is your (and anyone else who reads your code) only clue as to
what it does.

If you want to make sure it's a PHP file, then write another function to check
that.

// returns true if $filename ends with ".php" (case insensitive)
function isPHPfile($filename)
{
   return 0 == strcasecmp(extensionOf($filename), '.php');
}


See how isPHPfile() uses extensionOf()?  That's the beauty of small useful
functions.


Take care,

Nik

Message #12 by "Dirk Bruins" <dbruins@i...> on Fri, 28 Feb 2003 20:45:29
Gotcha.  Why not just change the name of the function ... to something 
like this:

function isPHPfile($filename)
{
 return (strrchr($filename, '.')==".php" || strrchr($filename, '.')
==".PHP" );
}


What does this part mean?  Specifically the "0 == " part.

0 == strcasecmp(extensionOf($filename), '.php')

Thanks Nik!

(BTW, I just bought Beginning Visual C++ and love it!)
Message #13 by "Nikolai Devereaux" <yomama@u...> on Fri, 28 Feb 2003 12:43:00 -0800
> Gotcha.  Why not just change the name of the function ... to something
> like this:
>
> function isPHPfile($filename)
> {
>     return (strrchr($filename, '.')==".php" ||
              strrchr($filename, '.')==".PHP" );
> }


Well, that's fine, but not (imho) ideal.  First of all, you're calling
strrchr() twice on the same string, with the same parameters.  That's a
waste.  It'd be more efficient if you only called it once.

Second, you're saying that the ONLY valid php files are those that end in
".PHP" or ".php".  What about ".Php"?  That won't pass your test.


The function I wrote, extensionOf() simply wraps a call to strrchr() to
return the substring from the last decimal point to the end of the filename.

isPHPfile() needs to compare a file extension against ".php", so why not use
the function that already returns the file extension?

Moving on...


> What does this part mean?  Specifically the "0 == " part.
>
> 0 == strcasecmp(extensionOf($filename), '.php')


For that, you should read the PHP manual.  Get into the habit -- 99% of all
your questions can be answered in there.

  http://www.php.net/strcasecmp


This function performs a case-insensitive comparison on the two strings, and
returns 0 if they're equal.  This is the easiest way to guarantee that
isPHPfile() returns true for ALL valid PHP file extensions (.PHP, .php,
.Php, .pHP, etc..).


Take care,

Nik

Message #14 by "Colin Horne" <colinhorne@b...> on Fri, 28 Feb 2003 21:02:13 -0000
Hey

I am rather new to PHP, but I thought I might try to give a helping hand.

You return true or false with

(strrchr($filename, '.')==".php" || strrchr($filename, '.')==".PHP");

I think this line of code would be better:

return (strtolower( strrchr($filename, '.'))==".php");

also, you would need to take into account ".php3" etc

As I said though, I'm quite new to PHP, so it may not work...

Regards

Colin Horne
----- Original Message -----
From: "Nikolai Devereaux" <yomama@u...>
To: "beginning php" <beginning_php@p...>
Sent: Friday, February 28, 2003 8:43 PM
Subject: [beginning_php] RE: reading dir structure ... not consistent.


>
> > Gotcha.  Why not just change the name of the function ... to something
> > like this:
> >
> > function isPHPfile($filename)
> > {
> >     return (strrchr($filename, '.')==".php" ||
>               strrchr($filename, '.')==".PHP" );
> > }
>
>
> Well, that's fine, but not (imho) ideal.  First of all, you're calling
> strrchr() twice on the same string, with the same parameters.  That's a
> waste.  It'd be more efficient if you only called it once.
>
> Second, you're saying that the ONLY valid php files are those that end in
> ".PHP" or ".php".  What about ".Php"?  That won't pass your test.
>
>
> The function I wrote, extensionOf() simply wraps a call to strrchr() to
> return the substring from the last decimal point to the end of the
filename.
>
> isPHPfile() needs to compare a file extension against ".php", so why not
use
> the function that already returns the file extension?
>
> Moving on...
>
>
> > What does this part mean?  Specifically the "0 == " part.
> >
> > 0 == strcasecmp(extensionOf($filename), '.php')
>
>
> For that, you should read the PHP manual.  Get into the habit -- 99% of
all
> your questions can be answered in there.
>
>   http://www.php.net/strcasecmp
>
>
> This function performs a case-insensitive comparison on the two strings,
and
> returns 0 if they're equal.  This is the easiest way to guarantee that
> isPHPfile() returns true for ALL valid PHP file extensions (.PHP, .php,
> .Php, .pHP, etc..).
>
>
> Take care,
>
> Nik
>
>

Message #15 by "Nikolai Devereaux" <yomama@u...> on Fri, 28 Feb 2003 14:30:30 -0800
> I am rather new to PHP, but I thought I might try to give a helping hand.
>
> You return true or false with
>
> (strrchr($filename, '.')==".php" || strrchr($filename, '.')==".PHP");
>
> I think this line of code would be better:
>
> return (strtolower( strrchr($filename, '.'))==".php");


This is a great suggestion.  Converting the extension to lowercase before
comparing it to ".php" pretty much does the same thing as performing a
case-insensitive comparison (strcasecmp).  Personally, I think using a
single built-in function is a little more efficient and more readable.



You're right that the function returns a boolean value (true or false), but
that's what it *should* do.

The name of the function is, after all, "isPHPfile".  The NAME of the
function should imply what it does.  It answers the question, "Is $filename
a PHP file?"  This kind of question can only be answered appropriately with
a "yes" (true) or "no" (false).


> also, you would need to take into account ".php3" etc

This isn't really an issue with the application as the developer (or the
application he writes) is the only one who will add PHP files to the
filesystem, and can therefore control the extensions of the files.


If you wanted to be fancy about it, you could do this:

function isPHPfile($filename)
{
    $pattern = "/\.[pP][hH]([pP][34]?|[tT][mM][lL]?)$/";
    return preg_match($pattern, $filename) > 0;
}

Which returns true for all upper- and lower-case letter combinations of the
following file extensions:
   .php, .php3, .php4, .phtm, and phtml



Take care,

Nik


  Return to Index