Idiomatic for the people — a Zend_Array proposal

©2010 Nigel Chapman: License: BSD License; this page last modified 31 January 2010.

Abstract

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.

Introduction

Zend_Array implements chainable array operations that work well with closures.

<?php 

    
echo Zend_Array::take(array('World!''Hello,'))->reverse()->join(' '); 
    
Hello, World!

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(); 

Sample Data

The following sample data will be used in the examples below.

<?php 

    $fibonacci 
Zend_Array::take(array(1235811)); 

    
$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->firstname01); 
        }

        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),
        )); 

"Get Statements"

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]

EXAMPLE 1. Numbers and strings

<?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(); 

EXAMPLE 2. Manipulating arrays

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(33)->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

EXAMPLE 3. Sorting and collating

Sorting by keys or attributes

<?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

Sorting by values

<?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

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

EXAMPLE 4. Tree traversal

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(', ');  

0, 1, 4, 9, 16, 25, 36, 49, 64, 81

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...

Extensions

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.

Zend_Array_Flot / jquery.flot.js

Just a simple proof-of-concept.

Flot | Examples

<?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(); 

Download Files

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:

Efficiency

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.

Proposal

http://framework.zend.com/wiki/display/ZFPROP/Zend_Array+-+Nigel+Chapman