# -*- php -*-
# The examples are taken from the Perl Cookbook
# By Tom Christiansen & Nathan Torkington
# see http://www.oreilly.com/catalog/cookbook for more

# @@PLEAC@@_NAME
# @@SKIP@@ PHP

# @@PLEAC@@_WEB
# @@SKIP@@ http://php.net/

# @@PLEAC@@_1.0
#-----------------------------
$string = '\n';                     # two characters, \ and an n
$string = 'Jon \'Maddog\' Orwant';  # literal single quotes
$string = 'Jon "Maddog" Orwant';    # literal double quotes
#-----------------------------
$string = "\n";                     # a "newline" character
$string = "Jon \"Maddog\" Orwant";  # literal double quotes
$string = "Jon 'Maddog' Orwant";    # literal single quotes
#-----------------------------
$a = 
"This is a multiline
here document";

$a = <<<EOF
This is a multiline here document
terminated by EOF on a line by itself
EOF;
#-----------------------------

# @@PLEAC@@_1.1
#-----------------------------
$value = substr($string, $offset, $count);
$value = substr($string, $offset);

$string = substr_replace($string, $newstring, $offset, $count);
$string = substr_replace($string, $newtail, $offset);
#-----------------------------
# get a 5-byte string, skip 3, then grab 2 8-byte strings, then the rest
list($leading, $s1, $s2, $trailing) =
    array_values(unpack("A5a/x3/A8b/A8c/A*d", $data);

# split at five byte boundaries
preg_match_all ("/.{5}/", $data, $f, PREG_PATTERN_ORDER);
$fivers = $f[0];

# chop string into individual characters
$chars  = $string;
#-----------------------------
$string = "This is what you have";
#         +012345678901234567890  Indexing forwards  (left to right)
#          109876543210987654321- Indexing backwards (right to left)
#           note that 0 means 10 or 20, etc. above

$first  = substr($string, 0, 1);  # "T"
$start  = substr($string, 5, 2);  # "is"
$rest   = substr($string, 13);    # "you have"
$last   = substr($string, -1);    # "e"
$end    = substr($string, -4);    # "have"
$piece  = substr($string, -8, 3); # "you"
#-----------------------------
$string = "This is what you have";
print $string;
#This is what you have

$string = substr_replace($string, "wasn't", 5, 2);  # change "is" to "wasn't"
#This wasn't what you have

$string = substr_replace($string, "ondrous", -12);  # "This wasn't wondrous"
#This wasn't wondrous

$string = substr_replace($string, "", 0, 1);        # delete first character
#his wasn't wondrous

$string = substr_replace($string, "", -10);         # delete last 10 characters
#his wasn'
#-----------------------------
if (preg_match("/pattern/", substr($string, -10)) {
    print "Pattern matches in last 10 characters\n";
}

# substitute "at" for "is", restricted to first five characters
$string=(substr_replace(preg_replace("/is/", "at", substr($string,0,5)),0,5);
#-----------------------------
# exchange the first and last letters in a string
$a = "make a hat";
list($a[0], $a[strlen($a)-1]) = Array(substr($a,-1), substr($a,0,1));
print $a;

#-----------------------------
# extract column with unpack
$a = "To be or not to be";
$b = unpack("x6/A6a", $a);  # skip 6, grab 6
print $b['a'];


$b = unpack("x6/A2b/X5/A2c", $a); # forward 6, grab 2; backward 5, grab 2
print $b['b']."\n".$b['c']."\n";

#-----------------------------
function cut2fmt() {
    $positions = func_get_args();
    $template  = '';
    $lastpos   = 1;
    foreach($positions as $place) {
        $template .= "A" . ($place - $lastpos) . " ";
        $lastpos   = $place;
    }
    $template .= "A*";
    return $template;
}

$fmt = cut2fmt(8, 14, 20, 26, 30);
print "$fmt\n";
#A7 A6 A6 A6 A4 A*
#-----------------------------

# @@PLEAC@@_1.2
#-----------------------------
# use $b if $b is true, else $c
$a = $b?$b:$c;

# set $x to $y unless $x is already true
$x || $x=$y;
#-----------------------------
# use $b if $b is defined, else $c
$a = defined($b) ? $b : $c;
#-----------------------------
$foo = $bar || $foo = "DEFAULT VALUE";
#-----------------------------
$dir = array_shift($_SERVER['argv']) || $dir = "/tmp";
#-----------------------------
$dir = $_SERVER['argv'][0] || $dir = "/tmp";
#-----------------------------
$dir = defined($_SERVER['argv'][0]) ? array_shift($_SERVER['argv']) : "/tmp";
#-----------------------------
$dir = count($_SERVER['argv']) ? $_SERVER['argv'][0] : "/tmp";
#-----------------------------
$count[$shell?$shell:"/bin/sh"]++;
#-----------------------------
# find the user name on Unix systems
$user = $_ENV['USER']
     || $user = $_ENV['LOGNAME']
     || $user = posix_getlogin()
     || $user = posix_getpwuid(posix_getuid())[0]
     || $user = "Unknown uid number $<";
#-----------------------------
$starting_point || $starting_point = "Greenwich";
#-----------------------------
count($a) || $a = $b;          # copy only if empty
$a = count($b) ? $b : $c;          # assign @b if nonempty, else @c
#-----------------------------

# @@PLEAC@@_1.3
#-----------------------------
list($VAR1, $VAR2) = array($VAR2, $VAR1);
#-----------------------------
$temp    = $a;
$a       = $b;
$b       = $temp;
#-----------------------------
$a       = "alpha";
$b       = "omega";
list($a, $b) = array($b, $a);        # the first shall be last -- and versa vice
#-----------------------------
list($alpha, $beta, $production) = Array("January","March","August");
# move beta       to alpha,
# move production to beta,
# move alpha      to production
list($alpha, $beta, $production) = array($beta, $production, $alpha);
#-----------------------------

# @@PLEAC@@_1.4
#-----------------------------
$num  = ord($char);
$char = chr($num);
#-----------------------------
$char = sprintf("%c", $num);                # slower than chr($num)
printf("Number %d is character %c\n", $num, $num);
#-----------------------------
$ASCII = unpack("C*", $string);
eval('$STRING = pack("C*", '.implode(',',$ASCII).');');
#-----------------------------
$ascii_value = ord("e");    # now 101
$character   = chr(101);    # now "e"
#-----------------------------
printf("Number %d is character %c\n", 101, 101);
#-----------------------------
$ascii_character_numbers = unpack("C*", "sample");
print explode(" ",$ascii_character_numbers)."\n";

eval('$word = pack("C*", '.implode(',',$ascii_character_numbers).');');
$word = pack("C*", 115, 97, 109, 112, 108, 101);   # same
print "$word\n";
#-----------------------------
$hal = "HAL";
$ascii = unpack("C*", $hal);
foreach ($ascii as $val) {
    $val++;                 # add one to each ASCII value
}
eval('$ibm = pack("C*", '.implode(',',$ascii).');');
print "$ibm\n";             # prints "IBM"
#-----------------------------

# @@PLEAC@@_1.5
#-----------------------------
// using perl regexp
$array = preg_split('//', $string ,-1, PREG_SPLIT_NO_EMPTY);
// using PHP function: $array = str_split($string);

// Cannot use unpack with a format of 'U*' in PHP.
#-----------------------------
for ($offset = 0; preg_match('/(.)/', $string, $matches, 0, $offset) > 0; $offset++) {
    // $matches[1] has charcter, ord($matches[1]) its number
}
#-----------------------------
$seen = array();
$string = "an apple a day";
foreach (str_split($string) as $char) {
    $seen[$char] = 1;
}
$keys = array_keys($seen);
sort($keys);
print "unique chars are: " . implode('', $keys)) . "\n";
unique chars are:  adelnpy
#-----------------------------
$seen = array();
$string = "an apple a day";
for ($offset = 0; preg_match('/(.)/', $string, $matches, 0, $offset) > 0; $offset++) {
    $seen[$matches[1]] = 1;
}
$keys = array_keys($seen);
sort($keys);
print "unique chars are: " . implode('', $keys) . "\n";
unique chars are:  adelnpy
#-----------------------------
$sum = 0;
foreach (unpack("C*", $string) as $byteval) {
    $sum += $byteval;
}
print "sum is $sum\n";
// prints "1248" if $string was "an apple a day"
#-----------------------------
$sum = array_sum(unpack("C*", $string));
#-----------------------------

// sum - compute 16-bit checksum of all input files
$handle = @fopen($argv[1], 'r');
$checksum = 0;
while (!feof($handle)) {
    $checksum += (array_sum(unpack("C*", fgets($handle))));
}
$checksum %= pow(2,16) - 1;
print "$checksum\n";

# download the following standalone program
#!/usr/bin/php
<?php
// slowcat - emulate a   s l o w   line printer

// usage: php slowcat.php [-DELAY] file

$delay = 1;
if (preg_match('/(.)/', $argv[1], $matches)) {
    $delay = $matches[1];
    array_shift($argv);
};
$handle = @fopen($argv[1], 'r');
while (!feof($handle)) {
    foreach (str_split(fgets($handle)) as $char) {
        print $char;
        usleep(5000 * $delay);
    }
}
#-----------------------------

# @@PLEAC@@_1.6
#-----------------------------
$revchars = strrev($string);
#-----------------------------
$revwords = implode(" ", array_reverse(explode(" ", $string)));
#-----------------------------
// reverse word order
$string = 'Yoda said, "can you see this?"';
$allwords    = explode(" ", $string);
$revwords    = implode(" ", array_reverse($allwords));
print $revwords . "\n";
this?" see you "can said, Yoda
#-----------------------------
$revwords = implode(" ", array_reverse(explode(" ", $string)));
#-----------------------------
$revwords = implode(" ", array_reverse(preg_split("/(\s+)/", $string)));
#-----------------------------
$word = "reviver";
$is_palindrome = ($word === strrev($word));
#-----------------------------
// quite a one-liner since "php" does not have a -n switch
% php -r 'while (!feof(STDIN)) { $word = rtrim(fgets(STDIN)); if ($word == strrev($word) && strlen($word) > 5) print $word; }' < /usr/dict/words
#-----------------------------

# @@PLEAC@@_1.8
#-----------------------------
$text = preg_replace('/\$(\w+)/e', '$$1', $text);
#-----------------------------
list($rows, $cols) = Array(24, 80);
$text = 'I am $rows high and $cols long';
$text = preg_replace('/\$(\w+)/e', '$$1', $text);
print $text;

#-----------------------------
$text = "I am 17 years old";
$text = preg_replace('/(\d+)/e', '2*$1', $text);
#-----------------------------
# expand variables in $text, but put an error message in
# if the variable isn't defined
$text = preg_replace('/\$(\w+)/e','isset($$1)?$$1:\'[NO VARIABLE: $$1]\'', $text);
#-----------------------------

// As PHP arrays are used as hashes too, separation of section 4
// and section 5 makes little sense.

# @@PLEAC@@_1.9
#-----------------------------
$big = strtoupper($little);
$little = strtolower($big);
// PHP does not have the\L and\U string escapes.
#-----------------------------
$big = ucfirst($little);
$little = strtolower(substr($big, 0, 1)) . substr($big, 1);
#-----------------------------
$beast   = "dromedary";
// capitalize various parts of $beast
$capit   = ucfirst($beast); // Dromedar
// PHP does not have the\L and\U string escapes.
$capall  = strtoupper($beast); // DROMEDAR
// PHP does not have the\L and\U string escapes.
$caprest = strtolower(substr($beast, 0, 1)) . substr(strtoupper($beast), 1); // dROMEDAR
// PHP does not have the\L and\U string escapes.
#-----------------------------
// titlecase each word's first character, lowercase the rest
$text = "thIS is a loNG liNE";
$text = ucwords(strtolower($text));
print $text;
This Is A Long Line
#-----------------------------
if (strtoupper($a) == strtoupper($b)) { // or strcasecmp($a, $b) == 0
    print "a and b are the same\n";
}
#-----------------------------
# download the following standalone program
#!/usr/bin/php
<?php
// randcap: filter to randomly capitalize 20% of the letters

function randcase($word) {
  return rand(0, 100) < 20 ? ucfirst($word) : lcfirst($word);
}
function lcfirst($word) {
  return strtolower(substr($word, 0, 1)) . substr($word, 1);
}
while (!feof(STDIN)) {
  print preg_replace("/(\w)/e", "randcase('\\1')", fgets(STDIN));
}

// % php randcap.php < genesis | head -9
#-----------------------------

# @@PLEAC@@_1.10
#-----------------------------
echo $var1 . func() . $var2; // scalar only
#-----------------------------
// PHP can only handle variable expression without operators
$answer = "STRING ${[ VAR EXPR ]} MORE STRING";
#-----------------------------
$phrase = "I have " . ($n + 1) . " guanacos.";
// PHP cannot handle the complex exression: ${\($n + 1)}
#-----------------------------
// Rest of Discussion is not applicable to PHP
#-----------------------------
// Interpolating functions not available in PHP
#-----------------------------

# @@PLEAC@@_1.11
# @@INCOMPLETE@@
# @@INCOMPLETE@@

# @@PLEAC@@_1.12
#-----------------------------
$output = wordwrap($str, $width, $break, $cut);
#-----------------------------
# download the following standalone program
#!/usr/bin/php
<?php
// wrapdemo - show how wordwrap works

$input = "Folding and splicing is the work of an editor, " .
         "not a mere collection of silicon " .
         "and " .
         "mobile electrons!";
$columns = 20;
print str_repeat("0123456789", 2) . "\n";
print wordwrap('    ' . $input, $columns - 3, "\n  ") . "\n";
#-----------------------------
// merge multiple lines into one, then wrap one long line
print wordwrap(str_replace("\n", " ", file_get_contents('php://stdin')));
#-----------------------------
while(!feof(STDIN)) {
    print wordwrap(str_replace("\n", " ", stream_get_line(STDIN, 0, "\n\n")));
    print "\n\n";
}
#-----------------------------

# @@PLEAC@@_1.13
#-----------------------------
//backslash
$var = preg_replace('/([CHARLIST])/', '\\\$1', $var);
// double
$var = preg_replace('/([CHARLIST])/', '$1$1', $var);
#-----------------------------
$var = preg_replace('/%/', '%%', $var);
#-----------------------------
$string = 'Mom said, "Don\'t do that."';
$string = preg_replace('/([\'"])/', '\\\$1', $string);
// in PHP you can also use the addslashes() function
#-----------------------------
$string = 'Mom said, "Don\'t do that."';
$string = preg_replace('/([\'"])/', '$1$1', $string);
#-----------------------------
$string = preg_replace('/([^A-Z])/', '\\\$1', $string);
#-----------------------------
// PHP does not have the \Q and \E string metacharacters
$string = "this is\\ a\\ test\\!";
// PHP's quotemeta() function is not the same as perl's quotemeta() function
$string = preg_replace('/(\W)/', '\\\$1', 'is a test!');
#-----------------------------

# @@PLEAC@@_1.14
#-----------------------------
$string = trim($string);
#-----------------------------
// print what's typed, but surrounded by > < symbols
while (!feof(STDIN)) {
    print ">" . substr(fgets(STDIN), 0, -1) . "<\n";
}
#-----------------------------
$string = preg_replace('/\s+/', ' ', $string); // finally, collapse middle
#-----------------------------
$string = trim($string);
$string = preg_replace('/\s+/', ' ', $string);
#-----------------------------
// 1. trim leading and trailing white space
// 2. collapse internal whitespace to single space each
function sub_trim($string) {
    $string = trim($string);
    $string = preg_replace('/\s+/', ' ', $string);
    return $string;
}
#-----------------------------

# @@PLEAC@@_1.15
# @@INCOMPLETE@@
# @@INCOMPLETE@@

# @@PLEAC@@_1.16
#-----------------------------
$code = soundex($string);
#-----------------------------
$phoned_words = metaphone("Schwern");
#-----------------------------
// substitution function for getpwent():
// returns an array of user entries,
// each entry contains the username and the full name
function getpwent() {
    $pwents = array();
    $handle = fopen("passwd", "r");
    while (!feof($handle)) {
        $line = fgets($handle);
        if (preg_match("/^#/", $line)) continue;
        $cols = explode(":", $line);
        $pwents[$cols[0]] = $cols[4];
    }
    return $pwents;
}

print "Lookup user: ";
$user = rtrim(fgets(STDIN));
if (empty($user)) exit;
$name_code = soundex($user);
$pwents = getpwent();
foreach($pwents as $username => $fullname) {
    preg_match("/(\w+)[^,]*\b(\w+)/", $fullname, $matches);
    list(, $firstname, $lastname) = $matches;
  
    if ($name_code == soundex($username) ||
        $name_code == soundex($lastname) ||
        $name_code == soundex($firstname))
    {
        printf("%s: %s %s\n", $username, $firstname, $lastname);
    }
}
#-----------------------------

# @@PLEAC@@_1.17
#-----------------------------
# download the following standalone program
#!/usr/bin/php
<?php
$data = <<<DATA
  analysed=> analyzed
  built-in=> builtin
  chastized   => chastised
  commandline => command-line
  de-allocate => deallocate
  dropin  => drop-in
  hardcode=> hard-code
  meta-data   => metadata
  multicharacter  => multi-character
  multiway=> multi-way
  non-empty   => nonempty
  non-profit  => nonprofit
  non-trappable   => nontrappable
  pre-define  => predefine
  preextend   => pre-extend
  re-compiling=> recompiling
  reenter => re-enter
  turnkey => turn-key
DATA;

$scriptName = $argv[0];
$verbose = ($argc > 1 && $argv[1] == "-v" && array_shift($argv));
$change = array();
foreach (preg_split("/\n/", $data) as $pair) {
  list($in, $out) = preg_split("/\s*=>\s*/", trim($pair));
  if (!$in || !$out) continue;
  $change[$in] = $out;
}
if (count($argv) > 1)  {
  // no in-place edit in PHP

  // preserve old files

  $orig = $argv[1] . ".orig";
  copy($argv[1], $orig);
  $input = fopen($orig, "r");
  $output = fopen($argv[1], "w");
} else if ($scriptName != "-") {
  $input = STDIN;
  trigger_error("$scriptName: Reading from stdin\n", E_USER_WARNING);
}
$ln = 1;
while (!feof($input)) {
  $line = fgets($input);
  foreach ($change as $in => $out) {
    $line = preg_replace("/$in/", $out, $line, -1, $count);
    if ($count > 0 && $verbose) {
      fwrite(STDERR, "$in => $out at $argv[1] line $ln.\n");
    }
  }
  @fwrite($output, $line);
  $ln++;
}
#-----------------------------
# download the following standalone program
#!/usr/bin/php
<?php
$data = <<<DATA
  analysed=> analyzed
  built-in=> builtin
  chastized   => chastised
  commandline => command-line
  de-allocate => deallocate
  dropin  => drop-in
  hardcode=> hard-code
  meta-data   => metadata
  multicharacter  => multi-character
  multiway=> multi-way
  non-empty   => nonempty
  non-profit  => nonprofit
  non-trappable   => nontrappable
  pre-define  => predefine
  preextend   => pre-extend
  re-compiling=> recompiling
  reenter => re-enter
  turnkey => turn-key
DATA;

$scriptName = $argv[0];
$verbose = ($argc > 1 && $argv[1] == "-v" && array_shift($argv));
if (count($argv) > 1)  {
  // no in-place edit in PHP

  // preserve old files

  $orig = $argv[1] . ".orig";
  copy($argv[1], $orig);
  $input = fopen($orig, "r");
  $output = fopen($argv[1], "w");
} else if ($scriptName != "-") {
  $input = STDIN;
  trigger_error("$scriptName: Reading from stdin\n", E_USER_WARNING);
}

$config = array();
foreach (preg_split("/\n/", $data) as $pair) {
  list($in, $out) = preg_split("/\s*=>\s*/", trim($pair));
  if (!$in || !$out) continue;
  $config[$in] = $out;
}

$ln = 1;
while (!feof($input)) {
  $i = 0;
  preg_match("/^(\s*)(.*)/", fgets($input), $matches); // emit leading whitespace

  fwrite($output, $matches[1]);
  foreach (preg_split("/(\s+)/", $matches[2], -1, PREG_SPLIT_DELIM_CAPTURE) as $token) { // preserve trailing whitespace

    fwrite($output, ($i++ & 1) ? $token : (array_key_exists($token, $config) ? $config[$token] : $token));
  }
}
#-----------------------------
// very fast, but whitespace collapse
while (!feof($input)) {
  $i = 0;
  preg_match("/^(\s*)(.*)/", fgets($input), $matches); // emit leading whitespace
  fwrite($output, $matches[1]);
  foreach (preg_split("/(\s+)/", $matches[2]) as $token) { // preserve trailing whitespace
    fwrite($output, (array_key_exists($token, $config) ? $config[$token] : $token) . " ");
  }
  fwrite($output, "\n");
}
#-----------------------------

// @@PLEAC@@_2.0
// As is the case under so many other languages floating point use under PHP is fraught
// with dangers. Although the basic techniques shown below are valid, please refer to
// the official PHP documentation for known issues, bugs, and alternate approaches 

// @@PLEAC@@_2.1
// Two basic approaches to numeric validation:
// * Built-in functions like 'is_numeric', 'is_int', 'is_float' etc
// * Regexes, as shown below

$s = '12.345';

preg_match('/\D/', $s) && die("has nondigits\n");
preg_match('/^\d+$/', $s) || die("not a natural number\n");
preg_match('/^-?\d+$/', $s) || die("not an integer\n");
preg_match('/^[+-]?\d+$/', $s) || die("not an integer\n");
preg_match('/^-?\d+\.?\d*$/', $s) || die("not a decimal\n");
preg_match('/^-?(?:\d+(?:\.\d*)?|\.\d+)$/', $s) || die("not a decimal\n");
preg_match('/^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/', $s) || die("not a C float\n");

// ----------------------------

function getnum($s)
{
  sscanf($s, "%D", $number); return isset($number) ? $number : 0;
}

echo getnum(123) . "\n";   // ok
echo getnum(0xff) . "\n";  // ..
echo getnum(044) . "\n";   // ..

echo getnum('x') . "\n";   // fail

// @@PLEAC@@_2.2
// In PHP floating point comparisions are 'safe' [meaning the '==' comparison operator
// can be used] as long as the value consists of 14 digits or less [total digits, either
// side of the decimal point e.g. xxxxxxx.xxxxxxx, xxxxxxxxxxxxxx., .xxxxxxxxxxxxxx]. If
// values with more digits must be compared, then:
//
// * Represent as strings, and take care to avoid any implicit conversions e.g. don't pass
//   a float as a float to a function and expect all digits to be retained - they won't be -
//   then use 'strcmp' to compare the strings
//
// * Avoid float use; perhaps use arbitrary precision arithmetic. In this case, the
//   'bccomp' function is relevant

// Will work as long as each floating point value is 14 digits or less
if ($float_1 == $float_2)
{
  ; // ...
}

// Compare as strings
$cmp = strcmp('123456789.123456789123456789', '123456789.123456789123456788');

// Use 'bccomp'
$precision = 5; // Number of significant comparison digits after decimal point
if (bccomp('1.111117', '1.111116', $precision))
{
  ; // ...
}

$precision = 6;
if (bccomp('1.111117', '1.111116', $precision))
{
  ; // ...
}

// ----------------------------

$wage = 536;
$week = $wage * 40;
printf("One week's wage is: $%.2f\n", $week / 100);

// @@PLEAC@@_2.3
// Preferred approach
$rounded = round($unrounded, $precision);

// Possible alternate approach
$format = '%[width].[prec]f';
$rounded = sprintf($format, $unrounded);

// ------------

$a = 0.255; $b = round($a, 2);
echo "Unrounded: {$a}\nRounded: {$b}\n";

$a = 0.255; $b = sprintf('%.2f', $a);
echo "Unrounded: {$a}\nRounded: {$b}\n";

$a = 0.255;
printf("Unrounded: %.f\nRounded: %.2f\n", $a, $a);

// ----------------------------

echo "number\tint\tfloor\tceil\n";

foreach(array(3.3, 3.5, 3.7, -3.3) as $number)
{
  printf("%.1f\t%.1f\t%.1f\t%.1f\n", $number, (int) $number, floor($number), ceil($number));
}

// @@PLEAC@@_2.4
// PHP offers the 'bindec' and 'decbin' functions to converting between binary and decimal

$num = bindec('0110110');

$binstr = decbin(54);

// @@PLEAC@@_2.5
foreach (range($X, $Y) as $i)
{
  ; // ...
}

foreach (range($X, $Y, 7) as $i)
{
  ; // ...
}

for ($i = $X; $i <= $Y; $i++)
{
  ; // ...
}

for ($i = $X; $i <= $Y; $i += 7)
{
  ; // ...
}

// ----------------------------

echo 'Infancy is:'; foreach(range(0, 2) as $i) echo " {$i}\n";
echo 'Toddling is:'; foreach(range(3, 4) as $i) echo " {$i}\n";
echo 'Childhood is:'; foreach(range(5, 12) as $i) echo " {$i}\n";

// @@PLEAC@@_2.6
// PHP offers no native support for Roman Numerals. However, a 'Numbers_Roman' class
// is available for download from PEAR: [http://pear.php.net/package/Numbers_Roman].
// Note the following 'include' directives are required:
//
//   include_once('Numbers/Roman.php');

$roman = Numbers_Roman::toNumeral($arabic);
$arabic = Numbers_Roman::toNumber($roman);

// ----------------------------

$roman_fifteen = Numbers_Roman::toNumeral(15);

$arabic_fifteen = Numbers_Roman::toNumber($roman_fifteen);

printf("Roman for fifteen is: %s\n", $roman_fifteen);
printf("Arabic for fifteen is: %d\n", $arabic_fifteen);

// @@PLEAC@@_2.7
// Techniques used here simply mirror Perl examples, and are not an endorsement
// of any particular RNG technique

// In PHP do this ...
$random = rand($lowerbound, $upperbound);
$random = rand($x, $y);

// ----------------------------

function make_password($chars, $reqlen)
{
  $len = strlen($chars);
  for ($i = 0; $i < $reqlen; $i++) $password .= substr($chars, rand(0, $len), 1);
  return $password;
}

$chars = 'ABCDEfghijKLMNOpqrstUVWXYz'; $reqlen = 8;

$password = make_password($chars, $reqlen);

// @@PLEAC@@_2.8
// PHP sports a large number of C Standard Library routines including the 'srand'
// function, used to re-seed the RNG used with calls to the 'rand' function. Thus,
// as per Perl example:

while (TRUE)
{
  $seed = (int) fgets(STDIN);
  if (!empty($seed)) break;
}

srand($seed);

// @@PLEAC@@_2.9
// The above is considered - for many reasons - a poor way of seeding the RNG. PHP
// also offers alternate versions of the functions, 'mt_srand' and 'mt_rand',
// which are described as faster, and more 'random', though key to obtaining a
// more 'random' distribution of generated numbers seems to be through using
// a combination of a previously saved random value in combination with an
// unrepeatable value [like the current time in microseconds] that is multiplied
// by a large prime number, or perhaps as part of a hash [examples available in
// PHP documentation for 'srand' and 'mt_srand']

mt_srand($saved_random_value + microtime() * 1000003);

// or

mt_srand(($saved_random_value + hexdec(substr(md5(microtime()), -8))) & 0x7fffffff);

// Use of 'mt_rand' together with an appropriate seeding approach should help better
// approximate the generation of a 'truly random value'
$truly_random_value = mt_rand();

// @@PLEAC@@_2.10
function random() { return (float) rand() / (float) getrandmax(); }

function gaussian_rand()
{
  $u1 = 0.0; $u2 = 0.0; $g1 = 0.0; $g2 = 0.0; $w = 0.0;
  
  do
  {
    $u1 = 2.0 * random() - 1.0; $u2 = 2.0 * random() - 1.0;
    $w = $u1 * $u1 + $u2 * $u2;
  } while ($w > 1.0);
  
  $w = sqrt((-2.0 * log($w)) / $w); $g2 = $u1 * $w; $g1 = $u2 * $w;

  return $g1;
}

// ------------

$mean = 25.0; $sdev = 2.0;
$salary = gaussian_rand() * $mean + $sdev;

printf("You have been hired at: %.2f\n", $salary);

// @@PLEAC@@_2.11
// 'deg2rad' and 'rad2deg' are actually PHP built-ins, but here is how you might implement
/  them if needed
function deg2rad_($deg) { return ($deg / 180.0) * M_PI; }
function rad2deg_($rad) { return ($rad / M_PI) * 180.0; }

// ------------

printf("%f\n", deg2rad_(180.0));
printf("%f\n", deg2rad(180.0));

// ----------------------------

function degree_sin($deg) { return sin(deg2rad($deg)); }

// ------------

$rad = deg2rad(380.0);

printf("%f\n", sin($rad));
printf("%f\n", degree_sin(380.0));

// @@PLEAC@@_2.12
function my_tan($theta) { return sin($theta) / cos($theta); }

// ------------

$theta = 3.7;

printf("%f\n", my_tan($theta));
printf("%f\n", tan($theta));

// @@PLEAC@@_2.13
$value = 100.0;

$log_e = log($value);
$log_10 = log10($value);

// ----------------------------

function log_base($base, $value) { return log($value) / log($base); }

// ------------

$answer = log_base(10.0, 10000.0);

printf("log(10, 10,000) = %f\n", $answer);

// @@PLEAC@@_2.14
// PHP offers no native support for matrices. However, a 'Math_Matrix' class
// is available for download from PEAR: [http://pear.php.net/package/Math_Matrix].
// Note the following 'include' directives are required:
//
//  include_once('Math/Matrix.php');

$a = new Math_Matrix(array(array(3, 2, 3), array(5, 9, 8)));
$b = new Math_Matrix(array(array(4, 7), array(9, 3), array(8, 1)));

echo $a->toString() . "\n";
echo $b->toString() . "\n";

// NOTE: When I installed this package I had to rename the 'clone' method else
// it would not load, so I chose to rename it to 'clone_', and this usage is
// shown below. This bug may well be fixed by the time you obtain this package

$c = $a->clone_();
$c->multiply($b);

echo $c->toString() . "\n";

// @@PLEAC@@_2.15
// PHP offers no native support for complex numbers. However, a 'Math_Complex' class
// is available for download from PEAR: [http://pear.php.net/package/Math_Complex].
// Note the following 'include' directives are required:
//
//   include_once('Math/Complex.php');
//   include_once('Math/TrigOp.php');
//   include_once('Math/ComplexOp.php');

$a = new Math_Complex(3, 5);
$b = new Math_Complex(2, -2);

$c = Math_ComplexOp::mult($a, $b);

echo $c->toString() . "\n";

// ----------------------------

$d = new Math_Complex(3, 4);
$r = Math_ComplexOp::sqrt($d);

echo $r->toString() . "\n";

// @@PLEAC@@_2.16
// Like C, PHP supports decimal-alternate notations. Thus, for example, the integer
// value, 867, is expressable in literal form as:
//
//   Hexadecimal -> 0x363
//   Octal       -> 01543
//
// For effecting such conversions using strings there is 'sprintf' and 'sscanf'.

$dec = 867;
$hex = sprintf('%x', $dec);
$oct = sprintf('%o', $dec);

// ------------

$dec = 0;
$hex = '363';

sscanf($hex, '%x', $dec);

// ------------

$dec = 0;
$oct = '1543';

sscanf($oct, '%o', $dec);

// ----------------------------

$number = 0;

printf('Gimme a number in decimal, octal, or hex: ');
sscanf(fgets(STDIN), '%D', $number);

printf("%d %x %o\n", $number, $number, $number);

// @@PLEAC@@_2.17
// PHP offers the 'number_format' built-in function to, among many other format tasks, 
// commify numbers. Perl-compatible [as well as extended] regexes are also available

function commify_series($s) { return number_format($s, 0, '', ','); }

// ------------

$hits = 3456789;

printf("Your website received %s accesses last month\n", commify_series($hits));

// ----------------------------

function commify($s)
{
  return strrev(preg_replace('/(\d\d\d)(?=\d)(?!\d*\.)/', '${1},', strrev($s)));
}

// ------------

$hits = 3456789;

echo commify(sprintf("Your website received %d accesses last month\n", $hits));

// @@PLEAC@@_2.18
function pluralise($value, $root, $singular='' , $plural='s')
{
  return $root . (($value > 1) ? $plural : $singular);
}

// ------------

$duration = 1;
printf("It took %d %s\n", $duration, pluralise($duration, 'hour'));
printf("%d %s %s enough.\n", $duration, pluralise($duration, 'hour'),
      pluralise($duration, '', 'is', 'are'));

$duration = 5;
printf("It took %d %s\n", $duration, pluralise($duration, 'hour'));
printf("%d %s %s enough.\n", $duration, pluralise($duration, 'hour'),
      pluralise($duration, '', 'is', 'are'));

// ----------------------------

function plural($singular)
{
  $s2p = array('/ss$/' => 'sses', '/([psc]h)$/' => '${1}es', '/z$/' => 'zes',
               '/ff$/' => 'ffs', '/f$/' => 'ves', '/ey$/' => 'eys',
               '/y$/' => 'ies', '/ix$/' => 'ices', '/([sx])$/' => '$1es',
               '$' => 's');

  foreach($s2p as $s => $p)
  {
    if (preg_match($s, $singular)) return preg_replace($s, $p, $singular);
  }
}

// ------------

foreach(array('mess', 'index', 'leaf', 'puppy') as $word)
{
  printf("%6s -> %s\n", $word, plural($word));
}

// @@PLEAC@@_2.19
// @@INCOMPLETE@@
// @@INCOMPLETE@@

// @@PLEAC@@_3.0
// PHP's date / time suport is quite extensive, and appears grouped into three areas of
// functionality:
//
// * UNIX / C Library [libc]-based routines, which include [among others]:
//   - localtime, gmtime
//   - strftime, strptime, mktime
//   - time, getdate, gettimeofday, 
//
// * PHP 'native' functions, those date / time routines released in earlier versions,
//   and which otherwise provide 'convenience' functionality; these include:
//   - date
//   - strtotime
//
// * 'DateTime' class-based. This facility appears [according to the PHP documentation]
//   to be extremely new / experimental, so whilst usage examples will be provided, they
//   should not be taken to be 'official' examples, and obviously, subject to change.
//   My own impression is that this facility is currently only partially implemented,
//   so there is limited use for these functions. The functions included in this group
//   are some of the 'date_'-prefixed functions; they are, however, not used standalone,
//   but as methods in conjunction with an object. Typical usage:
//
//     $today = new DateTime();             // actually calls: date_create($today, ...);
//     echo $today->format('U') . "\n";     // actually calls: date_format($today, ...);
//
// Also worth mentioning is the PEAR [PHP Extension and Repository] package, 'Calendar',
// which offers a rich set of date / time manipulation facilities. However, since it is
// not currently shipped with PHP, no examples appear

// Helper functions for performing date arithmetic 

function dateOffset()
{
  static $tbl = array('sec' => 1, 'min' => 60, 'hou' => 3600, 'day' => 86400, 'wee' => 604800);
  $delta = 0;

  foreach (func_get_args() as $arg)
  {
    $kv = explode('=', $arg);
    $delta += $kv[1] * $tbl[strtolower(substr($kv[0], 0, 3))];
  }

  return $delta;
}

function dateInterval($intvltype, $timevalue)
{
  static $tbl = array('sec' => 1, 'min' => 60, 'hou' => 3600, 'day' => 86400, 'wee' => 604800);
  return (int) round($timevalue / $tbl[strtolower(substr($intvltype, 0, 3))]);
}

// ----------------------------

// Extract indexed array from 'getdate'
$today = getdate();
printf("Today is day %d of the current year\n", $today['yday']);

// Extract indexed, and associative arrays, respectively, from 'localtime'
$today = localtime();
printf("Today is day %d of the current year\n", $today[7]);

$today = localtime(time(), TRUE);
printf("Today is day %d of the current year\n", $today['tm_yday']);

// @@PLEAC@@_3.1
define(SEP, '-');

// ------------

$today = getdate();

$day = $today['mday'];
$month = $today['mon'];
$year = $today['year'];

// Either do this to use interpolation:
$sep = SEP;
echo "Current date is: {$year}{$sep}{$month}{$sep}{$day}\n";

// or simply concatenate:
echo 'Current date is: ' . $year . SEP . $month . SEP . $day . "\n";

// ------------

$today = localtime(time(), TRUE);

$day = $today['tm_mday'];
$month = $today['tm_mon'] + 1;
$year = $today['tm_year'] + 1900;

printf("Current date is: %4d%s%2d%s%2d\n", $year, SEP, $month, SEP, $day);

// ------------

$format = 'Y' . SEP . 'n' . SEP . 'd';

$today = date($format);

echo "Current date is: {$today}\n";

// ------------

$sep = SEP;

$today = strftime("%Y$sep%m$sep%d");

echo "Current date is: {$today}\n";

// @@PLEAC@@_3.2
$timestamp = mktime($hour, $min, $sec, $month, $day, $year);

$timestamp = gmmktime($hour, $min, $sec, $month, $day, $year);

// @@PLEAC@@_3.3
$dmyhms = getdate();            // timestamp: current date / time

$dmyhms = getdate($timestamp);  // timestamp: arbitrary

$day = $dmyhms['mday'];
$month = $dmyhms['mon'];
$year = $dmyhms['year'];

$hours = $dmyhms['hours'];
$minutes = $dmyhms['minutes'];
$seconds = $dmyhms['seconds'];

// @@PLEAC@@_3.4
// Date arithmetic is probably most easily performed using timestamps [i.e. *NIX Epoch
// Seconds]. Dates - in whatever form - are converted to timestamps, these are
// arithmetically manipulated, and the result converted to whatever form required.
// Note: use 'mktime' to create timestamps properly adjusted for daylight saving; whilst
// 'strtotime' is more convenient to use, it does not, AFAIK, include this adjustment

$when = $now + $difference;
$then = $now - $difference;

// ------------

$now = mktime(0, 0, 0, 8, 6, 2003);

$diff1 = dateOffset('day=1'); $diff2 = dateOffset('weeks=2');

echo 'Today is:                 ' . date('Y-m-d', $now) . "\n";
echo 'One day in the future is: ' . date('Y-m-d', $now + $diff1) . "\n";
echo 'Two weeks in the past is: ' . date('Y-m-d', $now - $diff2) . "\n";

// ----------------------------

// Date arithmetic performed using a custom function, 'dateOffset'. Internally, offset may
// be computed in one of several ways:
// * Direct timestamp manipulation - fastest, but no daylight saving adjustment 
// * Via 'date' built-in function - slower [?], needs a base time from which to
//   compute values, but has daylight saving adjustment 
// * Via 'strtotime' built-in function - as for 'date'
// * Via 'DateTime' class
//
// Approach used here is to utilise direct timestamp manipulation in 'dateOffset' [it's
// performance can also be improved by replacing $tbl with a global definition etc],
// and to illustrate how the other approaches might be used 

// 1. 'dateOffset'

$birthtime = mktime(3, 45, 50, 1, 18, 1973);

$interval = dateOffset('day=55', 'hours=2', 'min=17', 'sec=5');

$then = $birthtime + $interval;

printf("Birthtime is: %s\nthen is:      %s\n", date(DATE_RFC1123, $birthtime), date(DATE_RFC1123, $then));

// ------------

// 2. 'date'

// Base values, and offsets, respectively
$hr = 3; $min = 45; $sec = 50; $mon = 1; $day = 18; $year = 1973;

$yroff = 0; $monoff = 0; $dayoff = 55; $hroff = 2; $minoff = 17; $secoff = 5;

// Base date
$birthtime = mktime($hr, $min, $sec, $mon, $day, $year, TRUE);

$year = date('Y', $birthtime) + $yroff;
$mon = date('m', $birthtime) + $monoff;
$day = date('d', $birthtime) + $dayoff;

$hr = date('H', $birthtime) + $hroff;
$min = date('i', $birthtime) + $minoff;
$sec = date('s', $birthtime) + $secoff;

// Offset date
$then = mktime($hr, $min, $sec, $mon, $day, $year, TRUE);

printf("Birthtime is: %s\nthen is:      %s\n", date(DATE_RFC1123, $birthtime), date(DATE_RFC1123, $then));

// ------------

// 3. 'strtotime'

// Generate timestamp whatever way is preferable
$birthtime = mktime(3, 45, 50, 1, 18, 1973);
$birthtime = strtotime('1/18/1973 03:45:50');

$then = strtotime('+55 days 2 hours 17 minutes 2 seconds', $birthtime);

printf("Birthtime is: %s\nthen is:      %s\n", date(DATE_RFC1123, $birthtime), date(DATE_RFC1123, $then));

// ------------

// 4. 'DateTime' class

$birthtime = new DateTime('1/18/1973 03:45:50');
$then = new DateTime('1/18/1973 03:45:50');
$then->modify('+55 days 2 hours 17 minutes 2 seconds');

printf("Birthtime is: %s\nthen is:      %s\n", $birthtime->format(DATE_RFC1123), $then->format(DATE_RFC1123));

// @@PLEAC@@_3.5
// Date intervals are most easily computed using timestamps [i.e. *NIX Epoch
// Seconds] which, of course, gives the interval result is seconds from which
// all other interval measures [days, weeks, months, years] may be derived.
// Refer to previous section for discussion of daylight saving and other related
// problems

$interval_seconds = $recent - $earlier;

// ----------------------------

// Conventional approach ...
$bree = strtotime('16 Jun 1981, 4:35:25');
$nat = strtotime('18 Jan 1973, 3:45:50');

// ... or, with daylight saving adjustment
$bree = mktime(4, 35, 25, 6, 16, 1981, TRUE);
$nat = mktime(3, 45, 50, 1, 18, 1973, TRUE);

$difference = $bree - $nat;

// 'dateInterval' custom function computes intervals in several measures given an
// interval in seconds. Note, 'month' and 'year' measures not provided
printf("There were %d seconds between Nat and Bree\n", $difference);
printf("There were %d weeks between Nat and Bree\n", dateInterval('weeks', $difference));
printf("There were %d days between Nat and Bree\n", dateInterval('days', $difference));
printf("There were %d hours between Nat and Bree\n", dateInterval('hours', $difference));
printf("There were %d minutes between Nat and Bree\n", dateInterval('mins', $difference));

// @@PLEAC@@_3.6
// 'getdate' accepts a timestamp [or implicitly calls 'time'] and returns an array of
// date components. It returns much the same information as 'strptime' except that
// the component names are different

$today = getdate();

$weekday = $today['wday'];
$monthday = $today['mday'];
$yearday = $today['yday'];

$weeknumber = (int) round($yearday / 7.0);

// Safter method of obtaining week number
$weeknumber = strftime('%U') + 1;

// ----------------------------

define(SEP, '/');

$day = 16;
$month = 6;
$year = 1981;

$timestamp = mktime(0, 0, 0, $month, $day, $year);

$date = getdate($timestamp);

$weekday = $date['wday'];
$monthday = $date['mday'];
$yearday = $date['yday'];

$weeknumber = (int) round($yearday / 7.0);

$weeknumber = strftime('%U', $timestamp) + 1;

// Interpolate ...
$sep = SEP;
echo "{$month}{$sep}{$day}{$sep}{$year} was a {$date['weekday']} in week {$weeknumber}\n";

// ... or, concatenate
echo $month . SEP . $day . SEP . $year . ' was a ' . $date['weekday']
     . ' in week ' . $weeknumber . "\n";

// @@PLEAC@@_3.7
// 'strtotime' parses a textual date expression by attempting a 'best guess' at
// the format, and either fails, or generates a timestamp. Timestamp could be fed
// into any one of the various functions; example:
$timestamp = strtotime('1998-06-03'); echo strftime('%Y-%m-%d', $timestamp) . "\n";

// 'strptime' parses a textual date expression according to a specified format,
// and returns an array of date components; components can be easily dumped
print_r(strptime('1998-06-03', '%Y-%m-%d'));

// ----------------------------

// Parse date string according to format
$darr = strptime('1998-06-03', '%Y-%m-%d');

if (!empty($darr))
{
  // Show date components in 'debug' form
  print_r($darr);

  // Check whether there was a parse error i.e. one or more components could not
  // be extracted from the string
  if (empty($darr['unparsed']))
  {
    // Properly parsed date, so validate required components using, 'checkdate'
    if (checkdate($darr['tm_mon'] + 1, $darr['tm_mday'], $darr['tm_year'] + 1900))
      echo "Parsed date verified as correct\n";
    else
      echo "Parsed date failed verification\n";
  }
  else
  {
    echo "Date string parse not complete; failed components: {$darr['unparsed']}\n";
  }
}
else
{
  echo "Date string could not be parsed\n";
}

// @@PLEAC@@_3.8
// 'date' and 'strftime' both print a date string based on:
// * Format String, describing layout of date components
// * Timestamp [*NIX Epoch Seconds], either given explicitly, or implictly
//   via a call to 'time' which retrieves current time value

$ts = 1234567890;

date('Y/m/d', $ts); 
date('Y/m/d', mktime($h, $m, $s, $mth, $d, $y, $is_dst)); 

date('Y/m/d');         // same as: date('Y/m/d', time());

// ------------

$ts = 1234567890;

strftime('%Y/%m/%d', $ts);
strftime('%Y/%m/%d', mktime($h, $m, $s, $mth, $d, $y, $is_dst));

strftime('%Y/%m/%d');  // same as: strftime('%Y/%m/%d', time());

// ----------------------------

// 'mktime' creates a local time timestamp
$t = strftime('%a %b %e %H:%M:%S %z %Y', mktime(3, 45, 50, 1, 18, 73, TRUE));
echo "{$t}\n";

// 'gmmktime' creates a GMT time timestamp
$t = strftime('%a %b %e %H:%M:%S %z %Y', gmmktime(3, 45, 50, 1, 18, 73));
echo "{$t}\n";

// ----------------------------

// 'strtotime' parses a textual date expression, and generates a timestamp 
$t = strftime('%A %D', strtotime('18 Jan 1973, 3:45:50'));
echo "{$t}\n";

// This should generate output identical to previous example
$t = strftime('%A %D', mktime(3, 45, 50, 1, 18, 73, TRUE));
echo "{$t}\n";

// @@PLEAC@@_3.9
// PHP 5 and above can use the built-in, 'microtime'. Crude implementation for ealier versions:
// function microtime() { $t = gettimeofday(); return (float) ($t['sec'] + $t['usec'] / 1000000.0); } 

// ------------

$before = microtime();

$line = fgets(STDIN);

$elapsed = microtime() - $before;

printf("You took %.3f seconds\n", $elapsed);

// ------------

define(NUMBER_OF_TIMES, 100);
define(SIZE, 500);

for($i = 0; $i < NUMBER_OF_TIMES; $i++)
{
  $arr = array();
  for($j = 0; $j < SIZE; $j++) $arr[] = rand();

  $begin = microtime();
  sort($arr);
  $elapsed = microtime() - $begin;

  $total_time += $elapsed;
}

printf("On average, sorting %d random numbers takes %.5f seconds\n", SIZE, $total_time / (float) NUMBER_OF_TIMES);

// @@PLEAC@@_3.10
// Low-resolution: sleep time specified in seconds
sleep(1);

// High-resolution: sleep time specified in microseconds [not reliable under Windows]
usleep(250000);

// @@PLEAC@@_3.11
// @@INCOMPLETE@@
// @@INCOMPLETE@@

// @@PLEAC@@_4.0
// Nested arrays are supported, and may be easily printed using 'print_r'

$nested = array('this', 'that', 'the', 'other');

$nested = array('this', 'that', array('the', 'other')); print_r($nested);

$tune = array('The', 'Star-Spangled', 'Banner');

// @@PLEAC@@_4.1
// PHP offers only the 'array' type which is actually an associative array, though
// may be numerically indexed, to mimic vectors and matrices; there is no separate
// 'list' type

$a = array('quick', 'brown', 'fox');

// ------------

$a = escapeshellarg('Why are you teasing me?');

// ------------

$lines = <<<END_OF_HERE_DOC
    The boy stood on the burning deck,
    it was as hot as glass.
END_OF_HERE_DOC;

// ------------

$bigarray = array_map('rtrim', file('mydatafile'));

// ------------

$banner = 'The mines of Moria';

$banner = escapeshellarg('The mines of Moria');

// ------------

$name = 'Gandalf';

$banner = "Speak {$name}, and enter!";

$banner = 'Speak ' . escapeshellarg($name) . ' and welcome!';

// ------------

$his_host = 'www.perl.com';

$host_info = `nslookup $his_host`;

$cmd = 'ps ' . posix_getpid(); $perl_info = `$cmd`;

$shell_info = `ps $$`;

// ------------

$banner = array('Costs', 'only', '$4.95');

$banner = array_map('escapeshellarg', split(' ', 'Costs only $4.95'));

// ------------

// AFAIK PHP doesn't support non-quoted strings ala Perl's 'q', 'qq' and 'qw', so arrays
// created from strings must use quoted strings, and make use of 'split' [or equivalent].
// A slew of functions exist for performing string quoting, including 'escapeshellarg',
// 'quotemeta', and 'preg_quote'

$brax = split(' ', '( ) < > { } [ ]');

// Do this to quote each element within '..'
// $brax = array_map('escapeshellarg', split(' ', '( ) < > { } [ ]'));

$rings = split(' ', 'Nenya Narya Vilya');

$tags = split(' ', 'LI TABLE TR TD A IMG H1 P');

$sample = split(' ', 'The vertical bar | looks and behaves like a pipe.');

// @@PLEAC@@_4.2
function commify_series($list)
{
  $n = str_word_count($list); $series = str_word_count($list, 1);

  if ($n == 0) return NULL;
  if ($n == 1) return $series[0];
  if ($n == 2) return $series[0] . ' and ' . $series[1];
  
  return join(', ', array_slice($series, 0, -1)) . ', and ' . $series[$n - 1];
}

// ------------

echo commify_series('red') . "\n";
echo commify_series('red yellow') . "\n";
echo commify_series('red yellow green') . "\n";

$mylist = 'red yellow green';
echo 'I have ' . commify_series($mylist) . " marbles.\n";

// ----------------------------

function commify_series($arr)
{
  $n = count($arr); $sepchar = ',';

  foreach($arr as $str)
  {
    if (strpos($str, ',') === false) continue;
    $sepchar = ';'; break; 
  }

  if ($n == 0) return NULL;
  if ($n == 1) return $arr[0];
  if ($n == 2) return $arr[0] . ' and ' . $arr[1];
  
  return join("{$sepchar} ", array_slice($arr, 0, -1)) . "{$sepchar} and " . $arr[$n - 1];
}

// ------------

$lists = array(
  array('just one thing'),
  split(' ', 'Mutt Jeff'),
  split(' ', 'Peter Paul Mary'),
  array('To our parents', 'Mother Theresa', 'God'),
  array('pastrami', 'ham and cheese', 'peanut butter and jelly', 'tuna'),
  array('recycle tired, old phrases', 'ponder big, happy thoughts'),
  array('recycle tired, old phrases', 'ponder big, happy thoughts', 'sleep and dream peacefully'));

foreach($lists as $arr)
{
  echo 'The list is: ' . commify_series($arr) . ".\n";
}

// @@PLEAC@@_4.3
// AFAICT you cannot grow / shrink an array to an arbitrary size. However, you can:
// * Grow an array by appending an element using subscrip notation, or using
//   either 'array_unshift' or 'array_push' to add one or more elements

$arr[] = 'one';
array_unshift($arr, 'one', 'two', 'three');
array_push($arr, 'one', 'two', 'three');

// * Shrink an array by using 'unset' to remove one or more specific elements, or
//   either 'array_shift' or 'array_pop' to remove an element from the ends

unset($arr[$idx1], $arr[$idx2], $arr[$idx3]);
$item = array_shift($arr);
$item = array_pop($arr);

// ----------------------------

function what_about_the_array()
{
  global $people;

  echo 'The array now has ' . count($people) . " elements\n";
  echo 'The index value of the last element is ' . (count($people) - 1) . "\n";
  echo 'Element #3 is ' . $people[3] . "\n";
}

$people = array('Crosby', 'Stills', 'Nash', 'Young');
what_about_the_array();

array_pop($people);
what_about_the_array();

// Cannot, AFAICT, resize the array to an arbitrary size

# @@PLEAC@@_4.4
foreach ($list as $item) {
    // do something with $item
}

// Environment listing example

// PHP defines a superglobal $_ENV to provide access to environment
// variables.

// Beware, array assignment means copying in PHP. You need to use
// the reference operator to avoid copying. But we want a copy here.
$env = $_ENV;

// PHP can sort an array by key, so you don't need to get keys,
// and then sort.
ksort($env);

foreach ($env as $key => $value) {
    echo "{$key}={$value}\n";
}

// Literal translation of Perl example would be:
$keys = array_keys($_ENV);
sort($keys);
foreach ($keys as $key) {
    echo "{$key}={$_ENV[$key]}\n";
}

// This assumes that MAX_QUOTA is a named constant.
foreach ($all_users as $user) {
    $disk_space = get_usage($user);
    if ($disk_space > MAX_QUOTA) {
        complain($user);
    }
}

// You can't modify array's elements in-place.
$array = array(1, 2, 3);
$newarray = array();
foreach ($array as $item) {
    $newarray[] = $item - 1;
}
print_r($newarray);

// Before PHP 5, that is. You can precede the reference operator
// before $item to get reference instead of copy.
$array = array(1, 2, 3);
foreach ($array as &$item) {
    $item--;
}
print_r($array);

// TODO: explain the old each() and list() iteration construct.
// foreach is new in PHP 4, and there are subtle differences.

// @@PLEAC@@_4.5
// Conventional 'read-only' access
foreach($array as $item)
{
  ; // Can access, but not update, array element referred to by '$item'
}

// ----

// '&' makes '$item' a reference
foreach($array as &$item)
{
  ; // Update array element referred to by '$item'
}

// ------------

$arraylen = count($array);

for($i = 0; $i < $arraylen; $i++)
{
  ; // '$array' is updateable via subscript notation
}

// ----------------------------

$fruits = array('Apple', 'Raspberry');

foreach($fruits as &$fruit)
{
  echo "{$fruit} tastes good in a pie.\n";
}

$fruitlen = count($fruits);

for($i = 0; $i < $fruitlen; $i++)
{
  echo "{$fruits[$i]} tastes good in a pie.\n";
}

// ----------------------------

$rogue_cats = array('Blackie', 'Goldie', 'Silkie');

// Take care to assign reference to '$rogue_cats' array via '=&'
$namelist['felines'] =& $rogue_cats;

// Take care to make '$cat' a reference via '&$' to allow updating
foreach($namelist['felines'] as &$cat)
{
  $cat .= ' [meow]';
}

// Via array reference
foreach($namelist['felines'] as $cat)
{
  echo "{$cat} purrs hypnotically.\n";
}

echo "---\n";

// Original array
foreach($rogue_cats as $cat)
{
  echo "{$cat} purrs hypnotically.\n";
}

// @@PLEAC@@_4.6
// PHP offers the 'array_unique' function to perform this task. It works with both keyed,
// and numerically-indexed arrays; keys / indexes are preserved; use of 'array_values' 
// is recommended to reindex numerically-indexed arrays since there will likely be missing
// indexes

// Remove duplicate values
$unique = array_unique($array);

// Remove duplicates, and reindex [for numerically-indexed arrays only]
$unique = array_values(array_unique($array));

// or use:
$unique = array_keys(array_flip($array));

// ----------------------------

// Selected Perl 'seen' examples
foreach($list as $item)
{
  if (!isset($seen[$item]))
  {
    $seen[$item] = TRUE;
    $unique[] = $item;
  }
}

// ------------

foreach($list as $item)
{
  $seen[$item] || (++$seen[$item] && ($unique[] = $item));
}

// ------------

function some_func($item)
{
  ; // Do something with '$item'
}

foreach($list as $item)
{
  $seen[$item] || (++$seen[$item] && some_func($item));
}

// ----------------------------

foreach(array_slice(preg_split('/\n/', `who`), 0, -1) as $user_entry)
{
  $user = preg_split('/\s/', $user_entry);
  $ucnt[$user[0]]++;
}

ksort($ucnt);

echo "users logged in:\n";

foreach($ucnt as $user => $cnt)
{
  echo "\t{$user} => {$cnt}\n";
}

// @@PLEAC@@_4.7
// PHP offers the 'array_diff' and 'array_diff_assoc' functions to perform this task. Same
// points as made about 'array_unique' apply here also

$a = array('c', 'a', 'b', 'd');
$b = array('c', 'a', 'b', 'e');

$diff = array_diff($a, $b);                 // $diff -> [3] 'd'
$diff = array_diff($b, $a);                 // $diff -> [3] 'e'

// Numerically-indexed array, reindexed
$diff = array_values(array_diff($a, $b));   // $diff -> [0] 'd'
$diff = array_values(array_diff($b, $a));   // $diff -> [0] 'e'

// ----------------------------

// 1st Perl 'seen' example only

$a = array('k1' => 11, 'k2' => 12, 'k4' => 14);
$b = array('k1' => 11, 'k2' => 12, 'k3' => 13);

foreach($b as $item => $value) { $seen[$item] = 1; }

// Stores key only e.g. $aonly[0] contains 'k4', same as Perl example
foreach($a as $item => $value) { if (!$seen[$item]) $aonly[] = $item; }

// Stores key and value e.g. $aonly['k4'] contains 14, same entry as in $a
foreach($a as $item => $value) { if (!$seen[$item]) $aonly[$item] = $value; }

// ----------------------------

// Conventional way: $hash = array('key1' => 1, 'key2' => 2);

$hash['key1'] = 1;
$hash['key2'] = 2;

$hash = array_combine(array('key1', 'key2'), array(1, 2));

// ------------

$seen = array_slice($b, 0);

$seen = array_combine(array_keys($b), array_fill(0, count($b), 1));

// @@PLEAC@@_4.8
// PHP offers a number of array-based 'set operation' functions:
// * union:        array_unique(array_merge(...))
// * intersection: array_intersect and family
// * difference:   array_diff and family
// which may be used for this type of task. Also, if returned arrays need to be
// reindexed, 'array_slice($array, 0)', or 'array_values($array)' are useful

$a = array(1, 3, 5, 6, 7, 8);
$b = array(2, 3, 5, 7, 9);

$union = array_values(array_unique(array_merge($a, $b))); // 1, 2, 3, 5, 6, 7, 8, 9
$isect = array_values(array_intersect($a, $b));           // 3, 5, 7
$diff = array_values(array_diff($a, $b));                 // 1, 8

// @@PLEAC@@_4.9
// PHP offers the 'array_merge' function to perform this task. Duplicate values are retained,
// but if arrays are numerically-indexed, resulting array is reindexed

$arr1 = array('c', 'a', 'b', 'd');
$arr2 = array('c', 'a', 'b', 'e');

$new = array_merge($arr1, $arr2);     // $new -> 'c', 'a', 'b', 'd', 'c', 'a', 'b', 'd'

// ----------------------------

$members = array('Time', 'Flies');
$initiates = array('An', 'Arrow');

$members = array_merge($members, $initiates);

// ------------

$members = array('Time', 'Flies');
$initiates = array('An', 'Arrow');

// 'array_splice' is the PHP equivalent to Perl's 'splice'
array_splice($members, 2, 0, array_merge(array('Like'), $initiates));
echo join(' ', $members) . "\n";

array_splice($members, 0, 1, array('Fruit'));
array_splice($members, -2, 2, array('A', 'Banana'));
echo join(' ', $members) . "\n";

// @@PLEAC@@_4.10
$reversed = array_reverse($array);

// ----------------------------

foreach(array_reverse($array) as $item)
{
  ; // ... do something with '$item' ...
}

// ------------

for($i = count($array) - 1; $i >= 0; $i--)
{
  ; // ... do something with '$array[$i]' ...
}

// ----------------------------

sort($array);
$array = array_reverse($array);

// ------------

rsort($array);

// @@PLEAC@@_4.11
// Array elements can be deleted using 'unset'; removing several elements would require applying
// 'unset' several times, probably in a loop. However, they would most likely also need to be
// reindexed, so a better approach would be to use 'array_slice' which avoids explicit looping.
// Where elements need to be removed, and those elements also returned, it is probably best to
// combine both operations in a function. This is the approach taken here in implementing both
// 'shiftN' and 'popN', and it is these functions that are used in the examples

function popN(&$arr, $n)
{
  $ret = array_slice($arr, -($n), $n);
  $arr = array_slice($arr, 0, count($arr) - $n);
  return $ret;
}

function shiftN(&$arr, $n)
{
  $ret = array_slice($arr, 0, $n);
  $arr = array_slice($arr, $n);
  return $ret;
}

// ------------

// Remove $n elements from the front of $array; return them in $fron
$front = shiftN($array, $n);

// Remove $n elements from the end of $array; return them in $end
$end = popN($array, $n);

// ------------

$friends = array('Peter', 'Paul', 'Mary', 'Jim', 'Tim');

list($this_, $that) = shiftN($friends, 2);

echo "{$this_} {$that}\n";

// ------------

$beverages = array('Dew', 'Jolt', 'Cola', 'Sprite', 'Fresca');

$pair = popN($beverages, 2);

echo join(' ', $pair) . "\n";

// @@PLEAC@@_4.12
// This section illustrates various 'find first' techniques. The Perl examples all use an
// explicit loop and condition testing [also repeated here]. This is the simplest, and
// [potentially] most efficient approach because the search can be terminated as soon as a
// match is found. However, it is worth mentioning a few alternatives:
// * 'array_search' performs a 'find first' using the element value rather than a condition
//    check, so isn't really applicable here
// * 'array_filter', whilst using a condition check, actually performs a 'find all', though
//   all but the first returned element can be discarded. This approach is actually less error
//   prone than using a loop, but the disadvantage is that each element is visited: there is no
//   means of terminating the search once a match has been found. It would be nice if this
//   function were to have a third parameter, a Boolean flag indicating whether to traverse
//   the whole array, or quit after first match [next version, maybe :) ?]

$found = FALSE;

foreach($array as $item)
{
  // Not found - skip to next item
  if (!$criterion) continue;

  // Found - save and leave
  $match = $item;
  $found = TRUE;
  break;  
}

if ($found)
{
  ; // do something with $match
}
else
{
  ; // not found
}

// ------------

function predicate($element)
{
  if (criterion) return TRUE;
  return FALSE;
}

$match = array_slice(array_filter($array, 'predicate'), 0, 1);

if ($match)
{
  ; // do something with $match[0]
}
else
{
  ; // $match is empty - not found
}

// ----------------------------

class Employee
{
  public $name, $age, $ssn, $salary;

  public function __construct($name, $age, $ssn, $salary, $category)
  {
    $this->name = $name;
    $this->age = $age;
    $this->ssn = $ssn;
    $this->salary = $salary;
    $this->category = $category;
  }
}

// ------------

$employees = array(
  new Employee('sdf', 27, 12345, 47000, 'Engineer'),
  new Employee('ajb', 32, 12376, 51000, 'Programmer'),
  new Employee('dgh', 31, 12355, 45000, 'Engineer'));

// ------------

function array_update($arr, $lambda, $updarr)
{
  foreach($arr as $key) $lambda($updarr, $key);
  return $updarr;
}

function highest_salaried_engineer(&$arr, $employee)
{
  static $highest_salary = 0;
  
  if ($employee->category == 'Engineer')
  {
    if ($employee->salary > $highest_salary)
    {
      $highest_salary = $employee->salary;
      $arr[0] = $employee;
    }
  }
}

// ------------

// 'array_update' custom function is modelled on 'array_reduce' except that it allows the
// return of an array, contents and length of which are entirely dependant on what the
// callback function does. Here, it is logically working in a 'find first' capacity
$highest_salaried_engineer = array_update($employees, 'highest_salaried_engineer', array());

echo 'Highest paid engineer is: ' . $highest_salaried_engineer[0]->name . "\n";

// @@PLEAC@@_4.13
// PHP implements 'grep' functionality [as embodied in the current section] in the 'array_filter'
// function

function predicate($element)
{
  if (criterion) return TRUE;
  return FALSE;
}

$matching = array_filter($list, 'predicate');

// ------------

$bigs = array_filter($nums, create_function('$n', 'return $n > 1000000;'));

// ------------

function is_pig($user)
{
  $user_details = preg_split('/(\s)+/', $user);
  // Assuming field 5 is the resource being compared
  return $user_details[5] > 1e7;  
}

$pigs = array_filter(array_slice(preg_split('/\n/', `who -u`), 0, -1), 'is_pig');

// ------------

$matching = array_filter(array_slice(preg_split('/\n/', `who`), 0, -1),
                         create_function('$user', 'return preg_match(\'/^gnat /\', $user);'));

// ------------

class Employee
{
  public $name, $age, $ssn, $salary;

  public function __construct($name, $age, $ssn, $salary, $category)
  {
    $this->name = $name;
    $this->age = $age;
    $this->ssn = $ssn;
    $this->salary = $salary;
    $this->category = $category;
  }
}

// ------------

$employees = array(
  new Employee('sdf', 27, 12345, 47000, 'Engineer'),
  new Employee('ajb', 32, 12376, 51000, 'Programmer'),
  new Employee('dgh', 31, 12355, 45000, 'Engineer'));

// ------------

$engineers = array_filter($employees,
                          create_function('$employee', 'return $employee->category == "Engineer";'));

// @@PLEAC@@_4.14
// PHP offers a rich set of sorting functions. Key features:
// * Inplace sorts; the original array, not a a copy, is sorted
// * Separate functions exist for sorting [both ascending and descending order]:
//   - By value, assign new keys / indices [sort, rsort]
//   - By key   [ksort, krsort] (for non-numerically indexed arrays)
//   - By value [asort, arsort]
//   - As above, but using a user-defined comparator [i.e. callback function]
//     [usort, uasort, uksort]
//   - Natural order sort [natsort]
// * Significantly, if sorting digit-only elements, whether strings or numbers,
//   'natural order' [i.e. 1 before 10 before 100 (ascending)] is retained. If
//   the elements are alphanumeric e.g. 'z1', 'z10' then 'natsort' should be
//   used [note: beware of 'natsort' with negative numbers; prefer 'sort' or 'asort']

$unsorted = array(7, 12, -13, 2, 100, 5, 1, -2, 23, 3, 6, 4);

sort($unsorted);                 // -13, -2, 1, 2, 3, 4, 5, 6, 7, 12, 23, 100
rsort($unsorted);                // 100, 23, 12, 7, 6, 5, 4, 3, 2, 1, -2, -13

asort($unsorted);                // -13, -2, 1, 2, 3, 4, 5, 6, 7, 12, 23, 100
arsort($unsorted);               // 100, 23, 12, 7, 6, 5, 4, 3, 2, 1, -2, -13

natsort($unsorted);              // -2, -13, 1, 2, 3, 4, 5, 6, 7, 12, 23, 100

// ------------

function ascend($left, $right) { return $left > $right; }
function descend($left, $right) { return $left < $right; }

// ------------

usort($unsorted, 'ascend');      // -13, -2, 1, 2, 3, 4, 5, 6, 7, 12, 23, 100
usort($unsorted, 'descend');     // 100, 23, 12, 7, 6, 5, 4, 3, 2, 1, -2, -13

uasort($unsorted, 'ascend');     // -13, -2, 1, 2, 3, 4, 5, 6, 7, 12, 23, 100
uasort($unsorted, 'descend');    // 100, 23, 12, 7, 6, 5, 4, 3, 2, 1, -2, -13

// ----------------------------

function kill_process($pid)
{
  // Is 'killable' ?
  if (!posix_kill($pid, 0)) return;

  // Ok, so kill in two stages
  posix_kill($pid, 15); // SIGTERM
  sleep(1);
  posix_kill($pid, 9);  // SIGKILL
}

function pid($pentry)
{
  $p = preg_split('/\s/', trim($pentry));
  return $p[0];
}

$processes = array_map('pid', array_slice(preg_split('/\n/', `ps ax`), 1, -1));
sort($processes);

echo join(' ,', $processes) . "\n";

echo 'Enter a pid to kill: ';
if (($pid = trim(fgets(STDIN))))
  kill_process($pid);

// @@PLEAC@@_4.15
// Tasks in this section would typically use the PHP 'usort' family of functions
// which are used with a comparator function so as to perform custom comparisions.
// A significant difference from the Perl examples is that these functions are
// inplace sorters, so it is the original array that is modified. Where this must
// be prevented a copy of the array can be made and sorted

function comparator($left, $right)
{
  ; // Compare '$left' with '$right' returning result
}

// ------------

$ordered = array_slice($unordered);
usort($ordered, 'comparator');

// ----------------------------

// The Perl example looks like it is creating a hash using computed values as the key,
// array values as the value, sorting on the computed key, then extracting the sorted
// values and placing them back into an array

function compute($value)
{
  ; // Return computed value utilising '$value'
}

// ------------

// Original numerically-indexed array [sample data used]
$unordered = array(5, 3, 7, 1, 4, 2, 6);

// Create hash using 'compute' function to generate the keys. This example assumes that
// each value in the '$unordered' array is used in generating the corresponding '$key'
foreach($unordered as $value)
{
  $precomputed[compute($value)] = $value;
}

// Copy the hash, and sort it by key
$ordered_precomputed = array_slice($precomputed, 0); ksort($ordered_precomputed);

// Extract the values of the hash in current order placing them in a new numerically-indexed
// array
$ordered = array_values($ordered_precomputed);

// ----------------------------

// As above, except uses 'array_update' and 'accum' to help create hash

function array_update($arr, $lambda, $updarr)
{
  foreach($arr as $key) $lambda($updarr, $key);
  return $updarr;
}

function accum(&$arr, $value)
{
  $arr[compute($value)] = $value;
}

// ------------

function compute($value)
{
  ; // Return computed value utilising '$value'
}

// ------------

// Original numerically-indexed array [sample data used]
$unordered = array(5, 3, 7, 1, 4, 2, 6);

// Create hash
$precomputed = array_update($unordered, 'accum', array());

// Copy the hash, and sort it by key
$ordered_precomputed = array_slice($precomputed, 0); ksort($ordered_precomputed);

// Extract the values of the hash in current order placing them in a new numerically-indexed
// array
$ordered = array_values($ordered_precomputed);

// ----------------------------

class Employee
{
  public $name, $age, $ssn, $salary;

  public function __construct($name, $age, $ssn, $salary)
  {
    $this->name = $name;
    $this->age = $age;
    $this->ssn = $ssn;
    $this->salary = $salary;
  }
}

// ------------

$employees = array(
  new Employee('sdf', 27, 12345, 47000),
  new Employee('ajb', 32, 12376, 51000),
  new Employee('dgh', 31, 12355, 45000));

// ------------

$ordered = array_slice($employees, 0);
usort($ordered, create_function('$left, $right', 'return $left->name > $right->name;'));

// ------------

$sorted_employees = array_slice($employees, 0);
usort($sorted_employees, create_function('$left, $right', 'return $left->name > $right->name;'));

$bonus = array(12376 => 5000, 12345 => 6000, 12355 => 0);

foreach($sorted_employees as $employee)
{
  echo "{$employee->name} earns \${$employee->salary}\n";
}

foreach($sorted_employees as $employee)
{
  if (($amount = $bonus[$employee->ssn]))
    echo "{$employee->name} got a bonus of: \${$amount}\n";
}

// ------------

$sorted = array_slice($employees, 0);
usort($sorted, create_function('$left, $right', 'return $left->name > $right->name || $left->age != $right->age;'));

// ----------------------------

// PHP offers a swag of POSIX functions for obtaining user information [i.e. they all read
// the '/etc/passwd' file for the relevant infroamtion], and it is these that should rightly
// be used for this purpose. However, since the intent of this section is to illustrate array
// manipulation, these functions won't be used. Instead a custom function mimicing Perl's
// 'getpwent' function will be implemented so the code presented here can more faithfully match
// the Perl code

function get_pw_entries()
{
  function normal_users_only($e)
  {
    $entry = split(':', $e); return $entry[2] > 100 && $entry[2] < 32768;
  }

  foreach(array_filter(file('/etc/passwd'), 'normal_users_only') as $entry)
    $users[] = split(':', trim($entry));

  return $users;
}

// ------------

$users = get_pw_entries();

usort($users, create_function('$left, $right', 'return $left[0] > $right[0];'));
foreach($users as $user) echo "{$user[0]}\n";

// ----------------------------

$names = array('sdf', 'ajb', 'dgh');

$sorted = array_slice($names, 0);
usort($sorted, create_function('$left, $right', 'return substr($left, 1, 1) > substr($right, 1, 1);'));

// ------------

$strings = array('bbb', 'aa', 'c');

$sorted = array_slice($strings, 0);
usort($sorted, create_function('$left, $right', 'return strlen($left) > strlen($right);'));

// ----------------------------

function array_update($arr, $lambda, $updarr)
{
  foreach($arr as $key) $lambda($updarr, $key);
  return $updarr;
}

function accum(&$arr, $value)
{
  $arr[strlen($value)] = $value;
}

// ----

$strings = array('bbb', 'aa', 'c');

$temp = array_update($strings, 'accum', array());
ksort($temp);
$sorted = array_values($temp);

// ----------------------------

function array_update($arr, $lambda, $updarr)
{
  foreach($arr as $key) $lambda($updarr, $key);
  return $updarr;
}

function accum(&$arr, $value)
{
  if (preg_match('/(\d+)/', $value, $matches))
    $arr[$matches[1]] = $value;
}

// ----

$fields = array('b1b2b', 'a4a', 'c9', 'ddd', 'a');

$temp = array_update($fields, 'accum', array());
ksort($temp);
$sorted_fields = array_values($temp);

// @@PLEAC@@_4.16
array_unshift($a1, array_pop($a1));  // last -> first
array_push($a1, array_shift($a1));   // first -> last

// ----------------------------

function grab_and_rotate(&$arr)
{
  $item = $arr[0];
  array_push($arr, array_shift($arr));
  return $item;
}

// ------------

$processes = array(1, 2, 3, 4, 5);

while (TRUE)
{
  $process = grab_and_rotate($processes);
  echo "Handling process {$process}\n";
  sleep(1);
}

// @@PLEAC@@_4.17
// PHP offers the 'shuffle' function to perform this task

$arr = array(1, 2, 3, 4, 5, 6, 7, 8, 9);

shuffle($arr);

echo join(' ', $arr) . "\n";

// ----------------------------

// Perl example equivalents
function fisher_yates_shuffle(&$a)
{
  $size = count($a) - 1;

  for($i = $size; $i >= 0; $i--)
  {
    if (($j = rand(0, $i)) != $i)
      list($a[$i], $a[$j]) = array($a[$j], $a[$i]);
  }
}

function naive_shuffle(&$a)
{
  $size = count($a);

  for($i = 0; $i < $size; $i++)
  {
    $j = rand(0, $size - 1);
    list($a[$i], $a[$j]) = array($a[$j], $a[$i]);
  }
}

// ------------

$arr = array(1, 2, 3, 4, 5, 6, 7, 8, 9);

fisher_yates_shuffle($arr);
echo join(' ', $arr) . "\n";

naive_shuffle($arr);
echo join(' ', $arr) . "\n";

// @@PLEAC@@_4.18
// @@INCOMPLETE@@
// @@INCOMPLETE@@

// @@PLEAC@@_4.19
// @@INCOMPLETE@@
// @@INCOMPLETE@@

// @@PLEAC@@_5.0
// PHP uses the term 'array' to refer to associative arrays - referred to in Perl
// as 'hashes' - and for the sake of avoiding confusion, the Perl terminology will
// be used. As a matter of interest, PHP does not sport a vector, matrix, or list
// type: the 'array' [Perl 'hash'] serves all these roles

$age = array('Nat' => 24, 'Jules' => 25, 'Josh' => 17);

$age['Nat'] = 24;
$age['Jules'] = 25;
$age['Josh'] = 17;

$age = array_combine(array('Nat', 'Jules', 'Josh'), array(24, 25, 17));

// ------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

$food_colour['Apple'] = 'red'; $food_colour['Banana'] = 'yellow';
$food_colour['Lemon'] = 'yellow'; $food_colour['Carrot'] = 'orange';

$food_colour = array_combine(array('Apple', 'Banana', 'Lemon', 'Carrot'),
                             array('red', 'yellow', 'yellow', 'orange'));

// @@PLEAC@@_5.1
$hash[$key] = $value;

// ------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

$food_colour['Raspberry'] = 'pink';

echo "Known foods:\n";
foreach($food_colour as $food => $colour) echo "{$food}\n";

// @@PLEAC@@_5.2
// Returns TRUE on all existing entries with non-NULL values
if (isset($hash[$key]))
  ; // entry exists  
else
  ; // no such entry 

// ------------

// Returns TRUE on all existing entries regardless of attached value
if (array_key_exists($key, $hash))
  ; // entry exists  
else
  ; // no such entry 

// ----------------------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

foreach(array('Banana', 'Martini') as $name)
{
  if (isset($food_colour[$name]))
    echo "{$name} is a food.\n";
  else
    echo "{$name} is a drink.\n";
}

// ----------------------------

$age = array('Toddler' => 3, 'Unborn' => 0, 'Phantasm' => NULL);

foreach(array('Toddler', 'Unborn', 'Phantasm', 'Relic') as $thing)
{
  echo "{$thing}:";
  if (array_key_exists($thing, $age)) echo ' exists';
  if (isset($age[$thing])) echo ' non-NULL';
  if ($age[$thing]) echo ' TRUE';
  echo "\n";
}

// @@PLEAC@@_5.3
// Remove one, or more, hash entries
unset($hash[$key]);

unset($hash[$key1], $hash[$key2], $hash[$key3]);

// Remove entire hash
unset($hash);

// ----------------------------

function print_foods()
{
  // Perl example uses a global variable
  global $food_colour;

  $foods = array_keys($food_colour);

  echo 'Foods:';
  foreach($foods as $food) echo " {$food}";

  echo "\nValues:\n";
  foreach($foods as $food)
  {
    $colour = $food_colour[$food];

    if (isset($colour))
      echo "  {$colour}\n";
    else
      echo "  nullified or removed\n";
  }
}

// ------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

echo "Initially:\n"; print_foods();

// Nullify an entry
$food_colour['Banana'] = NULL;
echo "\nWith 'Banana' nullified\n";
print_foods();

// Remove an entry
unset($food_colour['Banana']);
echo "\nWith 'Banana' removed\n";
print_foods();

// Destroy the hash
unset($food_colour);

// @@PLEAC@@_5.4
// Access keys and values
foreach($hash as $key => $value)
{
  ; // ...
}

// Access keys only
foreach(array_keys($hash) as $key)
{
  ; // ...
}

// Access values only
foreach($hash as $value)
{
  ; // ...
}

// ----------------------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

foreach($food_colour as $food => $colour)
{
  echo "{$food} is {$colour}\n";
}

foreach(array_keys($food_colour) as $food)
{
  echo "{$food} is {$food_colour[$food]}\n";
}

// ----------------------------

// 'countfrom' - count number of messages from each sender

$line = fgets(STDIN);

while (!feof(STDIN))
{
  if (preg_match('/^From: (.*)/', $line, $matches))
  {
    if (isset($from[$matches[1]]))
      $from[$matches[1]] += 1;
    else
      $from[$matches[1]] = 1;
  }

  $line = fgets(STDIN);
}

if (isset($from))
{
  echo "Senders:\n";  
  foreach($from as $sender => $count) echo "{$sender} : {$count}\n";
}
else
{
  echo "No valid data entered\n";
}

// @@PLEAC@@_5.5
// PHP offers, 'print_r', which prints hash contents in 'debug' form; it also
// works recursively, printing any contained arrays in similar form
//     Array
//     (
//         [key1] => value1 
//         [key2] => value2
//         ...
//     )

print_r($hash);

// ------------

// Based on Perl example; non-recursive, so contained arrays not printed correctly
foreach($hash as $key => $value)
{
  echo "{$key} => $value\n";
}

// ----------------------------

// Sorted by keys

// 1. Sort the original hash
ksort($hash);

// 2. Extract keys, sort, traverse original in key order
$keys = array_keys($hash); sort($keys);

foreach($keys as $key)
{
  echo "{$key} => {$hash[$key]}\n";
}

// Sorted by values

// 1. Sort the original hash
asort($hash);

// 2. Extract values, sort, traverse original in value order [warning: finds 
//    only first matching key in the case where duplicate values exist]
$values = array_values($hash); sort($values);

foreach($values as $value)
{
  echo $value . ' <= ' . array_search($value, $hash) . "\n";
}

// @@PLEAC@@_5.6
// Unless sorted, hash elements remain in the order of insertion. If care is taken to
// always add a new element to the end of the hash, then element order is the order
// of insertion. The following function, 'array_push_associative' [modified from original
// found at 'array_push' section of PHP documentation], does just that
function array_push_associative(&$arr)
{
  foreach (func_get_args() as $arg)
  {
    if (is_array($arg))
      foreach ($arg as $key => $value) { $arr[$key] = $value; $ret++; }
    else
      $arr[$arg] = '';
  }

  return $ret;
}

// ------------

$food_colour = array();

// Individual calls, or ...
array_push_associative($food_colour, array('Banana' => 'Yellow'));
array_push_associative($food_colour, array('Apple' => 'Green'));
array_push_associative($food_colour, array('Lemon' => 'Yellow'));

// ... one call, one array; physical order retained
// array_push_associative($food_colour, array('Banana' => 'Yellow', 'Apple' => 'Green', 'Lemon' => 'Yellow'));

print_r($food_colour);

echo "\nIn insertion order:\n";
foreach($food_colour as $food => $colour) echo "  {$food} => {$colour}\n";

$foods = array_keys($food_colour);

echo "\nStill in insertion order:\n";
foreach($foods as $food) echo "  {$food} => {$food_colour[$food]}\n";

// @@PLEAC@@_5.7
foreach(array_slice(preg_split('/\n/', `who`), 0, -1) as $entry)
{
  list($user, $tty) = preg_split('/\s/', $entry);
  $ttys[$user][] = $tty;

  // Could instead do this:
  // $user = array_slice(preg_split('/\s/', $entry), 0, 2);
  // $ttys[$user[0]][] = $user[1];
}

ksort($ttys);

// ------------

foreach($ttys as $user => $all_ttys)
{
  echo "{$user}: " . join(' ', $all_ttys) . "\n";
}

// ------------

foreach($ttys as $user => $all_ttys)
{
  echo "{$user}: " . join(' ', $all_ttys) . "\n";

  foreach($all_ttys as $tty)
  {
    $stat = stat('/dev/$tty');
    $pwent = posix_getpwuid($stat['uid']);
    $user = isset($pwent['name']) ? $pwent['name'] : 'Not available';
    echo "{$tty} owned by: {$user}\n";
  }
}

// @@PLEAC@@_5.8
// PHP offers the 'array_flip' function to perform the task of exchanging the keys / values
// of a hash i.e. invert or 'flip' a hash

$reverse = array_flip($hash);

// ----------------------------

$surname = array('Babe' => 'Ruth', 'Mickey' => 'Mantle'); 
$first_name = array_flip($surname);

echo "{$first_name['Mantle']}\n";

// ----------------------------

$argc == 2 || die("usage: {$argv[0]} food|colour\n");

$given = $argv[1];

$colour = array('Apple' => 'red', 'Banana' => 'yellow',
                'Lemon' => 'yellow', 'Carrot' => 'orange');

$food = array_flip($colour);

if (isset($colour[$given]))
  echo "{$given} is a food with colour: {$colour[$given]}\n";

if (isset($food[$given]))
  echo "{$food[$given]} is a food with colour: {$given}\n";

// ----------------------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

foreach($food_colour as $food => $colour)
{
  $foods_with_colour[$colour][] = $food;
}

$colour = 'yellow';
echo "foods with colour {$colour} were: " . join(' ', $foods_with_colour[$colour]) . "\n";

// @@PLEAC@@_5.9
// PHP implements a swag of sorting functions, most designed to work with numerically-indexed
// arrays. For sorting hashes, the 'key' sorting functions are required:
// * 'ksort', 'krsort', 'uksort'

// Ascending order
ksort($hash);

// Descending order [i.e. reverse sort]
krsort($hash);

// Comparator-based sort

function comparator($left, $right)
{
  // Compare left key with right key
  return $left > $right;
}

uksort($hash, 'comparator');

// ----------------------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

// ------------

ksort($food_colour);

foreach($food_colour as $food => $colour)
{
  echo "{$food} is {$colour}\n";
}

// ------------

uksort($food_colour, create_function('$left, $right', 'return $left > $right;'));

foreach($food_colour as $food => $colour)
{
  echo "{$food} is {$colour}\n";
}

// @@PLEAC@@_5.10
// PHP offers the 'array_merge' function for this task [a related function, 'array_combine',
// may be used to create a hash from an array of keys, and one of values, respectively]

// Merge two, or more, arrays
$merged = array_merge($a, $b, $c);

// Create a hash from array of keys, and of values, respectively
$hash = array_combine($keys, $values);

// ------------

// Can always merge arrays manually 
foreach(array($h1, $h2, $h3) as $hash)
{
  foreach($hash as $key => $value)
  {
    // If same-key values differ, only latest retained
    $merged[$key] = $value;

    // Do this to append values for that key
    // $merged[$key][] = $value;
  }
}

// ----------------------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

$drink_colour = array('Galliano' => 'yellow', 'Mai Tai' => 'blue');

// ------------

$ingested_colour = array_merge($food_colour, $drink_colour);

// ------------

$substance_colour = array();

foreach(array($food_colour, $drink_colour) as $hash)
{
  foreach($hash as $substance => $colour)
  {
    if (array_key_exists($substance, $substance_colour))
    {
      echo "Warning {$substance_colour[$substance]} seen twice. Using first definition.\n";
      continue;
    }
    $substance_colour[$substance] = $colour;
  }
}

// @@PLEAC@@_5.11
// PHP offers a number of array-based 'set operation' functions:
// * union:        array_merge
// * intersection: array_intersect and family
// * difference:   array_diff and family
// which may be used for this type of task

// Keys occurring in both hashes
$common = array_intersect_key($h1, $h2);

// Keys occurring in the first hash [left side], but not in the second hash
$this_not_that = array_diff_key($h1, $h2);

// ----------------------------

$food_colour = array('Apple' => 'red', 'Banana' => 'yellow',
                     'Lemon' => 'yellow', 'Carrot' => 'orange');

$citrus_colour = array('Lemon' => 'yellow', 'Orange' => 'orange', 'Lime' => 'green');

$non_citrus = array_diff_key($food_colour, $citrus_colour);

// @@PLEAC@@_5.12
// PHP implements a special type known as a 'resource' that encompasses things like file handles,
// sockets, database connections, and many others. The 'resource' type is, essentially, a
// reference variable that is not readily serialisable. That is to say:
// * A 'resource' may be converted to a string representation via the 'var_export' function
// * That same string cannot be converted back into a 'resource'
// So, in terms of array handling, 'resource' types may be stored as array reference values,
// but cannot be used as keys. 
//
// I suspect it is this type of problem that the Perl::Tie package helps resolve. However, since
// PHP doesn't, AFAIK, sport a similar facility, the examples in this section cannot be
// implemented using file handles as keys

$filenames = array('/etc/termcap', '/vmlinux', '/bin/cat');

foreach($filenames as $filename)
{
  if (!($fh = fopen($filename, 'r'))) continue;

  // Cannot do this as required by the Perl code:
  // $name[$fh] = $filename;

  // Ok
  $name[$filename] = $fh;
}

// Would traverse array via:
//
// foreach(array_keys($name) as $fh)
// ...
// or
//
// foreach($name as $fh => $filename)
// ...
// but since '$fh' cannot be a key, either of these will work:
//
// foreach($name as $filename => $fh)
// or
foreach(array_values($name) as $fh)
{
  fclose($fh);
}

// @@PLEAC@@_5.13
// PHP hashes are dynamic expanding and contracting as entries are added, and removed,
// respectively. Thus, there is no need to presize a hash, nor is there, AFAIK, any
// means of doing so except by the number of datums used when defining the hash

// zero elements
$hash = array();            

// ------------

// three elements
$hash = array('Apple' => 'red', 'Lemon' => 'yellow', 'Carrot' => 'orange');

// @@PLEAC@@_5.14
foreach($array as $element) $count[$element] += 1;

// @@PLEAC@@_5.15
$father = array('Cain' => 'Adam', 'Abel' => 'Adam', 'Seth' => 'Adam', 'Enoch' => 'Cain',
                'Irad' => 'Enoch', 'Mehujael' => 'Irad', 'Methusael'=> 'Mehujael',
                'Lamech' => 'Methusael', 'Jabal' => 'Lamech', 'Jubal' => 'Lamech',
                'Tubalcain' => 'Lamech', 'Enos' => 'Seth');

// ------------

$name = trim(fgets(STDIN));

while (!feof(STDIN))
{
  while (TRUE)
  {
    echo "$name\n";

    // Can use either:
    if (!isset($father[$name])) break;
    $name = $father[$name];

    // or:
    // if (!key_exists($name, $father)) break;
    // $name = $father[$name];

    // or combine the two lines:
    // if (!($name = $father[$name])) break;
  }

  echo "\n";
  $name = trim(fgets(STDIN));
}

// ----------------------------

define(SEP, ' ');

foreach($father as $child => $parent)
{
  if (!$children[$parent])
    $children[$parent] = $child;
  else
    $children[$parent] .= SEP . $child;
}

$name = trim(fgets(STDIN));

while (!feof(STDIN))
{
  echo $name . ' begat ';

  if (!$children[$name])
    echo "Nothing\n"
  else
    echo str_replace(SEP, ', ', $children[$name]) . "\n";

  $name = trim(fgets(STDIN));
}

// ----------------------------

define(SEP, ' ');

$files = array('/tmp/a', '/tmp/b', '/tmp/c');

foreach($files as $file)
{
  if (!is_file($file)) { echo "Skipping {$file}\n"; continue; }
  if (!($fh = fopen($file, 'r'))) { echo "Skipping {$file}\n"; continue; }

  $line = fgets($fh);

  while (!feof($fh))
  {
    if (preg_match('/^\s*#\s*include\s*<([^>]+)>/', $line, $matches))
    {
      if (isset($includes[$matches[1]]))
        $includes[$matches[1]] .= SEP . $file;
      else
        $includes[$matches[1]] = $file;
    }

    $line = fgets($fh);
  }

  fclose($fh);
}

print_r($includes);

// @@PLEAC@@_5.16
// @@INCOMPLETE@@
// @@INCOMPLETE@@

// @@PLEAC@@_9.0
$entry = stat('/bin/vi');
$entry = stat('/usr/bin');
$entry = stat($argv[1]);

// ------------

$entry = stat('/bin/vi');

$ctime = $entry['ctime'];
$size = $entry['size'];

// ----------------------------

// For the simple task of determining whether a file contains, text', a simple
// function that searches for a newline could be implemented. Not exactly
// foolproof, but very simple, low overhead, no installation headaches ...
function containsText($file)
{
  $status = FALSE;

  if (($fp = fopen($file, 'r')))
  {
    while (FALSE !== ($char = fgetc($fp)))
    {
      if ($char == "\n") { $status = TRUE; break; }
    }

    fclose($fp);
  }

  return $status;
}

// PHP offers the [currently experimental] Fileinfo group of functions to
// determine file types based on their contents / 'magic numbers'. This
// is functionality similar to the *NIX, 'file' utility. Note that it must
// first be installed using the PEAR utility [see PHP documentation] 
function isTextFile($file)
{
  // Note: untested code, but I believe this is how it is supposed to work
  $finfo = finfo_open(FILEINFO_NONE);
  $status = (finfo_file($finfo, $file) == 'ASCII text');
  finfo_close($finfo);
  return $status;
}

// Alternatively, use the *NIX utility, 'file', directly
function isTextFile($file)
{
  return exec(trim('file -bN ' . escapeshellarg($file))) == 'ASCII text';
}

// ----

containsText($argv[1]) || die("File {$argv[1]} doesn't have any text in it\n");

isTextFile($argv[1]) || die("File {$argv[1]} doesn't have any text in it\n");

// ----------------------------

$dirname = '/usr/bin/';

($dirhdl = opendir($dirname)) || die("Couldn't open {$dirname}\n");

while (($file = readdir($dirhdl)) !== FALSE)
{
  printf("Inside %s is something called: %s\n", $dirname, $file);
}

closedir($dirhdl);

// @@PLEAC@@_9.1
$filename = 'example.txt';

// Get the file's current access and modification time, respectively
$fs = stat($filename);

$readtime = $fs['atime'];
$writetime = $fs['mtime'];

// Alter $writetime, and $readtime ...

// Update file timestamp
touch($filename, $writetime, $readtime);

// ----------------------------

$filename = 'example.txt';

// Get the file's current access and modification time, respectively
$fs = stat($filename);

$atime = $fs['atime'];
$mtime = $fs['mtime'];

// Dedicated functions also exist to retrieve this information:
//
// $atime = $fileatime($filename);
// $mtime = $filemtime($filename);
//

// Perform date arithmetic. Traditional approach where arithmetic is performed
// directly with Epoch Seconds [i.e. the *NIX time stamp value] will work ...

define('SECONDS_PER_DAY', 60 * 60 * 24);

// Set file's access and modification times to 1 week ago
$atime -= 7 * SECONDS_PER_DAY;
$mtime -= 7 * SECONDS_PER_DAY;

// ... but care must be taken to account for daylight saving. Therefore, the
// recommended approach is to use library functions to perform such tasks:

$atime = strtotime('-7 days', $atime);
$mtime = strtotime('-7 days', $mtime);

// Update file timestamp
touch($filename, $mtime, $atime);

// Good idea to clear the cache after such updates have occurred so fresh
// values will be retrieved on next access
clearstatcache();

// ----------------------------

$argc == 2 || die("usage: {$argv[0]} filename\n");

$filename = $argv[1];
$fs = stat($filename);

$atime = $fs['atime'];
$mtime = $fs['mtime'];

// Careful here: since interactive, use, 'system', not 'exec', to launch [latter
// does not work under *NIX - at least, not for me :)]
system(trim(getenv('EDITOR') . ' vi ' . escapeshellarg($filename)), $retcode);

touch($filename, $mtime, $atime) || die("Error updating timestamp on file, {$filename}!\n");

// @@PLEAC@@_9.2
// The 'unlink' function is used to delete regular files, whilst the 'rmdir' function
// does the same on non-empty directories. AFAIK, no recursive-deletion facility
// exists, and must be manually programmed

$filename = '...';

@unlink($filename) || die("Can't delete, {$filename}!\n");

// ------------

$files = glob('...');
$problem = FALSE;

// Could simply use a foreach loop
foreach($files as $filename) { @unlink($filename) || $problem = TRUE; }

//
// Alternatively, an applicative approach could be used, one closer in spirit to
// largely-functional languages like Scheme
//
// function is_all_deleted($deleted, $filename) { return @unlink($filename) && $deleted; }
// $problem = !array_reduce($files, 'is_all_deleted', TRUE);
//

if ($problem)
{
  fwrite(STDERR, 'Could not delete all of:');
  foreach($files as $filename) { fwrite(STDERR, ' ' . $filename); }
  fwrite(STDERR, "\n"); exit(1);
} 

// ------------

function rmAll($files)
{
  $count = 0;

  foreach($files as $filename) { @unlink($filename) && $count++; };

  return $count;

// An applicative alternative using 'create_function', PHP's rough equivalent of 'lambda' ...
//
//  return array_reduce($files,
//    create_function('$count, $filename', 'return @unlink($filename) && $count++;'), 0);
}

// ----

$files = glob('...');
$toBeDeleted = sizeof($files);
$count = rmAll($files);

($count == $toBeDeleted) || die("Could only delete {$count} of {$toBeDeleted} files\n");

// @@PLEAC@@_9.3
$oldfile = '/tmp/old'; $newfile = '/tmp/new';

copy($oldfile, $newfile) || die("Error copying file\n");

// ----------------------------

// All the following copy a file by copying its contents. Examples do so in a single
// operation, but it is also possible to copy arbitrary blocks, or, line-by-line in 
// the case of 'text' files
$oldfile = '/tmp/old'; $newfile = '/tmp/new';

if (is_file($oldfile))
  file_put_contents($newfile, file_get_contents($oldfile));
else
  die("Problem copying file {$oldfile} to file {$newfile}\n");

// ------------

$oldfile = '/tmp/old'; $newfile = '/tmp/new';

fwrite(($nh = fopen($newfile, 'wb')), fread(($oh = fopen($oldfile, 'rb')), filesize($oldfile)));
fclose($oh);
fclose($nh);

// ------------

// As above, but with some error checking / handling
$oldfile = '/tmp/old'; $newfile = '/tmp/new';

($oh = fopen($oldfile, 'rb')) || die("Problem opening input file {$oldfile}\n");
($nh = fopen($newfile, 'wb')) || die("Problem opening output file {$newfile}\n");

if (($filesize = filesize($oldfile)) > 0)
{
  fwrite($nh, fread($oh, $filesize)) || die("Problem reading / writing file data\n");
}

fclose($oh);
fclose($nh);

// ----------------------------

// Should there be platform-specfic problems copying 'very large' files, it is
// a simple matter to call a system command utility via, 'exec'

// *NIX-specific example. Could check whether, 'exec', succeeded, but checking whether
// a file exists after the operation might be a better approach
$oldfile = '/tmp/old'; $newfile = '/tmp/new';

is_file($newfile) && unlink($newfile);

exec(trim('cp --force ' . escapeshellarg($oldfile) . ' ' . escapeshellarg($newfile)));

is_file($newfile) || die("Problem copying file {$oldfile} to file {$newfile}\n");

// For other operating systems just change:
// * filenames
// * command being 'exec'ed
// as the rest of the code is platform independant

// @@PLEAC@@_9.4
function makeDevInodePair($filename)
{
  if (!($fs = @stat($filename))) return FALSE;
  return strval($fs['dev'] . $fs['ino']);
}

// ------------

function do_my_thing($filename)
{
  // Using a global variable to mimic Perl example, but could easily have passed
  // '$seen' as an argument
  global $seen;

  $devino = makeDevInodePair($filename);

  // Process $filename if it has not previously been seen, else just increment
  if (!isset($seen[$devino]))
  {
    // ... process $filename ...

    // Set initial count
    $seen[$devino] = 1;
  }
  else
  {
    // Otherwise, just increment the count
    $seen[$devino] += 1;
  }
}

// ----

// Simple example
$seen = array();

do_my_thing('/tmp/old');
do_my_thing('/tmp/old');
do_my_thing('/tmp/old');
do_my_thing('/tmp/new');

foreach($seen as $devino => $count)
{
  echo "{$devino} -> {$count}\n";
}

// ------------

// A variation on the above avoiding use of global variables, and illustrating use of
// easily-implemented 'higher order' techniques

// Helper function loosely modelled on, 'array_reduce', but using an array as
// 'accumulator', which is returned on completion
function array_update($arr, $lambda, $updarr)
{
  foreach($arr as $key) $lambda($updarr, $key);
  return $updarr;
}

function do_my_thing(&$seen, $filename)
{
  if (!array_key_exists(($devino = makeDevInodePair($filename)), $seen))
  {
    // ... processing $filename ...

    // Update $seen
    $seen[$devino] = 1;
  }
  else
  {
    // Update $seen
    $seen[$devino] += 1;
  }
}

// ----

// Simple example
$files = array('/tmp/old', '/tmp/old', '/tmp/old', '/tmp/new');

// Could do this ...
$seen = array();
array_update($files, 'do_my_thing', &$seen);

// or this:
$seen = array_update($files, 'do_my_thing', array());

// or a 'lambda' could be used:
array_update($files,
             create_function('$seen, $filename', '... code not shown ...'),
             &$seen);

foreach($seen as $devino => $count)
{
  echo "{$devino} -> {$count}\n";
}

// ----------------------------

$files = glob('/tmp/*');

define(SEP, ';');
$seen = array();

foreach($files as $filename)
{
  if (!array_key_exists(($devino = makeDevInodePair($filename)), $seen))
    $seen[$devino] = $filename;
  else
    $seen[$devino] = $seen[$devino] . SEP . $filename;
}

$devino = array_keys($seen);
sort($devino);

foreach($devino as $key)
{
  echo $key . ':';
  foreach(split(SEP, $seen[$key]) as $filename) echo ' ' . $filename;
  echo "\n";
}

// @@PLEAC@@_9.5
// Conventional POSIX-like approach to directory traversal
$dirname = '/usr/bin/';

($dirhdl = opendir($dirname)) || die("Couldn't open {$dirname}\n");

while (($file = readdir($dirhdl)) !== FALSE)
{
  ; // ... do something with $dirname/$file
    // ...
}

closedir($dirhdl);

// ------------

// Newer [post PHP 4], 'applicative' approach - an array of filenames is
// generated that may be processed via external loop ...

$dirname = '/usr/bin/';

foreach(scandir($dirname) as $file)
{
  ; // ... do something with $dirname/$file
    // ...
}

// .. or, via callback application, perhaps after massaging by one of the
// 'array' family of functions [also uses, 'array_update', from earlier section]

$newlist = array_update(array_reverse(scandir($dirname)),
                        create_function('$filelist, $file',  ' ; '),
                        array());

// And don't forget that the old standby, 'glob', that returns an array of
// paths filtered using the Bourne Shell-based wildcards, '?' and '*', is
// also available

foreach(glob($dirname . '*') as $path)
{
  ; // ... do something with $path
    // ...
}

// ----------------------------

// Uses, 'isTextFile', from an earlier section
$dirname = '/usr/bin/';

echo "Text files in {$dirname}:\n";

foreach(scandir($dirname) as $file)
{
  // Take care when constructing paths to ensure cross-platform operability 
  $path = $dirname . $file;

  if (is_file($path) && isTextFile($path)) echo $path . "\n";
}

// ----------------------------

function plain_files($dirname)
{
  ($dirlist = glob($dirname . '*')) || die("Couldn't glob {$dirname}\n");

  // Pass function name directly if only a single function performs filter test
  return array_filter($dirlist, 'is_file');

  // Use, 'create_function', if a multi-function test is needed
  //
  // return array_filter($dirlist, create_function('$path', 'return is_file($path);'));
  //
}

// ------------

foreach(plain_files('/tmp/') as $path)
{
  echo $path . "\n";
}

// @@PLEAC@@_9.6
$dirname = '/tmp/';

// Full paths
$pathlist = glob($dirname . '*.c');

// File names only - glob-based matching
$filelist = array_filter(scandir($dirname),
                         create_function('$file', 'return fnmatch("*.c", $file);'));

// ----------------------------

$dirname = '/tmp/';

// File names only - regex-based matching [case-insensitive]
$filelist = array_filter(scandir($dirname),
                         create_function('$file', 'return eregi("\.[ch]$", $file);'));

// ----------------------------

$dirname = '/tmp/';

// Directory names - all-digit names
$dirs = array_filter(glob($dirname . '*', GLOB_ONLYDIR),
                     create_function('$path', 'return ereg("^[0-9]+$", basename($path));'));

// @@PLEAC@@_9.7
// Recursive directory traversal function and helper: traverses a directory tree
// applying a function [and a variable number of accompanying arguments] to each
// file

class Accumulator
{
  public $value;
  public function __construct($start_value) { $this->value = $start_value; }
}

// ------------

function process_directory_($op, $func_args)
{
  if (is_dir($func_args[0]))
  {
    $current = $func_args[0];
    foreach(scandir($current) as $entry)
    {
      if ($entry == '.' || $entry == '..') continue;
      $func_args[0] = $current . '/' . $entry;
      process_directory_($op, $func_args);
    }
  }
  else
  {
    call_user_func_array($op, $func_args);
  }
}

function process_directory($op, $dir)
{
  if (!is_dir($dir)) return FALSE;
  $func_args = array_slice(func_get_args(), 1);
  process_directory_($op, $func_args);
  return TRUE;
}

// ----------------------------

$dirlist = array('/tmp/d1', '/tmp/d2', '/tmp/d3');

// Do something with each directory in the list
foreach($dirlist as $dir)
{
  ;
  // Delete directory [if empty]     -> rmdir($dir); 
  // Make it the 'current directory' -> chdir($dir);
  // Get list of files it contains   -> $filelist = scandir($dir);
  // Get directory metadata          -> $ds = stat($dir);
}

// ------------

$dirlist = array('/tmp/d1', '/tmp/d2', '/tmp/d3');

function pf($path)
{
  // ... do something to the file or directory ...
  printf("%s\n", $path);
}

// For each directory in the list ...
foreach($dirlist as $dir)
{
  // Is this a valid directory ?
  if (!is_dir($dir)) { printf("%s does not exist\n", $dir); continue; }

  // Ok, so get all the directory's entries
  $filelist = scandir($dir);

  // An 'empty' directory will contain at least two entries: '..' and '.'
  if (count($filelist) == 2) { printf("%s is empty\n", $dir); continue; }

  // For each file / directory in the directory ...
  foreach($filelist as $file)
  {
    // Ignore '..' and '.' entries
    if ($file == '.' || $file == '..') continue;

    // Apply function to process the file / directory
    pf($dir . '/' . $file);
  }
}

// ----------------------------

function accum_filesize($file, $accum)
{
  is_file($file) && ($accum->value += filesize($file));
}

// ------------

// Verify arguments ...
$argc == 2 || die("usage: {$argv[0]} dir\n");
$dir = $argv[1];

is_dir($dir) || die("{$dir} does not exist / not a directory\n");

// Collect data [use an object to accumulate results]
$dirsize = new Accumulator(0);
process_directory('accum_filesize', $dir, $dirsize); 

// Report results
printf("%s contains %d bytes\n", $dir, $dirsize->value);

// ----------------------------

function biggest_file($file, $accum)
{
  if (is_file($file))
  {
    $fs = filesize($file);
    if ($accum->value[1] < $fs) { $accum->value[0] = $file; $accum->value[1] = $fs; }
  }
}

// ------------

// Verify arguments ...
$argc == 2 || die("usage: {$argv[0]} dir\n");
$dir = $argv[1];

is_dir($dir) || die("{$dir} does not exist / not a directory\n");

// Collect data [use an object to accumulate results]
$biggest = new Accumulator(array('', 0));
process_directory('biggest_file', $dir, $biggest); 

// Report results
printf("Biggest file is %s containing %d bytes\n", $biggest->value[0], $biggest->value[1]);

// ----------------------------

function youngest_file($file, $accum)
{
  if (is_file($file))
  {
    $fct = filectime($file);
    if ($accum->value[1] > $fct) { $accum->value[0] = $file; $accum->value[1] = $fct; }
  }
}

// ------------

// Verify arguments ...
$argc == 2 || die("usage: {$argv[0]} dir\n");
$dir = $argv[1];

is_dir($dir) || die("{$dir} does not exist / not a directory\n");

// Collect data [use an object to accumulate results]
$youngest = new Accumulator(array('', 2147483647));
process_directory('youngest_file', $dir, $youngest); 

// Report results
printf("Youngest file is %s dating %s\n", $youngest->value[0], date(DATE_ATOM, $youngest->value[1]));

// @@PLEAC@@_9.8
// AFAICT, there is currently no library function that recursively removes a
// directory tree [i.e. a directory, it's subdirectories, and any other files]
// with a single call. Such a function needs to be custom built. PHP tools
// with which to do this:
// * 'unlink', 'rmdir', 'is_dir', and 'is_file' functions, will all take care
//   of the file testing and deletion
// * Actual directory traversal requires obtaining directory / subdirectory
//   lists, and here there is much choice available, though care must be taken
//   as each has it's own quirks
//   - 'opendir', 'readdir', 'closedir'
//   - 'scandir'
//   - 'glob'
//   - SPL 'directory iterator' classes [newish / experimental - not shown here]
//
// The PHP documentation for 'rmdir' contains several examples, each illustrating
// one of each approach; the example shown here is loosely based on one of these
// examples

// Recursor - recursively traverses directory tree
function rmtree_($dir)
{
  $dir = "$dir";

  if ($dh = opendir($dir))
  {
    while (FALSE !== ($item = readdir($dh)))
    {
      if ($item != '.' && $item != '..')
      {
        $subdir = $dir . '/' . "$item";

        if (is_dir($subdir)) rmtree_($subdir);
        else @unlink($subdir);
      }
    }

    closedir($dh); @rmdir($dir);
  }
}

// Launcher - performs validation then starts recursive routine
function rmtree($dir)
{
  if (is_dir($dir))
  {
    (substr($dir, -1, 1) == '/') && ($dir = substr($dir, 0, -1));
    rmtree_($dir); return !is_dir($dir);
  }

  return FALSE;
}

// ------------

$argc == 2 || die("usage: rmtree dir\n");

rmtree($argv[1]) || die("Could not remove directory {$argv[1]}\n");

// @@PLEAC@@_9.9
$filepairs = array('x.txt' => 'x2.txt', 'y.txt' => 'y.doc', 'zxc.txt' => 'cxz.txt');

foreach($filepairs as $oldfile => $newfile)
{
  @rename($oldfile, $newfile) || fwrite(STDERR, sprintf("Could not rename %s to %s\n", $oldfile, $newfile));
}

// ----------------------------

// Call a system command utility via, 'exec'. *NIX-specific example. Could check whether,
// 'exec', succeeded, but checking whether a renamed file exists after the operation might
// be a better approach

$oldfile = '/tmp/old'; $newfile = '/tmp/new';

is_file($newfile) && unlink($newfile);

exec(trim('mv --force ' . escapeshellarg($oldfile) . ' ' . escapeshellarg($newfile)));

is_file($oldfile) || die("Problem renaming file {$oldfile} to file {$newfile}\n");

// For other operating systems just change:
// * filenames
// * command being 'exec'ed
// as the rest of the code is platform independant

// ----------------------------

// A modified implementation of Larry's Filename Fixer. Rather than passing
// a single expression, a 'from' regexp is passed; each match in the file
// name(s) is changed to the value of 'to'. It otherwise behaves the same
//

$argc > 2 || die("usage: rename from to [file ...]\n");

$from = $argv[1];
$to = $argv[2]; 

if (count(($argv = array_slice($argv, 3))) < 1)
  while (!feof(STDIN)) $argv[] = substr(fgets(STDIN), 0, -1);

foreach($argv as $file)
{
  $was = $file;
  $file = ereg_replace($from, $to, $file);

  if (strcmp($was, $file) != 0)
    @rename($was, $file) || fwrite(STDERR, sprintf("Could not rename %s to %s\n", $was, $file));
}

// @@PLEAC@@_9.10
$base = basename($path);
$dir = dirname($path);

// PHP's equivalent to Perl's 'fileparse'
$pathinfo = pathinfo($path);

$base = $pathinfo['basename'];
$dir = $pathinfo['dirname'];
$ext = $pathinfo['extension'];

// ----------------------------

$path = '/usr/lib/libc.a';

printf("dir is %s, file is %s\n", dirname($path), basename($path));

// ------------

$path = '/usr/lib/libc.a';

$pathinfo = pathinfo($path);

printf("dir is %s, name is %s, extension is %s\n", $pathinfo['dirname'], $pathinfo['basename'], $pathinfo['extension']);

// ----------------------------

// Handle Mac example as a simple parse task. However, AFAIK, 'pathinfo' is cross-platform,
// so should handle file path format differences transparently
$path = 'Hard%20Drive:System%20Folder:README.txt';

$macp = array_combine(array('drive', 'folder', 'filename'), split("\:", str_replace('%20', ' ', $path)));
$macf = array_combine(array('name', 'extension'), split("\.", $macp['filename'])); 

printf("dir is %s, name is %s, extension is %s\n", ($macp['drive'] . ':' . $macp['folder']), $macf['name'], ('.' . $macf['extension']));

// ----------------------------

// Not really necessary since we have, 'pathinfo', but better matches Perl example
function file_extension($filename, $separator = '.')
{
  return end(split(("\\" . $separator), $filename));
}

// ----

echo file_extension('readme.txt') . "\n";

// @@PLEAC@@_9.11
// @@INCOMPLETE@@
// @@INCOMPLETE@@

// @@PLEAC@@_9.12
// @@INCOMPLETE@@
// @@INCOMPLETE@@

// @@PLEAC@@_10.0
// Since defined at outermost scope, $greeted may be considered a global variable
$greeted = 0;

// ------------

// Must use, 'global', keyword to inform functions that $greeted already exists as
// a global variable. If this is not done, a local variable of that name is implicitly
// defined
function howManyGreetings()
{
  global $greeted;
  return $greeted;
}

function hello()
{
  global $greeted;
  $greeted++;
  echo "high there!, this procedure has been called {$greeted} times\n";
}

// ------------

hello();
$greetings = howManyGreetings();
echo "bye there!, there have been {$greetings} greetings so far\n";

// @@PLEAC@@_10.1
// Conventionally-defined function together with parameter list
function hypotenuse($side1, $side2)
{
  return sqrt(pow($side1, 2) + pow($side2, 2));
}

// ----

// Alternative is to define the function without parameter list, then use
// 'func_get_arg' to extract arguments
function hypotenuse()
{
  // Could check number of arguments passed with: 'func_num_args', which
  // would be the approach used if dealing with variable number of arguments
  $side1 = func_get_arg(0); $side2 = func_get_arg(1);

  return sqrt(pow($side1, 2) + pow($side2, 2));
}

// ------------

// 1. Conventional function call
$diag = hypotenuse(3, 4);

// ------------

// 2. Function call using, 'call_user_func' library routines
$funcname = 'hypotenuse';

// a. Pass function name, and variable number of arguments
$diag = call_user_func($funcname, 3, 4);

// b. Package arguments as array, pass together with function name
$args = array(3, 4); 
$diag = call_user_func_array($funcname, $args);

// ----------------------------

$nums = array(1.4, 3.5, 6.7);

// ------------

// Pass-by-value
function int_all($arr)
{
  return array_map(create_function('$n', 'return (int) $n;'), $arr);
}

// Pass-by-reference
function trunc_em(&$n)
{
  foreach ($n as &$value) $value = (int) $value;
}

// ------------

// $nums untouched; $ints is new array
$ints = int_all($nums);

// $nums updated
trunc_em($nums);

// @@PLEAC@@_10.2
// Strictly-speaking, PHP is neither lexically [no environment capture] nor
// dynamically [no variable shadowing] scoped. A script in which several
// functions have been defined has two, entirely separate, scopes:
//
// * A 'top-level' scope i.e. everything outside each function
//
// * A 'local scope' within each function; each function is a self-contained
//   entity and cannot [via conventional means] access variables outside its
//   local scope. Accessing a variable that has not been locally defined
//   serves to define it i.e. accessing a variable assumed to be global
//   sees a local variable of that name defined
//
// The way 'global' variables are provided is via a predefined array of
// variable names, $GLOBALS [it is one of a special set of variables known
// as 'superglobals'; such variables *are* accessable in all scopes]. Each
// entry in this array is a 'global' variable name, and may be freely
// accessed / updated. A more convenient means of accessing such variables
// is via the 'global' keyword: one or more variables within a function is
// declared 'global', and those names are then taken to refer to entries
// in the $GLOBALS array rather than seeing local variables of that name
// accessed or defined

function some_func()
{
  // Variables declared within a function are local to that function
  $variable = 'something';
}

// ----------------------------

// Top-level declared variables
$name = $argv[1]; $age = $argv[2];

$c = fetch_time();

$condition = 0;

// ------------

function run_check()
{
  // The globally-declared variable, '$condition', is not accessable within
  // the function unless it declared as 'global. Had this not been done then
  // attempts to access, '$condition', would have seen a local variable
  // of that name declared and updated. Same applies to other variables
  global $condition, $name, $age, $c;

  $condition = 1;
  // ...
}

function check_x($x)
{
  $y = 'whatever';

  // This function only has access to the parameter, '$x', and the locally
  // declared variable, '$y'.

  // Whilst 'run_check' has access to several global variables, the current
  // function does not. For it to access the global variable, '$condition',
  // it must be declared 'global'
  run_check();

  global $condition;

  // 'run_check' will have updated, '$condition', and since it has been
  // declared 'global' here, it is accessable

  if ($condition)
  {
    ; // ...
  }
}

// @@PLEAC@@_10.3
// Local scopes are not created in the same way as in Perl [by simply enclosing
// within braces]: only via the creation of functions are local scopes created

// Doesn't create a local scope; '$myvariable' is created at top-level
{
  $myvariable = 7;
}

// '$myvariable' is accessable here
echo $myvariable . "\n";

// ------------

{
  $counter = 0;

  // Local scope created within function, but not within surrounding braces
  // so:
  // * '$counter' is actually a top-level variable, so globally accessable
  // * 'next_counter' has no implict access to '$counter'; must be granted
  //   via 'global' keyword

  function next_counter() { global $counter; $counter++; }
}

// ----------------------------

// PHP doesn't, AFAIK, offer an equivalent to Perl's BEGIN block. Similar
// behaviour may be obtained by defining a class, and including such code
// in its constructor

class BEGIN
{
  private $myvariable;

  function __construct()
  {
    $this->myvariable = 5;
  }

  function othersub()
  {
    echo $this->myvariable . "\n";
  }
}

// ------------

$b = new BEGIN();

$b->othersub();

// ----------------------------

// PHP, like C, supports 'static' local variables, that is, those that upon
// first access are initialised, and thence retain their value between function
// calls. However, the 'counter' example is better implemented as a class

class Counter
{
  private $counter;

  function __construct($counter_init)
  {
    $this->counter = $counter_init;
  }

  function next_counter() { $this->counter++; return $this->counter; }
  function prev_counter() { $this->counter; return $this->counter; }
}

// ------------

$counter = new Counter(42);
echo $counter->next_counter() . "\n";
echo $counter->next_counter() . "\n";
echo $counter->prev_counter() . "\n";

// @@PLEAC@@_10.4
// AFAICT there is no means of obtaining the name of the currently executing
// function, or, for that matter, perform any stack / activation record,
// inspection. It *is* possible to:
//
// * Obtain a list of the currently-defined functions ['get_defined_functions']
// * Check whether a specific function exists ['function_exists']
// * Use the 'Reflection API'
//
// So, to solve this problem would seem to require adopting a convention where
// a string representing the function name is passed as an argument, or a local
// variable [perhaps called, '$name'] is so set [contrived, and of limited use]

function whoami()
{
  $name = 'whoami';
  echo "I am: {$name}\n";
}

// ------------

whoami();

// @@PLEAC@@_10.5
// In PHP all items exist as 'memory references' [i.e. non-modifiable pointers],
// so when passing an item as a function argument, or returning an item from
// a function, it is this 'memory reference' that is passed, and *not* the
// contents of that item. Should several references to an item exist [e.g. if
// passed to a function then at least two such references would exist in
// different scopes] they would all be refering to the same copy of the item.
// However, if an attempt is made to alter the item is made, a copy is made
// and it is the copy that is altered, leaving the original intact.
// 
// The PHP reference mechanism is used to selectively prevent this behaviour,
// and ensure that if a change is made to an item that no copy is made, and that
// it is the original item that is changed. Importantly, there is no efficiency
// gain from passing function parameters using references if the parameter item
// is not altered.

// A copy of the item referred to by, '$arr', is made, and altered; original
// remains intact
function array_by_value($arr)
{
  $arr[0] = 7;
  echo $arr[0] . "\n"; 
}

// No copy is made; original item referred to by, '$arr', is altered
function array_by_ref(&$arr)
{
  $arr[0] = 7;
  echo $arr[0] . "\n"; 
}

// ------------

$arr = array(1, 2, 3);

echo $arr[0] . "\n";         // output: 1 
array_by_value($arr);        // output: 7
echo $arr[0] . "\n";         // output: 1 

$arr = array(1, 2, 3);

echo $arr[0] . "\n";         // output: 1 
array_by_ref($arr);          // output: 7
echo $arr[0] . "\n";         // output: 7 

// ----------------------------

// Since, 'add_vecpair', does not attempt to alter either, '$x' or '$y', it makes
// no difference whether they are 'passed by value', or 'passed by reference'
function add_vecpair($x, $y)
{
  $r = array();
  $length = count($x);
  for($i = 0; $i < $length; $i++) $r[$i] = $x[$i] + $y[$i];
  return $r;
}

// ...
count($arr1) == count($arr2) || die("usage: add_vecpair ARR1 ARR2\n");

// 'passed by value'
$arr3 = add_vecpair($arr1, $arr2);

// 'passed by reference' [also possible to override default 'passed by value'
// if required]
$arr3 = add_vecpair(&$arr1, &$arr2);

// @@PLEAC@@_10.6
// PHP can be described as a dynamically typed language because variables serve
// as identifiers, and the same variable may refer to data of various types.
// As such, the set of arguments passed to a function may vary in type between
// calls, as can the type of return value. Where this is likely to occur type
// checking should be performed either / both within the function body, and
// when obtaining it's return value. As for Perl-style 'return context', I
// don't believe it is supported by PHP

// Can return any type
function mysub()
{
  // ...
  return 5;
  // ...
  return array(5);
  // ...
  return '5';
}

// Throw away return type [i.e. returns a 'void' type ?]
mysub();

// Check return type. Can do via:
// * gettype($var)
// * is_xxx e.g. is_array($var), is_muneric($var), ...
$ret = mysub();

if (is_numeric($ret))
{
  ; // ...
}

if (is_array($ret))
{
  ; // ...
}

if (is_string($ret))
{
  ; // ...
}

// @@PLEAC@@_10.7
// PHP doesn't directly support named / keyword parameters, but these can be
// easily mimiced using a class of key / value pairs, and passing a variable
// number of arguments

class KeyedValue
{
  public $key, $value;
  public function __construct($key, $value) { $this->key = $key; $this->value = $value; }
}

function the_func()
{
  foreach (func_get_args() as $arg)
  {
    printf("Key: %10s|Value:%10s\n", $arg->key, $arg->value);
  }
}

// ----

the_func(new KeyedValue('name', 'Bob'),
         new KeyedValue('age', 36),
         new KeyedValue('income', 51000));

// ----------------------------

// Alternatively, an associative array of key / value pairs may be constructed.
// With the aid of the 'extract' built-in function, the key part of this array
// may be intsntiated to a variable name, thus more closely approximating the
// behaviour of true named parameters

function the_func($var_array)
{
  extract($var_array);

  if (isset($name)) printf("Name:   %s\n", $name);
  if (isset($age)) printf("Age:    %s\n", $age);
  if (isset($income)) printf("Income: %s\n", $income);
}

// ----

the_func(array('name' => 'Bob', 'age' => 36, 'income' => 51000));

// ----------------------------

class RaceTime
{
  public $time, $dim;
  public function __construct($time, $dim) { $this->time = $time; $this->dim = $dim; }
}

function the_func($var_array)
{
  extract($var_array);

  if (isset($start_time)) printf("start:  %d - %s\n", $start_time->time, $start_time->dim);
  if (isset($finish_time)) printf("finish: %d - %s\n", $finish_time->time, $finish_time->dim);
  if (isset($incr_time)) printf("incr:   %d - %s\n", $incr_time->time, $incr_time->dim);
}

// ----

the_func(array('start_time' => new RaceTime(20, 's'),
               'finish_time' => new RaceTime(5, 'm'),
               'incr_time' => new RaceTime(3, 'm')));

the_func(array('start_time' => new RaceTime(5, 'm'),
               'finish_time' => new RaceTime(30, 'm')));

the_func(array('start_time' => new RaceTime(30, 'm')));

// @@PLEAC@@_10.8
// The 'list' keyword [looks like a function but is actually a special language
// construct] may be used to perform multiple assignments from a numerically
// indexed array of values, and offers the added bonus of being able to skip
// assignment of one, or more, of those values

function func() { return array(3, 6, 9); }

// ------------

list($a, $b, $c) = array(6, 7, 8);

// Provided 'func' returns an numerically-indexed array, the following
// multiple assignment will work
list($a, $b, $c) = func();

// Any existing variables no longer wanted would need to be 'unset'
unset($b);

// As above, but second element of return array discarded
list($a,,$c) = func();

// ----------------------------

// Care needed to ensure returned array is numerically-indexed
list($dev, $ino,,,$uid) = array_slice(array_values(stat($filename)), 0, 13);

// @@PLEAC@@_10.9
// Multiple return values are possible via packing a set of values within a
// numerically-indexed array and using 'list' to extract them

function some_func() { return array(array(1, 2, 3), array('a' => 1, 'b' => 2)); }

// ------------

list($arr, $hash) = some_func();

// ----------------------------

function some_func(&$arr, &$hash) { return array($arr, $hash); }

// ------------

$arrin = array(1, 2, 3); $hashin = array('a' => 1, 'b' => 2);

list($arr, $hash) = some_func($arrin, $hashin);

// @@PLEAC@@_10.10
// AFAICT, most of the PHP library functions are designed to return some required 
// value on success, and FALSE on exit. Whilst it is possible to return NULL, or
// one of the recognised 'empty' values [e.g. '' or 0 or an empty array etc],
// FALSE actually seems to be the preferred means of indicating failure

function a_func() { return FALSE; }

a_func() || die("Function failed\n");

if (!a_func()) die("Function failed\n");

// @@PLEAC@@_10.11
// Whether PHP is seen to support prototyping depends on the accepted
// definition of this term:
//
// * Prototyping along the lines used in Ada, Modula X, and even C / C++,
//   in which a function's interface is declared separately from its
//   implementation, is *not* supported
//
// * Prototyping in which, as part of the function definition, parameter
//   information must be supplied. In PHP a function definition neither
//   parameter, nor return type, information needs to be supplied, though
//   it is usual to see a parameter list supplied [indicates the number,
//   positional order, and optionally, whether a parameter is passed by
//   reference; no type information is present]. In short, prototyping in
//   PHP is optional, and limited

function func_with_one_arg($arg1)
{
  ; // ...
}

function func_with_two_arg($arg1, $arg2)
{
  ; // ...
}

function func_with_three_arg($arg1, $arg2, $arg3)
{
  ; // ...
}

// The following may be interpreted as meaning a function accepting no
// arguments:
function func_with_no_arg()
{
  ; // ...
}

// whilst the following may mean a function taking zero or more arguments
function func_with_no_arg_information()
{
  ; // ...
}

// @@PLEAC@@_10.12
// Unlike in Perl, PHP's 'die' [actually an alias for 'exit'] doesn't throw
// an exception, but instead terminates the script, optionally either
// returning an integer value to the operating system, or printing a message.
// So, the following, does not exhibit the same behaviour as the Perl example

die("some message\n"); 

// Instead, like so many modern languages, PHP implements exception handling
// via the 'catch' and 'throw' keywords. Furthermore, a C++ or Java programmer
// would find PHP's exception handling facility remarkably similar to those
// of their respective languages. A simple, canonical example follows:

// Usual to derive new exception classes from the built-in, 'Exception',
// class
class MyException extends Exception
{
  // ...
}

// ...

try
{
  // ...
  if ($some_problem_detected) throw new MyException('some message', $some_error_code);
  // ..
}

catch (MyException $e)
{
  ; // ... handle the problem ...
}

// ----------------------------

class FullMoonException extends Exception
{
  // ...
}

// ...

try
{
  // ...
  if ($some_problem_detected) throw new FullMoonException('...', $full_moon_error_code);
  // ..
}

catch (FullMoonException $e)
{
  // ... rethrow the exception - will propagate to higher level ...
  throw $e;
}

// @@PLEAC@@_10.13
// Please refer to discussion about PHP scope in section two of this chapter.
// Briefly, PHP assumes a variable name within a function to be local unless
// it has been specifically declared with the, 'global', keyword, in which
// case it refers to a variable in the 'superglobal' array, '$GLOBALS'. Thus,
// inadvertant variable name shadowing cannot occur since it is it not possible 
// to use the same name to refer to both a local and a global variable. If
// accessing a global variable care should be taken to not accidentally update
// it. The techniques used in this section are simply not required.

// *** NOT TRANSLATED ***

// @@PLEAC@@_10.14
// In PHP once a function has been defined it remains defined. In other words,
// it cannot be undefined / deleted, nor can that particular function name be
// reused to reference another function body. Even the lambda-like functions
// created via the 'create_function' built-in, cannot be undefined [they exist
// until script termination, thus creating too many of these can actually
// exhaust memory !]. However, since the latter can be assigned to variables,
// the same variable name can be used to reference difference functions [and
// when this is done the reference to the previous function is lost (unless
// deliberately saved), though the function itself continues to exist].
//
// If, however, all that is needed is a simple function aliasing facility,
// then just assign the function name to a variable, and execute using the
// variable name

// Original function
function expand() { echo "expand\n"; }

// Prove that function exists
echo (function_exists('expand') ? 'yes' : 'no') . "\n";

// Use a variable to alias it
$grow = 'expand';

// Call function via original name, and variable, respectively
expand();

$grow(); 

// Remove alias variable
unset($grow);

// ----------------------------

function fred() { echo "fred\n"; }

$barney = 'fred';

$barney();

unset($barney);

fred();

// ------------

$fred = create_function('', 'echo "fred\n";');

$barney = $fred;

$barney();

unset($barney);

$fred();

// ----------------------------

function red($text) { return "<FONT COLOR='red'>$text</FONT>"; }

echo red('careful here') . "\n";

// ------------

$colour = 'red';

$$colour = create_function('$text', 'global $colour;
return "<FONT COLOR=\'$colour\'>$text</FONT>";');

echo $$colour('careful here') . "\n";

unset($$colour);

// ----

$colours = split(' ', 'red blue green yellow orange purple violet');

foreach ($colours as $colour)
{
  $$colour = create_function('$text', 'global $colour;
  return "<FONT COLOR=\'$colour\'>$text</FONT>";');
}

foreach ($colours as $colour) { echo $$colour("Careful with this $colour, James") . "\n"; }

foreach ($colours as $colour) { unset($$colour); }

// @@PLEAC@@_10.15
// PHP sports an AUTOLOAD facility that is quite easy to use, but, AFAICT, is geared
// towards the detection of unavailable classes rather than for individual functions.
// Here is a rudimentary example:

function __autoload($classname)
{
  if (!file_exists($classname))
  {
    // Class file does not exist, so handle situation; in this case,
    // issue error message, and exit program
    die("File for class: {$classname} not found - aborting\n");
  }
  else
  {
    // Class file exists, so load it
    require_once $classname;
  }
}

// ------------

// Attempt to instantiate object of undefined class 
new UnknownClassObject();

// Execution continues here if class exists
// ...

// ----------------------------

// It is also possible to perform [quite extensive] introspection on functions,
// variables etc, so it is possible to check whether a function exists before
// executing it, thus allowing a non-existent functions to be searched for and
// loaded from a source file, or perhaps dynamically defined. An example of what
// could be described as a custom autoload facility appears below.

$colours = array('red', 'blue', 'green', 'yellow', 'orange', 'purple', 'violet');

foreach ($colours as $colour)
{
  $$colour = create_function('$text', 'global $colour;
  return "<FONT COLOR=\'$colour\'>$text</FONT>";');
}

// Let's add a new colour to the list
array_push($colours, 'chartreuse'); 

foreach ($colours as $colour)
{
  // Checking whether function is defined
  if (!function_exists($$colour))
  {
    // Doesn't exist, so dynamically define it
    $$colour = create_function('$text', 'global $colour;
    return "<FONT COLOR=\'$colour\'>$text</FONT>";');

    // Alternatively, if it exists in a source file, 'include' the file:
    // include 'newcolours.php'
  }

  echo $$colour("Careful with this $colour, James") . "\n";
}

foreach ($colours as $colour) unset($$colour); 

// @@PLEAC@@_10.16
// *** Warning *** Whilst PHP *does* allow functions to be defined within other
// functions it needs to be clearly understood that these 'inner' functions:
// * Do not exist until the outer function is called a first time, at which time
//   they then remain defined
// * Are global in scope, so are accessable outside the function by their name;
//   the fact that they are nested within another function has, AFAICT, no bearing
//   on name resolution
// * Do not form a closure: the inner function is merely 'parked' within the
//   outer function, and has no implicit access to the outer function's variables
//   or other inner functions

function outer($arg)
{
  $x = $arg + 35;
  function inner() { return $x * 19; }

  // *** wrong *** 'inner' returns 0 * 19, not ($arg + 35) * 19
  return $x + inner();
}

// ----------------------------

function outer($arg)
{
  $x = $arg + 35;

  // No implicit access to outer function scope; any required data must be
  // explicity passed
  function inner($x) { return $x * 19; }

  return $x + inner($x);
}

// ------------ 

// Equivalent to previously-shown code
function inner($x)
{
  return $x * 19;
}

function outer($arg)
{
  $x = $arg + 35;
  return $x + inner($x);
}

// @@PLEAC@@_10.17
// @@INCOMPLETE@@
// @@INCOMPLETE@@

// @@PLEAC@@_16.1
// Run a command and return its results as a string.
$output_string = shell_exec('program args');

// Same as above, using backtick operator.
$output_string = `program args`;

// Run a command and return its results as a list of strings,
// one per line.
$output_lines = array();
exec('program args', $output_lines);

// -----------------------------

// The only way to execute a program without using the shell is to
// use pcntl_exec(). However, there is no way to do redirection, so
// you can't capture its output.

$pid = pcntl_fork();
if ($pid == -1) {
    die('cannot fork');
} elseif ($pid) {
    pcntl_waitpid($pid, $status);
} else {
    // Note that pcntl_exec() automatically prepends the program name
    // to the array of arguments; the program name cannot be spoofed.
    pcntl_exec($program, array($arg1, $arg2));
}

// @@PLEAC@@_16.2
// Run a simple command and retrieve its result code.
exec("vi $myfile", $output, $result_code);

// -----------------------------

// Use the shell to perform redirection.
exec('cmd1 args | cmd2 | cmd3 >outfile');
exec('cmd args <infile >outfile 2>errfile');

// -----------------------------

// Run a command, handling its result code or signal.
$pid = pcntl_fork();
if ($pid == -1) {
    die('cannot fork');
} elseif ($pid) {
    pcntl_waitpid($pid, $status);
    if (pcntl_wifexited($status)) {
        $status = pcntl_wexitstatus($status);
        echo "program exited with status $status\n";
    } elseif (pcntl_wifsignaled($status)) {
        $signal = pcntl_wtermsig($status);
        echo "program killed by signal $signal\n";
    } elseif (pcntl_wifstopped($status)) {
        $signal = pcntl_wstopsig($status);
        echo "program stopped by signal $signal\n";
    }
} else {
    pcntl_exec($program, $args);
}

// -----------------------------

// Run a command while blocking interrupt signals.
$pid = pcntl_fork();
if ($pid == -1) {
    die('cannot fork');
} elseif ($pid) {
    // parent catches INT and berates user
    declare(ticks = 1);
    function handle_sigint($signal) {
        echo "Tsk tsk, no process interruptus\n";
    }
    pcntl_signal(SIGINT, 'handle_sigint');
    while (!pcntl_waitpid($pid, $status, WNOHANG)) {}
} else {
    // child ignores INT and does its thing
    pcntl_signal(SIGINT, SIG_IGN);
    pcntl_exec('/bin/sleep', array('10'));
}

// -----------------------------

// Since there is no direct access to execv() and friends, and
// pcntl_exec() won't let us supply an alternate program name
// in the argument list, there is no way to run a command with
// a different name in the process table.

// @@PLEAC@@_16.3
// Transfer control to the shell to run another program.
pcntl_exec('/bin/sh', array('-c', 'archive *.data'));
// Transfer control directly to another program.
pcntl_exec('/path/to/archive', array('accounting.data'));

// @@PLEAC@@_16.4
// Handle each line in the output of a process.
$readme = popen('program arguments', 'r');
while (!feof($readme)) {
    $line = fgets($readme);
    if ($line === false) break;
    // ...
}
pclose($readme);

// -----------------------------

// Write to the input of a process.
$writeme = popen('program arguments', 'w');
fwrite($writeme, 'data');
pclose($writeme);

// -----------------------------

// Wait for a process to complete.
$f = popen('sleep 1000000', 'r');  // child goes to sleep
pclose($f);                        // and parent goes to lala land

// -----------------------------

$writeme = popen('program arguments', 'w');
fwrite($writeme, "hello\n");  // program will get hello\n on STDIN
pclose($writeme);             // program will get EOF on STDIN

// -----------------------------

// Output buffering callback that sends output to the pager.
function ob_pager($output, $mode) {
    static $pipe;
    if ($mode & PHP_OUTPUT_HANDLER_START) {
        $pager = getenv('PAGER');
        if (!$pager) $pager = '/usr/bin/less';  // XXX: might not exist
        $pipe = popen($pager, 'w');
    }
    fwrite($pipe, $output);
    if ($mode & PHP_OUTPUT_HANDLER_END) {
        pclose($pipe);
    }
}

// Redirect standard output to the pager.
ob_start('ob_pager');

// Do something useful that writes to standard output, then
// close the output buffer.
// ...
ob_end_flush();

// @@PLEAC@@_16.5
// Output buffering: Only display a certain number of lines of output.
class Head {
    function Head($lines=20) {
        $this->lines = $lines;
    }

    function filter($output, $mode) {
        $result = array();
        $newline = '';
        if (strlen($output) > 0 && $output[strlen($output) - 1] == "\n") {
            $newline = "\n";
            $output = substr($output, 0, -1);
        }
        foreach (explode("\n", $output) as $i => $line) {
            if ($this->lines > 0) {
                $this->lines--;
                $result[] = $line;
            }
        }
        return $result ? implode("\n", $result) . $newline : '';
    }
}

// Output buffering: Prepend line numbers to each line of output.
class Number {
    function Number() {
        $this->line_number = 0;
    }

    function filter($output, $mode) {
        $result = array();
        $newline = '';
        if (strlen($output) > 0 && $output[strlen($output) - 1] == "\n") {
            $newline = "\n";
            $output = substr($output, 0, -1);
        }
        foreach (explode("\n", $output) as $i => $line) {
            $this->line_number++;
            $result[] = $this->line_number . ': ' . $line;
        }
        return implode("\n", $result) . $newline;
    }
}

// Output buffering: Prepend "> " to each line of output.
class Quote {
    function Quote() {
    }

    function filter($output, $mode) {
        $result = array();
        $newline = '';
        if (strlen($output) > 0 && $output[strlen($output) - 1] == "\n") {
            $newline = "\n";
            $output = substr($output, 0, -1);
        }
        foreach (explode("\n", $output) as $i => $line) {
            $result[] = "> $line";
        }
        return implode("\n", $result) . $newline;
    }
}

// Use arrays as callbacks to register filter methods.
ob_start(array(new Head(100), 'filter'));
ob_start(array(new Number(), 'filter'));
ob_start(array(new Quote(), 'filter'));

// Act like /bin/cat.
while (!feof(STDIN)) {
    $line = fgets(STDIN);
    if ($line === false) break;
    echo $line;
}

// Should match number of calls to ob_start().
ob_end_flush();
ob_end_flush();
ob_end_flush();

// @@PLEAC@@_16.6
// Process command-line arguments using fopen(). PHP supports URLs for
// filenames as long as the "allow_url_fopen" configuration option is set.
//
// Valid URL protocols include:
//   - http://www.myserver.com/myfile.html
//   - ftp://ftp.myserver.com/myfile.txt
//   - compress.zlib://myfile.gz
//   - php://stdin
//
// See http://www.php.net/manual/en/wrappers.php for details.
//
$filenames = array_slice($argv, 1);
if (!$filenames) $filenames = array('php://stdin');
foreach ($filenames as $filename) {
    $handle = @fopen($filename, 'r');
    if ($handle) {
        while (!feof($handle)) {
            $line = fgets($handle);
            if ($line === false) break;
            // ...
        }
        fclose($handle);
    } else {
        die("can't open $filename\n");
    }
}

// @@PLEAC@@_16.7
$output = `cmd 2>&1`;                          // with backticks
// or
$ph = popen('cmd 2>&1');                       // with an open pipe
while (!feof($ph)) { $line = fgets($ph); }     // plus a read
// -----------------------------
$output = `cmd 2>/dev/null`;                   // with backticks
// or
$ph = popen('cmd 2>/dev/null');                // with an open pipe
while (!feof($ph)) { $line = fgets($ph); }     // plus a read
// -----------------------------
$output = `cmd 2>&1 1>/dev/null`;              // with backticks
// or
$ph = popen('cmd 2>&1 1>/dev/null');           // with an open pipe
while (!feof($ph)) { $line = fgets($ph); }     // plus a read
// -----------------------------
$output = `cmd 3>&1 1>&2 2>&3 3>&-`;           // with backticks
// or
$ph = popen('cmd 3>&1 1>&2 2>&3 3>&-|');       // with an open pipe
while (!feof($ph)) { $line = fgets($ph); }     // plus a read
// -----------------------------
exec('program args 1>/tmp/program.stdout 2>/tmp/program.stderr');
// -----------------------------
$output = `cmd 3>&1 1>&2 2>&3 3>&-`;
// -----------------------------
$fd3 = $fd1;
$fd1 = $fd2;
$fd2 = $fd3;
$fd3 = null;
// -----------------------------
exec('prog args 1>tmpfile 2>&1');
exec('prog args 2>&1 1>tmpfile');
// -----------------------------
// exec('prog args 1>tmpfile 2>&1');
$fd1 = "tmpfile";        // change stdout destination first
$fd2 = $fd1;             // now point stderr there, too
// -----------------------------
// exec('prog args 2>&1 1>tmpfile');
$fd2 = $fd1;             // stderr same destination as stdout
$fd1 = "tmpfile";        // but change stdout destination

// @@PLEAC@@_16.8
// Connect to input and output of a process.
$proc = proc_open($program,
                  array(0 => array('pipe', 'r'),
                        1 => array('pipe', 'w')),
                  $pipes);
if (is_resource($proc)) {
    fwrite($pipes[0], "here's your input\n");
    fclose($pipes[0]);
    echo stream_get_contents($pipes[1]);
    fclose($pipes[1]);
    $result_code = proc_close($proc);
    echo "$result_code\n";
}

// -----------------------------

$all = array();
$outlines = array();
$errlines = array();
exec("( $cmd | sed -e 's/^/stdout: /' ) 2>&1", $all);
foreach ($all as $line) {
    $pos = strpos($line, 'stdout: ');
    if ($pos !== false && $pos == 0) {
        $outlines[] = substr($line, 8);
    } else {
        $errlines[] = $line;
    }
}
print("STDOUT:\n");
print_r($outlines);
print("\n");
print("STDERR:\n");
print_r($errlines);
print("\n");

// @@PLEAC@@_16.9
$proc = proc_open($cmd,
                  array(0 => array('pipe', 'r'),
                        1 => array('pipe', 'w'),
                        2 => array('pipe', 'w')),
                  $pipes);

if (is_resource($proc)) {
    // give end of file to kid, or feed him
    fclose($pipes[0]);

    // read till EOF
    $outlines = array();
    while (!feof($pipes[1])) {
        $line = fgets($pipes[1]);
        if ($line === false) break;
        $outlines[] = rtrim($line);
    }

    // XXX: block potential if massive
    $errlines = array();
    while (!feof($pipes[2])) {
        $line = fgets($pipes[2]);
        if ($line === false) break;
        $errlines[] = rtrim($line);
    }

    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($proc);

    print("STDOUT:\n");
    print_r($outlines);
    print("\n");
    print("STDERR:\n");
    print_r($errlines);
    print("\n");
}

// -----------------------------

// cmd3sel - control all three of kids in, out, and error.
$cmd = "grep vt33 /none/such - /etc/termcap";
$proc = proc_open($cmd,
                  array(0 => array('pipe', 'r'),
                        1 => array('pipe', 'w'),
                        2 => array('pipe', 'w')),
                  $pipes);

if (is_resource($proc)) {
    fwrite($pipes[0], "This line has a vt33 lurking in it\n");
    fclose($pipes[0]);

    $readers = array($pipes[1], $pipes[2]);
    while (stream_select($read=$readers,
                         $write=null,
                         $except=null,
                         0, 200000) > 0) {
        foreach ($read as $stream) {
            $line = fgets($stream);
            if ($line !== false) {
                if ($stream === $pipes[1]) {
                    print "STDOUT: $line";
                } else {
                    print "STDERR: $line";
                }
            }
            if (feof($stream)) {
                $readers = array_diff($readers, array($stream));
            }
        }
    }

    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($proc);
}

// @@PLEAC@@_16.10
// PHP supports fork/exec/wait but not pipe. However, it does
// support socketpair, which can do everything pipes can as well
// as bidirectional communication. The original recipes have been
// modified here to use socketpair only.

// -----------------------------

// pipe1 - use socketpair and fork so parent can send to child
$sockets = array();
if (!socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets)) {
    die(socket_strerror(socket_last_error()));
}
list($reader, $writer) = $sockets;

$pid = pcntl_fork();
if ($pid == -1) {
    die('cannot fork');
} elseif ($pid) {
    socket_close($reader);
    $line = sprintf("Parent Pid %d is sending this\n", getmypid());
    if (!socket_write($writer, $line, strlen($line))) {
        socket_close($writer);
        die(socket_strerror(socket_last_error()));
    }
    socket_close($writer);
    pcntl_waitpid($pid, $status);
} else {
    socket_close($writer);
    $line = socket_read($reader, 1024, PHP_NORMAL_READ);
    printf("Child Pid %d just read this: `%s'\n", getmypid(), rtrim($line));
    socket_close($reader);  // this will happen anyway
    exit(0);
}

// -----------------------------

// pipe2 - use socketpair and fork so child can send to parent
$sockets = array();
if (!socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets)) {
    die(socket_strerror(socket_last_error()));
}
list($reader, $writer) = $sockets;

$pid = pcntl_fork();
if ($pid == -1) {
    die('cannot fork');
} elseif ($pid) {
    socket_close($writer);
    $line = socket_read($reader, 1024, PHP_NORMAL_READ);
    printf("Parent Pid %d just read this: `%s'\n", getmypid(), rtrim($line));
    socket_close($reader);
    pcntl_waitpid($pid, $status);
} else {
    socket_close($reader);
    $line = sprintf("Child Pid %d is sending this\n", getmypid());
    if (!socket_write($writer, $line, strlen($line))) {
        socket_close($writer);
        die(socket_strerror(socket_last_error()));
    }
    socket_close($writer);  // this will happen anyway
    exit(0);
}

// -----------------------------

// pipe3 and pipe4 demonstrate the use of perl's "forking open"
// feature to reimplement pipe1 and pipe2. pipe5 uses two pipes
// to simulate socketpair. Since PHP supports socketpair but not
// pipe, and does not have a "forking open" feature, these
// examples are skipped here.

// -----------------------------

// pipe6 - bidirectional communication using socketpair
$sockets = array();
if (!socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets)) {
    die(socket_strerror(socket_last_error()));
}
list($child, $parent) = $sockets;

$pid = pcntl_fork();
if ($pid == -1) {
    die('cannot fork');
} elseif ($pid) {
    socket_close($parent);
    $line = sprintf("Parent Pid %d is sending this\n", getmypid());
    if (!socket_write($child, $line, strlen($line))) {
        socket_close($child);
        die(socket_strerror(socket_last_error()));
    }
    $line = socket_read($child, 1024, PHP_NORMAL_READ);
    printf("Parent Pid %d just read this: `%s'\n", getmypid(), rtrim($line));
    socket_close($child);
    pcntl_waitpid($pid, $status);
} else {
    socket_close($child);
    $line = socket_read($parent, 1024, PHP_NORMAL_READ);
    printf("Child Pid %d just read this: `%s'\n", getmypid(), rtrim($line));
    $line = sprintf("Child Pid %d is sending this\n", getmypid());
    if (!socket_write($parent, $line, strlen($line))) {
        socket_close($parent);
        die(socket_strerror(socket_last_error()));
    }
    socket_close($parent);
    exit(0);
}

// @@PLEAC@@_16.11
// -----------------------------
// % mkfifo /path/to/named.pipe
// -----------------------------

$fifo = fopen('/path/to/named.pipe', 'r');
if ($fifo !== false) {
    while (!feof($fifo)) {
        $line = fgets($fifo);
        if ($line === false) break;
        echo "Got: $line";
    }
    fclose($fifo);
} else {
    die('could not open fifo for read');
}

// -----------------------------

$fifo = fopen('/path/to/named.pipe', 'w');
if ($fifo !== false) {
    fwrite($fifo, "Smoke this.\n");
    fclose($fifo);
} else {
    die('could not open fifo for write');
}

// -----------------------------
// % mkfifo ~/.plan                    #  isn't this everywhere yet?
// % mknod  ~/.plan p                  #  in case you don't have mkfifo
// -----------------------------

// dateplan - place current date and time in .plan file
while (true) {
    $home = getenv('HOME');
    $fifo = fopen("$home/.plan", 'w');
    if ($fifo === false) {
        die("Couldn't open $home/.plan for writing.\n");
    }
    fwrite($fifo,
           'The current time is '
           . strftime('%a, %d %b %Y %H:%M:%S %z')
           . "\n");
    fclose($fifo);
    sleep(1);
}

// -----------------------------

// fifolog - read and record log msgs from fifo

$fifo = null;

declare(ticks = 1);
function handle_alarm($signal) {
    global $fifo;
    if ($fifo) fclose($fifo);   // move on to the next queued process
}
pcntl_signal(SIGALRM, 'handle_alarm');

while (true) {
    pcntl_alarm(0);             // turn off alarm for blocking open
    $fifo = fopen('/tmp/log', 'r');
    if ($fifo === false) {
        die("can't open /tmp/log");
    }
    pcntl_alarm(1);             // you have 1 second to log

    $service = fgets($fifo);
    if ($service === false) continue; // interrupt or nothing logged
    $service = rtrim($service);

    $message = fgets($fifo);
    if ($message === false) continue; // interrupt or nothing logged
    $message = rtrim($message);

    pcntl_alarm(0);             // turn off alarms for message processing

    if ($service == 'http') {
        // ignoring
    } elseif ($service == 'login') {
        // log to /var/log/login
        $log = fopen('/var/log/login', 'a');
        if ($log !== false) {
            fwrite($log,
                   strftime('%a, %d %b %Y %H:%M:%S %z')
                   . " $service $message\n");
            fclose($log);
        } else {
            trigger_error("Couldn't log $service $message to /var/log/login\n",
                          E_USER_WARNING);
        }
    }
}

// @@PLEAC@@_16.12
// sharetest - test shared variables across forks

$SHM_KEY = ftok(__FILE__, chr(1));
$handle = sem_get($SHM_KEY);
$buffer = shm_attach($handle, 1024);

// The original recipe has an INT signal handler here. However, it
// causes erratic behavior with PHP, and PHP seems to do the right
// thing without it.

for ($i = 0; $i < 10; $i++) {
    $child = pcntl_fork();
    if ($child == -1) {
        die('cannot fork');
    } elseif ($child) {
        $kids[] = $child; // in case we care about their pids
    } else {
        squabble();
        exit();
    }
}

while (true) {
    print 'Buffer is ' . shm_get_var($buffer, 1) . "\n";
    sleep(1);
}
die('Not reached');

function squabble() {
    global $handle;
    global $buffer;
    $i = 0;
    $pid = getmypid();
    while (true) {
        if (preg_match("/^$pid\\b/", shm_get_var($buffer, 1))) continue;
        sem_acquire($handle);
        $i++;
        shm_put_var($buffer, 1, "$pid $i");
        sem_release($handle);
    }
}

// Buffer is 14357 1
// Buffer is 14355 3
// Buffer is 14355 4
// Buffer is 14354 5
// Buffer is 14353 6
// Buffer is 14351 8
// Buffer is 14351 9
// Buffer is 14350 10
// Buffer is 14348 11
// Buffer is 14348 12
// Buffer is 14357 10
// Buffer is 14357 11
// Buffer is 14355 13
// ...

// @@PLEAC@@_16.13
// Available signal constants
% php -r 'print_r(get_defined_constants());' | grep '\[SIG' | grep -v _
    [SIGHUP] => 1
    [SIGINT] => 2
    [SIGQUIT] => 3
    [SIGILL] => 4
    [SIGTRAP] => 5
    [SIGABRT] => 6
    [SIGIOT] => 6
    [SIGBUS] => 7
    [SIGFPE] => 8
    [SIGKILL] => 9
    [SIGUSR1] => 10
    [SIGSEGV] => 11
    [SIGUSR2] => 12
    [SIGPIPE] => 13
    [SIGALRM] => 14
    [SIGTERM] => 15
    [SIGSTKFLT] => 16
    [SIGCLD] => 17
    [SIGCHLD] => 17
    [SIGCONT] => 18
    [SIGSTOP] => 19
    [SIGTSTP] => 20
    [SIGTTIN] => 21
    [SIGTTOU] => 22
    [SIGURG] => 23
    [SIGXCPU] => 24
    [SIGXFSZ] => 25
    [SIGVTALRM] => 26
    [SIGPROF] => 27
    [SIGWINCH] => 28
    [SIGPOLL] => 29
    [SIGIO] => 29
    [SIGPWR] => 30
    [SIGSYS] => 31
    [SIGBABY] => 31

// Predefined signal handler constants
% php -r 'print_r(get_defined_constants());' | grep '\[SIG' | grep _
    [SIG_IGN] => 1
    [SIG_DFL] => 0
    [SIG_ERR] => -1

// @@PLEAC@@_16.14
// send pid a signal 9
posix_kill($pid, 9);
// send whole job a signal 1
posix_kill($pgrp, -1);
// send myself a SIGUSR1
posix_kill(getmypid(), SIGUSR1);
// send a SIGHUP to processes in pids
foreach ($pids as $pid) posix_kill($pid, SIGHUP);

// -----------------------------

// Use kill with pseudo-signal 0 to see if process is alive.
if (posix_kill($minion, 0)) {
    echo "$minion is alive!\n";
} else {
    echo "$minion is deceased.\n";
}

// @@PLEAC@@_16.15
// call got_sig_quit for every SIGQUIT
pcntl_signal(SIGQUIT, 'got_sig_quit');
// call got_sig_pipe for every SIGPIPE
pcntl_signal(SIGPIPE, 'got_sig_pipe');
// increment ouch for every SIGINT
function got_sig_int($signal) { global $ouch; $ouch++; }
pcntl_signal(SIGINT, 'got_sig_int');
// ignore the signal INT
pcntl_signal(SIGINT, SIG_IGN);
// restore default STOP signal handling
pcntl_signal(SIGSTOP, SIG_DFL);

// @@PLEAC@@_16.16
// the signal handler
function ding($signal) {
    fwrite(STDERR, "\x07Enter your name!\n");
}

// prompt for name, overriding SIGINT
function get_name() {
    declare(ticks = 1);
    pcntl_signal(SIGINT, 'ding');

    echo "Kindly Stranger, please enter your name: ";
    while (!@stream_select($read=array(STDIN),
                           $write=null,
                           $except=null,
                           1)) {
        // allow signals to be observed
    }
    $name = fgets(STDIN);

    // Since pcntl_signal() doesn't return the old signal handler, the
    // best we can do here is set it back to the default behavior.
    pcntl_signal(SIGINT, SIG_DFL);

    return $name;
}

// @@PLEAC@@_16.17
function got_int($signal) {
    pcntl_signal(SIGINT, 'got_int');  // but not for SIGCHLD!
    // ...
}
pcntl_signal(SIGINT, 'got_int');

// -----------------------------

declare(ticks = 1);
$interrupted = false;

function got_int($signal) {
    global $interrupted;
    $interrupted = true;
    // The third argument to pcntl_signal() determines if system calls
    // should be restarted after a signal. It defaults to true.
    pcntl_signal(SIGINT, 'got_int', false);  // or SIG_IGN
}
pcntl_signal(SIGINT, 'got_int', false);

// ... long-running code that you don't want to restart

if ($interrupted) {
    // deal with the signal
}

// @@PLEAC@@_16.18
// ignore signal INT
pcntl_signal(SIGINT, SIG_IGN);

// install signal handler
declare(ticks = 1);
function tsktsk($signal) {
    fwrite(STDERR, "\x07The long habit of living indisposeth us for dying.");
    pcntl_signal(SIGINT, 'tsktsk');
}
pcntl_signal(SIGINT, 'tsktsk');

// @@PLEAC@@_16.19
pcntl_signal(SIGCHLD, SIG_IGN);

// -----------------------------

declare(ticks = 1);
function reaper($signal) {
    $pid = pcntl_waitpid(-1, $status, WNOHANG);
    if ($pid > 0) {
        // ...
        reaper($signal);
    }
    // install *after* calling waitpid
    pcntl_signal(SIGCHLD, 'reaper');
}
pcntl_signal(SIGCHLD, 'reaper');

// -----------------------------

declare(ticks = 1);
function reaper($signal) {
    $pid = pcntl_waitpid(-1, $status, WNOHANG);
    if ($pid == -1) {
        // No child waiting. Ignore it.
    } else {
        if (pcntl_wifexited($signal)) {
            echo "Process $pid exited.\n";
        } else {
            echo "False alarm on $pid\n";
        }
        reaper($signal);
    }
    pcntl_signal(SIGCHLD, 'reaper');
}
pcntl_signal(SIGCHLD, 'reaper');

// @@PLEAC@@_16.20
// PHP does not support sigprocmask().

// @@PLEAC@@_16.21
declare(ticks = 1);
$aborted = false;

function handle_alarm($signal) {
    global $aborted;
    $aborted = true;
}
pcntl_signal(SIGALRM, 'handle_alarm');

pcntl_alarm(3600);
// long-time operations here
pcntl_alarm(0);
if ($aborted) {
    // timed out - do what you will here
}