Cake 1.2's Set class eats nested arrays for breakfast!
Posted on 24/2/07 by Felix Geisendörfer
Hey folks,
I was just taking a little trip through the CakePHP core code trying to wrap my head around Acl, Model behaviors and all sorts of stuff. While doing so I saw that the core code starts to be using the Set class more and more that was added a while ago. So far this has been a little dark spot for me in the core and from my previous quick looks at the class I've never been quite able to figure out what it's exact purpose was. Until now all I knew was "well it's probably some fancy array manipulation code that is somewhat obfuscated and undocumented". Oh boy, I wish I had spent more time on this earlier. It's probably one of coolest new features in 1.2 and nobody realizes it ; ).
So before starting to drool over it too much ahead of time, let's take a look at a simple example. You have an array of $users as it could have been returned from a findAll call to your User model:
(
0 => array
(
'User' => array
(
'id' => 1
, 'name' => 'Felix'
)
)
, 1 => array
(
'User' => array
(
'id' => 2
, 'name' => 'Bob'
)
)
, 2 => array
(
'User' => array
(
'id' => 3
, 'name' => 'Jim'
)
)
);
What you really want however, is just a simple array containing all user 'name's: array('Felix', 'Bob', 'Jim'). Hmm. Up until today I'd probably have written some code like this to do it:
Simple enough, right? Not any more! Using the new Set class we can achieve the exact same outcome like this:
Doesn't blow you away yet? Well, let's look at another example. Let's say our User model as a hasMany associations to an Item model. Then we would get an array like this:
(
0 => array
(
'User' => array
(
'id' => 1
, 'name' => 'Felix'
, 'Item' => array
(
0 => array
(
'id' => 1
, 'name' => 'Mouse'
)
, 1 => array
(
'id' => 2
, 'name' => 'KeyBoard'
)
)
)
)
, 1 => array
(
'User' => array
(
'id' => 2
, 'name' => 'Bob'
, 'Item' => array
(
0 => array
(
'id' => 3
, 'name' => 'CD'
)
)
)
)
, 2 => array
(
'User' => array
(
'id' => 3
, 'name' => 'Jim'
, 'Item' => array
(
0 => array
(
'id' => 4
, 'name' => 'USB Stick'
)
, 1 => array
(
'id' => 5
, 'name' => 'MP3 Player'
)
, 2 => array
(
'id' => 6
, 'name' => 'Cellphone'
)
)
)
)
);
Now here is how I would have traditionally turned this into a 'User.name' => 'User.items' array:
foreach ($users as $user)
{
foreach ($user['User']['Item'] as $item)
{
$userItems[$user['User']['name']][] = $item['name'];
}
}
But using the new Set class this is still pretty much a simple one-liner (split up in multiple lines so you don't have to scroll):
(
Set::extract($users, '{n}.User.name')
, Set::extract($users, '{n}.User.Item.{n}.name')
);
Both methods will output:
(
[Felix] => Array
(
[0] => Mouse
[1] => KeyBoard
)
[Bob] => Array
(
[0] => CD
)
[Jim] => Array
(
[0] => USB Stick
[1] => MP3 Player
[2] => Cellphone
)
)
"But doesn't it cost more performance to loop through the array twice in the Set example?" I hear some of you cry. Yes it does. And? Have you built your application yet? Does it implement all features you are dreaming of? And most importantly: Do your web stats indicate you are going to have 1 million hits / day soon? If so go back into your code and remove the Set example with the less succinct foreach alternative. If not, listen to Chris Hartjes who's motto for 2007 is Just Build It, Damnit!.
Anyway, here comes my last fun thing to do with Set::extract - parsing an RSS feed for all post titles. For my example I'll use the new XML class in Cake 1.2. Right now Set::extract only supports arrays but hopefully it will either natively support Xml objects at some point, or the Xml class get it's own extract function. For now I've written a little function that can turn an Xml instance into an array that looks like this:
{
$array = array();
foreach ($node->children as $child)
{
if (empty($child->children))
{
$value = $child->value;
}
else
{
$value = xmltoArray($child);
}
$key = $child->name;
if (!isset($array[$key]))
{
$array[$key] = $value;
}
else
{
if (!is_array($array[$key]) || !isset($array[$key][0]))
{
$array[$key] = array($array[$key]);
}
$array[$key][] = $value;
}
}
return $array;
}
So now let's assume we would want to extract all post titles from my feed: http://feeds.feedburner.com/thinkingphp we could leverage the Set class to make our code as succinct as:
$feed = xmltoArray(new XML('http://feeds.feedburner.com/thinkingphp'));
$postTitles = Set::extract($feed, 'rss.channel.item.{n}.title');
Which will give you a $postTitles array like this:
(
[0] => How-to: Use Html 4.01 in CakePHP 1.2
[1] => Looking up foreign key values using Model::displayField
[2] => Bug-fix update for SVN/FTP Deployment Task
[3] => Access your config files rapidly (Win32 only)
[4] => Making error handling for Model::save more beautiful in CakePHP
[5] => Full content RSS feed
[6] => Visual Sorting - Some Javascript fun I had last night
)
Now that's beauty right there and a good way to end this post ; ). Take a look at the Set classes source to find out about some other cool methods it has, but to me this is by far the coolest.
-- Felix Geisendörfer aka the_undefined
You can skip to the end and add a comment.
For your last example it would be much better to use xpath query.. it's as well 2 line code.. but it will be much more efficient.. and anyway I believe you can do much more with xpath than with xmltoarray plus Cake's set class.
As side note: I think all those XML to array methods are made and used by those who are afraid of learning DOM.. and it's shame - DOM is much more powerfull than array - learning it well, opens many doors.
medyk: ?. Maybe I'm missing something but neither do I know of a DOM nor of a XPATH implementation in CakePHP. I'm wrong about that and those exist please let me know as that would be really exiting, but meanwhile be assured that the DOM and I are having a beer together several times a week ... ; ).
Oh and please also keep in mind: xmlToArray as well as the last example are like a 30 minute hack and no production ready xml query language. I was just exited about the extract function and felt like using it on xml, that's all.
Hey Felix,
Great post. Couple other things I wanted to point out about the Set object. In the latest SVN version of Cake 1.2, there's also Set::insert(), Set::check(), and Set::remove(), which all use the same path syntax.
Also, we're looking at getting XPath implemented in the XML object before 1.2 final.
Felix Geisendorfer's Blog: Cake 1.2’s Set class eats nested arrays for breakfast!...
...
[...] Felix Geisendorfer has a great functionality note that CakePHP users might want to check out. It’s related to the Set class and how it handles nested arrays. So far this has been a little dark spot for me in the core and from my previous quick looks at the class I’ve never been quite able to figure out what it’s exact purpose was. Until now all I knew was “well it’s probably some fancy array manipulation code that is somewhat obfuscated and undocumented”. Oh boy, I wish I had spent more time on this earlier. It’s probably one of coolest new features in 1.2 and nobody realizes it. [...]
Felix.. XPath is implemented in php DOM extension which is part of php core.. :)
medyk: Is that a standard extension in terms of being available on most hosts? Didn't know about it so far but might take a look at it at some point.
Damn .. it work's, thank you .. no more nested foreach in a foreach in a foreach .. and so on
Felix it's in php5 and this extension comes with default php configuration.. so unless your host disabled it for any reason (very unlikely) it will be available for you.
medyk: php5 there you go. I use it for client stuff but personally I'm still doing a lot of php4 and if I was to write some generic library it would always have to work in php4 and 5 for me to make sense ; ).
Wow! And I've been fighting arrays all freaking day.
To me php4 doesn't exist.. I write heavy OOP applications.. If there wouldn't be php5 I wouldn't use php at all... and trying to be backwards compatible with php4 is really impossible witm my code... after all I don't think it's worth a hassle - you know we live in 2007 :)
[...] long time - no post, as always - I suck. I intend to make up for it with a screencast on unit testing in the next days, but meanwhile I want to talk about my favourite data type in PHP again: Arrays. For those of you just tuning in, I already wrote about how Cake 1.2’s Set class eats nested arrays for breakfast a while ago and if you haven't read this post yet, go ahead and do it now ; ). Todays post features a brand new Set function called merge that was a side product of me working on a cool new cake class. If you've done a lot of array work in the past, you've probably have come in situations where you wanted to merge to arrays into a new one. Usually that's a no-brainer in PHP by simply using the array_merge function (or the CakePHP wrapper 'am'): PLAIN TEXT PHP: [...]
Thanks for the great tip!
I've found Set::extract() very useful in conjunction with Set::contains() as well.
if( Set:contains(Set::extract::($group['User'], '{n}.id'), a($this->Session->read('User.id'))) ) {
// then User belongs to the Group
}
Is it just me or is there just not enough information out there?
What is "{n}"??????
Arrgh!!!!
JDS: It's not just you. CakePHP 1.2 is not released and some internal class like Set is going to be pretty far down the list when it comes to documentation - that's why I highlight it here ; ).
{n} indicates that any numerical key will be matched and looped through. Sample:
$a = array('User' => array(0 => array('name' => 'Jim'), 1=> array('name' => 'Bob')));
$b = Set::extract($a, 'User.{n}.name');
is the same as:
$b = array('Jim', 'Bob');
Does that explain it?
[...] post interesting, make sure to subscribe to the RSS feed. If you have something to say go ahead and leave a comment. This blog has removed the nofollow attribute and is running the BlogFollow plugin. By leaving yourcomment you get a back link to your site and if your site has a feed, a snippet from the latest entry will appear below your comment. [...]
Felix,
this is very valuable information. Thank you very much for all your work.
May I add parts of it into: http://book.cakephp.org/
(Of course so may want to do it on your own :-))
Unless stated otherwise all info found on this page should be considered public domain, so go for it ; ).
[...] 這篇文章給了很好的示範: Cake 1.2’s Set class eats nested arrays for breakfast Cakephp set class [...]
Looks like you need to repair the title of this page so there's more Google juice.
Aaron: What do you mean?
I liked it so much I implemented it in javascript (using mootools for the map function on data.. replace with your own toolkit)
function extract(data, path) {
var key = path.split('.', 1)[0];
var rest = path.slice(path.indexOf('.') + 1);
if (key == '{n}') {
return data.map(function(item){
return extract(item, rest);
});
} else {
if (rest == key) {
return data[key];
}
else {
return extract(data[key], rest);
}
}
}
johnbenclark1: Nice! Now do it for jQuery. And while you're at it try to see if you can get the new XPath stuff going as well ; P.
felix: i just did a simple test with jquery and it worked quite well :)
Why not use Set::combine
instead of
$userItems = array_combine(Set::extract(..,Set::extract
Jörg: Why use super glue if duck tape will do? : )
Felix: thanks for this!
As someone new to Cake & PHP I came to this post looking for help iterating through nested arrays (I was getting lost in nested foreach loops!) but I like this solution much better. Keep finding myself on debuggable.com - great stuff!
If you're looking for an elegant way of making a find('list') that shows two or more fields in the select box, then here's how to do it:
$membersArray = $this->Member->find('all');
$membersList = Set::combine(
$membersArray,
'{n}.Member.MemberID',
array(
'{0} {1}',
'{n}.Member.MemberFirstName',
'{n}.Member.MemberLastName'
)
);
You can even use this to put related model data into the select box. Very very useful little trick :-)
Felix! Thanks a lot for this. Its 8 minutes to go before my working day is over, and I've just found your post. Thanks for saving the day!!
Thanks for the amazing write up on the Set class. I have been ignoring it for too long finally going to take a stab at it rather than use foreach loops.
Cheers
This post is too old. We do not allow comments here anymore in order to fight spam. If you have real feedback or questions for the post, please contact us.
Long live the Set::extract() method, something that I should be working on to port for CakePHP 1.1. It should work out-of-the-box but still need to test it.