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