debuggable

 
Contact Us
 

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:

$users = array
(
    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:

$userNames = array();
foreach ($users as $user)
{
    $userNames[] = $user['User']['name'];
}

Simple enough, right? Not any more! Using the new Set class we can achieve the exact same outcome like this:

$userNames = Set::extract($users, '{n}.User.name');

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:

$users = array
(
    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:

$userItems = 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):

$userItems = array_combine
(
    Set::extract($users, '{n}.User.name')
    , Set::extract($users, '{n}.User.Item.{n}.name')
);

Both methods will output:

Array
(
    [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:

function xmltoArray($node)
{
    $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:

uses('Xml');

$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:

Array
(
    [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

 
&nsbp;

You can skip to the end and add a comment.

Mariano Iglesias said on Feb 24, 2007:

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.

medyk said on Feb 24, 2007:

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.

Felix Geisendörfer said on Feb 25, 2007:

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.

Nate said on Feb 26, 2007:

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.

PHPDeveloper.org said on Feb 28, 2007:

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

medyk said on Mar 01, 2007:

Felix.. XPath is implemented in php DOM extension which is part of php core.. :)

Felix Geisendörfer said on Mar 01, 2007:

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.

Nils Hitze said on Mar 01, 2007:

Damn .. it work's, thank you .. no more nested foreach in a foreach in a foreach .. and so on

medyk said on Mar 01, 2007:

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.

Felix Geisendörfer said on Mar 01, 2007:

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

Benjamin Hirsch said on Mar 07, 2007:

Wow! And I've been fighting arrays all freaking day.

medyk said on Mar 08, 2007:

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: [...]

Seph Lietz said on Apr 13, 2007:

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

}


JDS  said on Apr 24, 2007:

Is it just me or is there just not enough information out there?

What is "{n}"??????

Arrgh!!!!

Felix Geisendörfer said on Apr 24, 2007:

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?

CakePHP Tutorials :: PseudoCoder.com said on Feb 11, 2008:

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

Alex  said on Feb 14, 2008:

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 :-))

Felix Geisendörfer said on Feb 18, 2008:

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

Aaron  said on May 01, 2008:

Looks like you need to repair the title of this page so there's more Google juice.

Felix Geisendörfer said on May 02, 2008:

Aaron: What do you mean?

johnbenclark  said on Jul 02, 2008:

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

}

}

}

Felix Geisendörfer said on Jul 03, 2008:

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.

Oktay Acikalin said on Sep 29, 2008:

felix: i just did a simple test with jquery and it worked quite well :)

Jörg Peters  said on Nov 29, 2008:

Why not use Set::combine
instead of

$userItems = array_combine(Set::extract(..,Set::extract

Felix Geisendörfer said on Nov 29, 2008:

Jörg: Why use super glue if duck tape will do? : )

pragnatek said on Dec 04, 2008:

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!

Ben  said on Feb 27, 2009:

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 :-)

Chris said on Jul 29, 2009:

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

Andrew said on Nov 24, 2009:

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.