©2010 Nigel Chapman: License: BSD License; this page last modified 31 January 2010.
Zend_Array is a 'fluent' wrapper for PHP's array functions, giving them a chainable interface that works well with closures, i.e. anonymous or lambda functions; functions stored in variables or passed as arguments to other functions. It has been influenced considerably by Ruby and JQuery, and makes PHP more idiomatic. It is designed for ease of use, in line with the UNIX philosophy of simple and reliable tools, which become poweful in combination. It is very new software, and you should expand the test suite to your satisfaction before using it for anything important.
Zend_Array implements chainable array operations that work well with closures.
<?php
echo Zend_Array::take(array('World!', 'Hello,'))->reverse()->join(' ');
Though it does a lot more, it's good at simple data pipelines:
<?php
$csv = '"Blogs, Fred",30,45354.50,"She\'ll be right!"' . "\n"
. '"Jung, Francis",67,0.00,"No worries!"' . "\n"
. '"Jones, Dave",44,37500,"Owzat, ay?"';
$format = "%-15s %3d y.o. %9.2f p.a. \"%s\"";
echo "<pre>";
echo Zend_Array::takeCsv($csv)->sprintf($format)->joinLines();
echo "</pre>\n\n";
Blogs, Fred 30 y.o. 45354.50 p.a. "She'll be right!" Jung, Francis 67 y.o. 0.00 p.a. "No worries!" Jones, Dave 44 y.o. 37500.00 p.a. "Owzat, ay?"
Here's how it does a Schwartzian Transform in PHP, using the classic example of sorting lines by their last words.
<?php
// Excerpt from 'Epigrams on Programming' by Alan J. Perlis.
// http://www-pu.informatik.uni-tuebingen.de/users/klaeren/epigrams.html
$epigrams = Zend_Array::takeLines(
"One man's constant is another man's variable.
Functions delay binding: data structures induce binding. Moral: Structure data late in the programming process.
Syntactic sugar causes cancer of the semi-colons.
Every program is a part of some other program and rarely fits.
If a program manipulates a large amount of data, it does so in a small number of ways.");
$lastWord = create_function('$s', 'return end(split(" ", trim($s, " .!?")));');
// In PHP 5.3: $lastWord = function ($s) { return end(split(' ', trim($s, " .?!"))); }
echo $epigrams->sort($lastWord)
->highlight($lastWord, '<span class="highlight">')
->htmlList();
The following sample data will be used in the examples below.
<?php
$fibonacci = Zend_Array::take(array(1, 2, 3, 5, 8, 11));
$words = Zend_Array::takeWords("The quick brown fox jumped over the lazy dog.");
// http://en.wikipedia.org/wiki/States_and_territories_of_Australia
$populationDensity = Zend_Array::takeCsv(
'"Australian Capital Territory","339900","2358","144.1"
"Victoria","5205200","227146","22.9"
"New South Wales","6889100","800642","8.6"
"Tasmania","493300","68401","7.2"
"Queensland","4182100","1730648","2.4"
"South Australia","1584500","983482","1.6"
"Western Australia","2105800","2529875","0.8"
"Northern Territory","215000","1349129","0.2"');
// Some anonymous functions
$lowerCase = create_function('$s', 'return strtolower($s);');
$stringLength = create_function('$s', 'return strlen($s);');
$stringReverse = create_function('$s', 'return strrev($s);');
// And a simple 'person' object
class Person
{
public function __construct($firstname, $surname, $age, $postcode, $income)
{
$this->firstname = $firstname;
$this->surname = $surname;
$this->age = (int)$age;
$this->postcode = $postcode;
$this->income = $income;
}
public function firstLetter()
{
return substr($this->firstname, 0, 1);
}
public function ageBracket()
{
$decade = floor($this->age / 10);
return ($decade * 10) . '-' . ((($decade + 1) * 10) - 1);
}
}
$people = Zend_Array::take(array(
new Person('Dean', 'Hendricksen', 19, '4010', 25000.00),
new Person('Derek', 'Grimmett', 43, '3923', 34702.90),
new Person('Monique', 'Depardieu', 41, '3829', 56376.45),
new Person('Dave', 'Jones', 76, '3830', 32500),
));
Much of Zend_Array's functionality depends on a single function called 'get'. It figures out what you're looking for in the elements of an array:
When I call sort($x), $x can be any argument recognized by get().
<?php
// Get an array key or object property from each object:
echo "<p>" . $people->get('surname')->join(', ') . "</p>\n";
// Get the result of a method call on each object.
echo "<p>" . $people->get('ageBracket')->join(', ') . "</p>\n";
// Get the result of a user-defined function run on each element:
$initials = create_function('$p', 'return $p->firstname[0].$p->surname[0];');
echo "<p>" . $people->get($initials)->join(', ') . "</p>\n";
// Or combine these effects:
echo $people->get(array('surname', $initials, 'ageBracket'))
->sprintf("<b>%s</b> (%s, %s)")
->htmlList();
Hendricksen, Grimmett, Depardieu, Jones
10-19, 40-49, 40-49, 70-79
DH, DG, MD, DJ
PHP's own library of functions can be used as well (@todo: take arguments):
<?php
// Observe that this recurses into subarrays if it finds them...
echo $people->php('get_object_vars')->php('strtoupper')->htmlTable();
echo "<p>" . $people->get('surname')->php('strlen')->json() . "</p>\n";
| DEAN | HENDRICKSEN | 19 | 4010 | 25000 |
| DEREK | GRIMMETT | 43 | 3923 | 34702.9 |
| MONIQUE | DEPARDIEU | 41 | 3829 | 56376.45 |
| DAVE | JONES | 76 | 3830 | 32500 |
[11,8,9,5]
<?php
$f = $fibonacci; // See Above
$p = $populationDensity->get('1'); // 2nd column
$a = $populationDensity->get('2'); // 3rd column
echo Zend_Array::take(array(
array("" , 'Minimum', 'Maximum', 'Average', 'Std. Dev.'),
array("Fibonacci" , $f->min(), $f->max(), $f->average(), $f->stddev()),
array("Population" , $p->min(), $p->max(), $p->average(), $p->stddev()),
array("Area" , $a->min(), $a->max(), $a->average(), $a->stddev()),
))->htmlTable();
| Minimum | Maximum | Average | Std. Dev. | |
| Fibonacci | 1 | 11 | 5 | 3.51188458428 |
| Population | 215000 | 6889100 | 2626862.5 | 2350283.20897 |
| Area | 2358 | 2529875 | 961460.125 | 828014.078777 |
<?php
// Simple replace, incl. case-insentitive
echo Zend_Array::take(array(
$words->replace('the', '***')->join(' '),
$words->replace('/the/i', '***')->join(' '),
))->htmlList();
<?php
// Boldfacing vowels. There's more than one way to do it.
$bold = create_function('$s', 'return "<b>$s[0]</b>";');
echo Zend_Array::take(array(
$words->replace('/[aeiou]/', '<b>$0</b>')->join(' '),
$words->replace('/[aeiou]/', $bold)->join(' '),
$words->replace(array('a', 'e', 'i', 'o', 'u'), $bold)->join(' '),
// ... but you would use regexps for case-insesitivity
// in most practical situations.
))->htmlList();
<?php
// Some utility functions
$containsT = create_function('$s', 'return stripos($s, "t") !== false;');
echo Zend_Array::take(array(
$words->grep('/T/i')->join(' '),
$words->filter($containsT)->join(' '),
))->htmlList();
Arrays can be chopped up into smaller arrays in several ways:
<?php
echo Zend_Array::take(array(
$words->first(),
$words->first(5)->join(' ... '),
$words->slice(3, 3)->join(' ... '),
$words->last(3)->join(' ... '),
$words->random(),
$words->random(3)->join(' ... '), // Reload
))->htmlList();
echo $words->columns(3)->htmlTable();
| The | fox | the |
| quick | jumped | lazy |
| brown | over | dog |
<?php
echo $populationDensity->sort('0')->htmlTable(); // Sort first column
| Australian Capital Territory | 339900 | 2358 | 144.1 |
| New South Wales | 6889100 | 800642 | 8.6 |
| Northern Territory | 215000 | 1349129 | 0.2 |
| Queensland | 4182100 | 1730648 | 2.4 |
| South Australia | 1584500 | 983482 | 1.6 |
| Tasmania | 493300 | 68401 | 7.2 |
| Victoria | 5205200 | 227146 | 22.9 |
| Western Australia | 2105800 | 2529875 | 0.8 |
<?php
echo Zend_Array::take(array(
array('ASCII Ascending', $words->sort()->join(', ')),
array('ASCII Descending', $words->sort()->reverse()->join(', ')),
array('Transform and sort', $words->map($lowerCase)->sort()->join(', ')),
array('(and make unique)', $words->map($lowerCase)->sort()->unique()->join(', ')),
array('Sort by function result', $words->sort($lowerCase)->join(', ')),
array('Multisort', $words->sort(array($stringLength, $lowerCase))->join(', ')),
))->htmlTable();
| ASCII Ascending | The, brown, dog, fox, jumped, lazy, over, quick, the |
| ASCII Descending | the, quick, over, lazy, jumped, fox, dog, brown, The |
| Transform and sort | brown, dog, fox, jumped, lazy, over, quick, the, the |
| (and make unique) | brown, dog, fox, jumped, lazy, over, quick, the |
| Sort by function result | brown, dog, fox, jumped, lazy, over, quick, The, the |
| Multisort | The, dog, fox, the, lazy, over, brown, quick, jumped |
Collation means grouping arrays for iteration; you can collate by several values in one operation.
<?php
// Group by age bracket, sort DESC by surname
echo "<table>\n";
foreach ($people->collate(array('ageBracket'))->done() as $ageBracket => $peopleInBracket) {
echo "<tr><th colspan=\"5\">$ageBracket</th></tr>\n";
echo Zend_Array::take($peopleInBracket)->sort('-surname')->htmlTableRows();
}
echo "</table>\n";
| 10-19 | ||||
|---|---|---|---|---|
| Dean | Hendricksen | 19 | 4010 | 25000 |
| 40-49 | ||||
| Derek | Grimmett | 43 | 3923 | 34702.9 |
| Monique | Depardieu | 41 | 3829 | 56376.45 |
| 70-79 | ||||
| Dave | Jones | 76 | 3830 | 32500 |
<?php
// Group by first initial, sort by age.
echo "<table>\n";
foreach ($people->collate(array('firstLetter'))->done() as $letter => $peopleStartingWith) {
echo "<tr><th colspan=\"3\">$letter</th></tr>\n";
echo Zend_Array::take($peopleStartingWith)->get(array('surname', 'firstname', 'age'))->sort('2')->htmlTableRows();
}
echo "</table>\n";
| D | ||
|---|---|---|
| Hendricksen | Dean | 19 |
| Grimmett | Derek | 43 |
| Jones | Dave | 76 |
| M | ||
| Depardieu | Monique | 41 |
Because all Zend_Array operations recurse by default, tree traversal has already been observed in some of the operations above.
<?php
$square = create_function(
'$s',
'if (!is_array($s)) { return (int)$s * (int)$s; }'
);
$tree = Zend_Array::take(array(
array(
0,
1,
array(
2,
3,
),
array(
4,
5,
6,
),
),
7,
8,
array(
array(
array(
9,
),
),
),
));
// Two depth-first traversals:
echo $tree->map($square)->flatten()->join(', ');
A more serious example would start with Zend_Array::takeXML($str), or takeXMLFile($path) — unwritten, but easy to wrap SimpleXML. But would XML be part of the core functionality? Or do we need...
Zend_Array should contain only a minimal set of functionality for array topography, tree traversal, mapping and filtering constructions, basic string and number operations, and HTML. Anything specialized can go into extensions.
Just a simple proof-of-concept.
<?php
// Must sort before load prior to 5.3; static binding issue.
echo $people->sort('age')->load('Flot')
->barGraph(
$get = array('age', 'income'),
$options = array(
'label' => 'Income vs. Age',
'color' => 'blue',
))
->lineGraph(
$get = array('age', 'postcode'),
$options = array(
'label' => 'Postcode vs. Age',
'color' => 'green',
'yaxis' => 2,
))
->htmlCanvas();
Everything on this page is generated live; the examples are the actual code. The source code for Zend_Array can be viewed here:
To get everything necessary to render this page (incl. a copy of Zend Loader, it's only dependency) and a copy of JQuery with the Flot Graphing library, click here:
This is not presently an exercise in efficiency but rather idiom and convenience. Having said that, there's no reason the core functions can't be optimized if necessary.
http://framework.zend.com/wiki/display/ZFPROP/Zend_Array+-+Nigel+Chapman