Fluent Arrays and Strings in PHP

I’ve been working some with jQuery and Ruby lately, as you might know they both have very neat fluent interfaces for writing short and easily understandable code. Especially Ruby’s array and string handling should be something that can be done in PHP so I started googling for something nice but so far the best I’ve been able to find is pooQuery. Update: Apparently pooQuery really is a joke (see the comments at the bottom of this post), anyway here is an example from it of how fluent stuff can go too far:

take('Hello World')->and->call('substr')->with(array(it(), 0, 5))
        ->and->take(call()->result)->and->display();

It’s probably very possible that what I’m looking for is already part of some PHP framework or such but I didn’t want to spend more time looking than being productive. So back to the drawing board with Ruby as the inspiration with a little help from Lisp (I’ve “stolen” function names from both), take for instance this example from the wxRuby editor:

keywords = arr.find_all{|wrd|  
      wrd.downcase.index(sub_str) == 0 && wrd.downcase != sub_str 
    }.uniq.sort.join(" ").strip

At the moment we don’t have something like the block functionality demonstrated above in PHP. It will never be as clean because we have have to resort to writing our code as a string one way or the other with all the special headaches that comes with it. However the typical usage of a block is some simple test to filter with, just like in the above example and in such cases we can do pretty good with PHP. Here is the PHP equivalent:

$arr = array('Apple', 'Applepie', 'Appletart', 'apples', 'apples', 'orangepie', 'orangeapple', 'purple', 'red', 'applemousse');
Arr::fluent($arr)->collect(
		"(strripos('??', 'apple') === 0 && strtolower('??') != 'apple')"
	)->uniq()->sort()->join()->trim()->out();

Output:

Applepie Appletart applemousse apples

2D example:

$arr = array(
	array('name' => 'John', 'age' => 65), 
	array('name' => 'Mary', 'age' => 32), 
	array('name' => 'Jonny', 'age' => 45));
Arr::fluent2d($arr)->collect(
			"strripos('??', 'jo') !== false", false, 'name'
		)->normal2d()->out();

Output:

Array
(
    [0] => Array
        (
            [name] => John
            [age] => 65
        )

    [2] => Array
        (
            [name] => Jonny
            [age] => 45
        )

)

Not too bad. What I’ve done is basically wrapping all the array (as Arr) and string (as Str) functions I use frequently in a fluent interface, 80% of the code is very straightforward and self evident, some functions like collect() above requires a little explaining though. Basically all the “magic” in this little library is accomplished with eval(), func_get_args() and call_user_func_array(). Note the use of place holders in the form of ‘??’, the reason we don’t use ‘?’ is because ? is an operand in some expressions.

Let’s take a look at the code:

function collect($expr, $doKey = false, $key = false){
  if($this->d == 1) 	return $this->common_eval($expr, true, $doKey);
  else			    return $this->common_2d('collect', $expr, $doKey, $key);
}

private function common_2d($f, $expr, $doKey, $key){
  $old 		 = $this->normal2d(true);
  $doKey 	= $doKey == false ? 'false' : 'true';
  $new 		= $this->apply2d($f, "\"$expr\"", $doKey)->compact($key)->normal2d(true);
  $this->c 	= $old;
  $this->make2d();
  return $this->isectKey($new);
}

private function common_eval($expr, $bool2, $doKey = false){
  foreach($this->c as $key => $a){
    $tmp 	 = $doKey == false ? $a : $key;
    $replace 	= ($a instanceof Str && !$doKey) ? $a->c : $tmp;
    eval($this->fix_expr($expr, $replace));
    if($result == $bool2) $rarr[$key] = $a;
  }
  return $this->ret($rarr);
}

private function fix_expr($expr, $replace){ 
  return "\$result = ".str_replace('??', $replace, $expr).";"; 
}



The dimensionality of the array is flagged with $this->d which means that the library is working with either normal arrays or tables (sub arrays have same size), no hybrids or higher dimensions thanks. Luckily hybrids are rare in real life (at least in my life). $doKey flags if we are going to work with the keys (true) or the values (false). If the array is two dimensional we also have to pass $key to let the code know which column we want to work with. In the above case we work with the names.

Let’s start with the 1D example, it’s less complex. We will pass the expression to work with, a boolean that is used to either keep or delete whatever we manage to match, del() is a synonym for passing false, collect will of course pass true since we want to keep the matches. In both the examples above $doKey is false, we work with the values. We continue with looping through the containter, $this->c. If an element ($a) in turn is a fluent object we need to be able to handle this and work with its container, hence the instanceof Str test, Str is the name of our fluent string class. Finally we evaluate, and as you can see the result of the evaluation will be stored in $result which will be available to the evaluating code, if the result is true we keep the value.

The 2D example is more complex, we now make use of apply2d() and compact() too:

function apply2d(){
  $params 	= func_get_args();
  $f 		    = array_shift($params);
  foreach($this->c as &$a){
    $param_str = implode(',', $params);
    eval("\$a = \$a->$f($param_str);");
  }
  return $this;	
}

function compact($onKey = false){
  foreach($this->c as $key => $a){
    $tmp = $this->d == 1 ? $a : $a->c;
    $tmp = $onKey === false ? $tmp : $tmp[$onKey];
    if(!empty($tmp)) 
      $rarr[$key] = $a;
  }
  return $this->ret($rarr);
}

Due to the fact that apply2d() works with the container (c) as a reference we need to store the initial state of the container before we start working with it - in order to be able to later compare the before and after states. normal2d() will per default convert the current container to a normal 2D array, not an array of fluent Arr objects. If passed true it will return the 2D array instead which is what we are doing here.

The contents of $doKey needs to be passed as a string since they will later be evaluated as parts of one, hence the second line in the common_2d function. The new resultant array will be stored in $new, the current container will now also have been changed and we restore everything by setting it to the old value we stored in the beginning of the function followed by calling make2d() which will convert the container from a normal 2D array to an array of Arr objects, we are now completely back to the original state and have the new array in $new.

That’s why we can use isectKey() to get rid of all the entries of our container that do not belong in the $new array:

function isectKey($arr){ 
  $this->c = array_intersect_key($this->c, $arr); return $this; 
}

In all our manipulations we preserve the keys of the resultant arrays, hence the manipulation with array_intersect_key.

The need to go from string to array and/or back will frequently arise, for instance when unserializing/serializing and splitting/joining. There are more complex functions though that can be used when we have a fluent array filled with fluent strings, we can then walk the array and apply a certain method to each member:

$arr = array('banana-split', 'apple-pie');
Arr::fluent($arr)->makeStrs()->strApply('split', "'-'")->apply2d('join')->normalStrs()->out();
}

Output:

Array
(
    [0] => banana split
    [1] => apple pie
)

Here we first create a fluent array of course, then we turn each member in the array into Str instances, then we apply the Str method split() to each string, that will give us a fluent array containing an array of fluent arrays which we can apply the Arr method join() to. The result will be a fluent array containing fluent Strings, we’re back where we started, finally we apply normalStrs() to get a fluent array filled with normal strings. Let’s look at strApply:

function strApply(){
  $params 	= func_get_args();
  $f 			= array_shift($params);
  $param_str 	= implode(',',$params);
  foreach($this->c as &$a){
    if($a instanceof Str)
      eval("\$a = \$a->$f($param_str);");
  }
  return $this;
}

Very similar to apply2d, except we have to check each member for compatibility with instanceof in case we have a mixed array.

Another method that checks for compatibility is setIf() which as its name implies will set all values that matches an expression to something else:

$arr = array(new Str('banana'), 'banana', 'orange');
Arr::fluent($arr)->setIf("'??' == 'banana'", 'apple' )->out();

Will output:

Array
(
    [0] => Str Object
        (
            [c] => apple
        )

    [1] => apple
    [2] => orange
)

Again we check with instanceof :

function setIf($expr, $assign){
  foreach($this->c as &$a){
    $str_q 	= $a instanceof Str;
    $tmp 	= $str_q ? $a->c : $a;
    eval($this->fix_expr($expr, $tmp));
    if($result){
      if($str_q) 	$a->c 	= $assign;
      else		 $a 	   = $assign;
    }
  }
  return $this;
}

If we have a Str we use the container instead. Let’s take a look at sorting:

$arr = array(
	array('name' => 'John', 'age' => 65), 
	array('name' => 'Mary', 'age' => 32), 
	array('name' => 'Alfred', 'age' => 45));
Arr::fluent2d($arr)->serial()->unserial()->make2d()
	->sort('desc', 'age')->normal2d()->out();

Will output:

Array
(
    [0] => Array
        (
            [name] => John
            [age] => 65
        )

    [1] => Array
        (
            [name] => Alfred
            [age] => 45
        )

    [2] => Array
        (
            [name] => Mary
            [age] => 32
        )
)

The sort() method is kind of messy and based on what was arrived at in the sorting 2d arrays in php post:

function sort($flags = null, $key = null){
  if($this->d == 1){
    switch($flags){
      case'num': 	$flag = SORT_NUMERIC; break;
      case'string': 	 $flag = SORT_STRING; break;
      default: 		  $flag = SORT_REGULAR; break;
    }
    asort($this->c, $flag); 
    return $this;
  }else{
    foreach($this->c as $sub) $s_col[] = $sub->c[$key];
    $s_type   = is_string($s_col[0]) ? SORT_STRING : SORT_NUMERIC;
    $arr 	= $this->normal2d()->c;
    $flag 	= $flags == 'desc' ? SORT_DESC : SORT_ASC;
    if($s_type == SORT_STRING) $s_col = array_map('strtolower', $s_col);
    array_multisort($s_col, $s_type, $flag, $arr);
    return self::fluent2d($arr);
  }
}

As you can see, the $flags argument can be ‘desc’ or ‘asc’ to sort in a descending or ascending fashion - if we are sorting a 2d array ($this->d is 2), then we also need a column name to sort by, in this case ‘age’. Talking about array_map, let’s check the map() method:

Arr::fluent2d($arr)->map('strtolower')->flatten('name')->sort()->out();

Will output

Array
(
    [2] => alfred
    [0] => john
    [1] => mary
)

if we use the same array as above.

function map($f = null){
  if($this->d == 1){
    $this->c = array_map($f, $this->c);
    return $this;
  }else
    return $this->apply2d('map', "'$f'"); 
}

Note again the use of apply2d if we have a 2d array, note also the stuff we have to do in order for things to evaluate properly in the context of a string sent to eval(), in this case it’s wrapping $f with ‘ ‘.

Let’s move over to stuff specific to the fluent string, let’s begin with format():

$str = "There are %d monkeys in a %s in %s";
Str::fluent($str)->format('10', 'tree', 'Thailand')->out();

Will output:

There are 10 monkeys in a tree in Thailand
function format(){
  $params 	= func_get_args();
  $call_arr 	  = array_merge(array($this->c), $params);
  $this->c 	 = call_user_func_array('sprintf', $call_arr);
  return $this;
}

Not much to note here, we simply prepare our data what we need to pass to call_user_func_array.

More complex stuff is regular expressions which basically has two shortcuts, match() and matchPos() for simple use cases of matchArb() (Arb as in Arbitrary). Let’s take a look at all of them:

$str = "applepie applemousse cucumber appletart";
Str::fluent($str)->matchPos('/apple\w+/')->out();
Str::fluent($str)->match('/apple\w+/')->sort()->join()->out();
$str = "fruit apple fruit orange color purple fruit pineapple color red";
Str::fluent($str)->matchArb('/(fruit)\s(\w+)/', PREG_SET_ORDER|PREG_OFFSET_CAPTURE, 0, 2)
	->make2d()->flatten()->join(';')->out();

Output:

Array
(
    [0] => 0
    [1] => 9
    [2] => 30
)

applemousse applepie appletart

apple;6;orange;18;pineapple;44
private function match_common($pattern, $flag, $offset, $pos = false){
  preg_match_all($pattern, $this->c, $matches, $flag, $offset);
  if($flag == PREG_OFFSET_CAPTURE) 	foreach($matches[0] as $match) 	$rarr[] = $match[1];
  else if($flag == PREG_PATTERN_ORDER) 	foreach($matches[0] as $match) 	$rarr[] = $match;
  else						        foreach($matches as $match) 	$rarr[] = $match[$pos];
  return new Arr($rarr);
}

function match($pattern, $offset = 0){ 
  return $this->match_common($pattern, PREG_PATTERN_ORDER, $offset); 
}

function matchPos($pattern, $offset = 0){ 
  return $this->match_common($pattern, PREG_OFFSET_CAPTURE, $offset); 
}

function matchArb($pattern, $flag, $offset, $pos){ 
  return $this->match_common($pattern, $flag, $offset, $pos); 
}

So when we use matchArb we pass something else than PREG_OFFSET_CAPTURE or PREG_PATTERN_ORDER. In that case we need to also pass the position/key we want to fetch in the resultant $matches array. In our example it’s 2.

The replace method is easier to implement:

$str = "applepie applemousse cucumber appletart";
Str::fluent($str)->matchReplace('/(apple)(\w+)/', 'orange${2}')->split()->sort()->join()->out();

Output:

cucumber orangemousse orangepie orangetart

Code:

function matchReplace($pattern, $replacement, $limit = -1){
  $this->c 		= preg_replace($pattern, $replacement, $this->c, $limit, $count);
  $this->count 	      = $count;
  return $this;
}

Finally we have apply() which can be used to run a function not implemented in the interface yet (if you are too lazy to implement):

$arr1 = array('apple', 'orange', 'purple');
$arr2 = array('fruit1', 'fruit2', 'color1');
Arr::fluent($arr1)->apply('array_combine', $arr2, '?')->sort()->out();

$str = "apple orange apple guava lime durian";
echo Str::fluent($str)->apply('r', 'substr_count', '?', 'apple');
}

Output:

Array
(
    [fruit1] => apple
    [fruit2] => orange
    [color1] => purple
)

2

The method is defined in FluentExt which both Arr and Str inherits from, note the switch case statement that controls how we return. In the string case above we pass ‘r’ so we return the result instead of a new Str instance. As you probably have realized already the ‘?’ will be replaced with the current container, in this case we don’t need ‘??’, to my knowledge none of the global array and string functions can have ‘?’ as one of their arguments (let me know if they can).

private function fill_place_holder(&$arr){
  foreach($arr as &$a)
    if($a == '?') $a = $this->c;
}

function apply(){
  $params 	= func_get_args();
  $this		->fill_place_holder($params);
  $el 		= array_shift($params);
  if(strlen($el) == 1){
    $ret 	= $el;
    $f 		= array_shift($params);
  }else{
    $f 		= $el;
    $ret	= false;
  }
  switch($ret){
    case 'r':
      return call_user_func_array($f, $params);
      break;
    default:
      $this->c = call_user_func_array($f, $params);
      return $this;
      break;
  }
}

There is one more thing that I will leave up to you to explore how it works, here is the test though:

$arr1 = array('apple', 'orange', 'purple');
$arr2 = array('fruit1', 'fruit2', 'color1');
$arr3 = array($arr1, $arr1);
Arr::fluent2d($arr3)->apply2sub('array_combine', $arr2, '?')
	->normal2d()->out();

Output:

Array
(
    [0] => Array
        (
            [fruit1] => apple
            [fruit2] => orange
            [color1] => purple
        )

    [1] => Array
        (
            [fruit1] => apple
            [fruit2] => orange
            [color1] => purple
        )
)

Download Source

And that’s it, check the source if you are unsure about some things, I’m happy anyway, I finally have my string and array interface. A final note though, the code is optimized for size and programmer friendliness, not the machine, you probably don’t want to use this stuff for arrays with thousands of entries, especially the 2D stuff. Note also that what is not tested in test.php has not been tested, it might or might not work.

  • Digg
  • del.icio.us
  • blogmarks
  • Reddit
  • Simpy
  • StumbleUpon
  • Technorati
  • description
  • Ma.gnolia
  • Slashdot
  • Sphinn
  • Spurl

Related Posts


Tags: , , ,

Posts linking to this article:

[Web] 連結分享
PHP. A Service Locator for PHP5; Hierarchical caching; Fluent Arrays and Strings in PHP; PHP - Caching Pages with Output Buffering; An introduction to friendly URLs in PHP; Maximizing Your MySQL Database; Three Quick Tips To Make Your ...

ProDevTips Blog: Fluent Arrays and Strings in PHP
On the ProDevTips blog, Henrik has written up an extensive tutorial with plenty of code examples on working with something inspired by a few other languages - fluent arrays and strings. I've been working some with jQuery and Ruby lately ...

Subscribe with Google Reader

Viewing 6 Comments

    • ^
    • v
    Hi, it's the creator of pooQuery here... pooQuery is a joke. It actually does work, but it's a joke; I never intended it as something seriously useful, outside showing that you can do stupidly fluent stuff with PHP.

    While I can see fluent interfaces making things simpler in some cases, inventing a whole new "meta-language" like you're doing here is kind of pointless in my opinion. For example, with a quick glance over your fluent code, I had absolutely no idea about what's going on for the most part.

    Isn't it supposed to make things easier, rather than making you learn another, possibly slower execution time-wise, way of doing the same thing?
    • ^
    • v
    You little... :) I read the comments over there but though there was a lot of speculation it never got clear whether it was a joke or not.

    1.) The "meta language" you are referring to is just executable PHP with placeholders, unfortunately a necessary evil, however it shouldn't be too hard to manage, the hardest part is simply keeping track of how some arguments work in some places. Making a manual for something like this wouldn't be too much work for the most part.

    2.) The whole point is that it simply isn't just another way of doing the same thing, it's a better, smaller, clearer way of doing the same thing (as opposed to your joke). Do you really think jQuery and Ruby would be as popular as they are if it wasn't super easy to string executions together in this way?

    Personally I hate having to do pointless assignments just to keep my code readable. To my knowledge only Lisp manages to pull off the whole func(func(arg)) thing in a graceful way, certainly not PHP...
    • ^
    • v
    I can't say about Ruby since I haven't used it, but jQuery does a lot more than wraps JavaScript functionality in chains. Its usefulness in my opinion springs from the power the methods grant you - you could do the same stuff with a non-chained style as well, but they decided to use chains.


    I can agree with the useless assignations part. I guess it's just something you gotta deal with when working in a language that isn't completely object-based. I don't find myself doing that many of those though, as usually I would perform more actions for each assigned variable, conditionally etc. so chains wouldn't work... except pQ style when(...)->not->greaterThan(...)->do(...) chains, which I've actually implemented but they are undocumented in my blogposts.

    Oh, and eval is evil!... Although people have said pooQuery is evil as well ;)
    • ^
    • v
    It's funny, I never really understood the whole evil vs not evil debate regarding various aspects of various environments/languages, arguably PHP has more of these things than any language. Regardless of which ever of these aspects you use the evilness of it will depend on how you use it, usually sloppily or properly. Although I am hard pressed to think of a situation where register globals is a cool thing...
    • ^
    • v
    If you want an unbiased, helpful article on the pros and cons of eval (cons are security and speed), see this: http://blog.joshuaeichorn.com/archives/2005/08/...

    In case of your fluent library I think it's a tradeoff between programming speed and code speed: Once you have learnt and memorized the fluent interface (don't know how long that will take), you will achieve more with less code that looks more elegant. But I fear that "ugly" raw PHP code will be significantly faster than your library. Maybe you can test my assumption with some benchmarks.
    • ^
    • v
    Learning the stuff should be easy easier than using straight PHP anyway, since when you create something like this you're bound to wrap some pretty horrible core names with more intuitive ones.

    Also when using apply() et al with this little library all the arguments will be evaluated in the same order as the core functions. Plus often there are good defaults being passed, with a fluent library I believe you will spend a lot less time checking the PHP function reference which translates into productivity.

    A agree totally with you on the speed though, there are a lot of assignments etc going on back and forth. One simply has to decide when to not use it for fear of bad performance. At the moment I'm creating a back end admin system where the load will not be very big, therefore performance will not be an issue this time.

Trackbacks

close Reblog this comment
blog comments powered by Disqus