Multiple Display Field

By Daniel Albert (resshin)
The behavior allows us to use multiple display field (such as "first_name" and "last_name") as a display field for generating list from a model.
This is really one of common issues: using multiple fields as display field when using the find('list') function.

Strangely enough, there's only little hint out there about how to do it "elegantly" in CakePHP. tclineks already make a great article in http://bin.cakephp.org/saved/19252#modify, but the code just won't works. I think it's happened because CakePHP now handles find function differently than the previous versions.

So then I wrote the code below to help anyone who need to use multiple fields in their display field.
Note: The afterFind function is taken without any change from tclineks' bin.

Using it is as simple as this:

  1. In your model, define that the model is acting as MultipleDisplayFieldBehavior. For example, see Figure 1.
  2. To generate a list, simply use the find('list') function. For example, see Figure 2.

Note: you must define the "displayField" property. This field (which can be a virtual/non-existent field) will then holds the concatenation of display fields that you defined.

Download code
class User extends AppModel {
    var $name = "User";
    var $displayField = "full_name";
    var $actsAs = array('MultipleDisplayFields' => array(
        'fields' => array('first_name', 'last_name'),
        'pattern' => '%s %s'
    ));
}
Figure 1

Download code
$userList = $this->User->find("list");
Figure 2

The MultipleDisplayFieldsBehavior class is defined as below.
Save it as "multiple_display_fields.php" and put the file inside "app/models/behaviors/" folder.

Model Class:

Download code <?php 
class MultipleDisplayFieldsBehavior extends ModelBehavior {
    var 
$config = array();
    
    function 
setup(&$model$config = array()) {
        
$default = array(
            
'fields' => array($model->name.'.first_name'$model->name.'.last_name'),
            
'pattern' => '%s %s'
        
); 
        
$this->config[$model->name] = $default;
        
        if(isset(
$config['fields'])) {
            
$this->config[$model->name]['fields'] = $config['fields'];
        }
        if(isset(
$config['pattern'])) {
            
$this->config[$model->name]['pattern'] = $config['pattern'];
        }
    }
    
    function 
afterFind(&$model$results) {
        
// if displayFields is set, attempt to populate
        
foreach ($results as $key => $val) {
            
$displayFieldValues = array();

            if (isset(
$val[$model->name])) {
                
// ensure all fields are present
                
$fields_present true;
                foreach (
$this->config[$model->name]['fields'] as $field) {
                    if (
array_key_exists($field,$val[$model->name])) {
                        
$fields_present $fields_present && true;
                        
$displayFieldValues[] = $val[$model->name][$field]; // capture field values
                    
} else {
                        
$fields_present false;
                        break;
                    }
                }

                
// if all fields are present then set displayField based on $displayFieldValues and displayFieldPattern
                
if ($fields_present) {
                    
$params array_merge(array($this->config[$model->name]['pattern']), $displayFieldValues);
                    
$results[$key][$model->name][$model->displayField] = call_user_func_array('sprintf'$params );
                }
            }
        }
        return 
$results;
    }


    function 
beforeFind(&$model, &$queryData) {
        if(isset(
$queryData["list"])) {
            
$queryData['fields'] = array();
            
            
//substr is used to get rid of "{n}" fields' prefix...
            
array_push($queryData['fields'], substr($queryData['list']['keyPath'], 4));
            foreach(
$this->config[$model->name]['fields'] as $field) {
                
array_push($queryData['fields'], $model->name.".".$field);
            }
        }
        
//$model->varDump($queryData);
        
return $queryData;
    }
}
?>

Hope it helps you... :)

 

Comments 803

CakePHP Team Comments Author Comments
 

Question

1 Call me dense but...

Don't you have to reference 'full_name' somewhere else? Somewhere to tell the system to use the new behavior for that virtual field?

Maybe unrelated but I'm getting an error when I use this: (Only shows when debug is set to 1 or greater.)


Warning (512): SQL Error: 1064: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'varDump' at line 1 [CORE/cake/libs/model/datasources/dbo_source.php, line 512]
Posted Oct 23, 2008 by Dan Berlyoung
 

Comment

2 instruction 4 newbie (like me!!!)

under models/behaviors create a file named multiple_display_fields.php
and put the class MultipleDisplayFieldsBehavior code inside

comment line :
$model->varDump($queryData);
and everything will works!!!
Posted Nov 6, 2008 by Nico
 

Comment

3 oops

ah, you're right..

The line:
$model->varDump($queryData);

was used for debugging...
forgot to take it out -_-;

fixed.
Posted Nov 9, 2008 by Daniel Albert
 

Bug

4 $results in afterFind() can contain differently formatted arrays

Hi Daniel,
afterFind() gets different sort of results according to what context it is called in. Your method doesn't seem to check for the less obvious forms which will result in notice level errors and malfunction.

Here I describe two other $results formats:
http://blog.pepa.info/php-html-css/cakephp/what-you-can-expect-as-results-in-afterfind/
I hope this helps.
Petr
Posted Nov 16, 2008 by Petr 'PePa' Pavel
 

Comment

5 ...

Posted Nov 23, 2008 by Yevgeny Tomenko
 

Comment

6 wow

Hi Daniel,
afterFind() gets different sort of results according to what context it is called in. Your method doesn't seem to check for the less obvious forms which will result in notice level errors and malfunction.
...
Petr

Hi Petr,
I've read your blog, and now I'm aware of those result's schemes, thanks to you :)

And fortunately, the method afterFind() above has already handled the other scheme's. Notice the if statement:
if (isset($val[$model->name]))
It'll ignore the 1st and 2nd scheme :)

Posted Dec 5, 2008 by Daniel Albert
 

Comment

7 possible extension

I managed to extend this by adding fields from $belongsTo models. You just need to make sure that you have at least "recursive" => 0 in the model and in the find( "list" ) calls in order to get the JOINs this needs.
Model code:

var $actsAs = array(    'MultipleDisplayFields' => array (    'fields' => array (    'Collection.name',
                                                                                    'Designer.name',
                                                                                    'CollectionType.name',
                                                                                    'CollectionTarget.name',
                                                                                    'CollectionCategory.name',
                                                                                    'Season.name',
                                                                                    'Season.syear'
                                                                                     ),
                                                                'pattern' => '%s %s %s %s %s %s %d'
                                                                )
                            );

Behaviour code:

<?php
class MultipleDisplayFieldsBehavior extends ModelBehavior {
    var 
$config = array();

    function 
setup(&$model$config = array()) {
        
$default = array(
            
'fields' => array($model->name => array( 'first_name''last_name' ) ),
            
'pattern' => '%s %s'
        
);
        
$this->config[$model->name] = $default;

        if(isset(
$config['fields'])) {
            
$myFields = array();
            foreach (
$config['fields'] as $key => $val) {
                
$modelField explode"."$val );
                if( empty( 
$myFields[$modelField[0]] ) ) $myFields[$modelField[0]] = array();
                
$myFields[$modelField[0]][] = $modelField[1];
            }
            
$this->config[$model->name]['fields'] = $myFields;
        }
        if(isset(
$config['pattern'])) {
            
$this->config[$model->name]['pattern'] = $config['pattern'];
        }
    }

    function 
afterFind ( &$model$results ) {
        
// if displayFields is set, attempt to populate
        
foreach ($results as $key=>$result) {
            
$displayFieldValues = array();
            
$fields_present true;

            foreach( 
$this->config[$model->name]['fields'] as $mName => $mFields ) {
                if (isset(
$result[$mName])) {
                    foreach( 
$mFields as $mField ) {
                        if( 
array_key_exists$mField$result[$mName] ) ) {
                            
$fields_present $fields_present && true;
                            
$displayFieldValues[] = $result[$mName][$mField];
                        } else {
                            
$fields_present false;
                        }
                    }
                } else {
                    
$fields_present false;
                }
            }

            if (
$fields_present) {
                
$params array_merge(array($this->config[$model->name]['pattern']), $displayFieldValues);
                
$results[$key][$model->name][$model->displayField] = call_user_func_array('sprintf'$params );
            }
        }
        return 
$results;
    }


    function 
beforeFind(&$model, &$queryData) {
        if(isset(
$queryData["list"])) {
            
$queryData['fields'] = array();
            
//substr is used to get rid of "{n}" fields' prefix...
            
array_push($queryData['fields'], substr($queryData['list']['keyPath'], 4));
            foreach (
$this->config[$model->name]['fields'] as $mName => $mFields) {
                foreach( 
$mFields as $mField ) {
                    
array_push$queryData['fields'], $mName"."$mField );
                }
            }
        }
//        debug($queryData);
        
return $queryData;
    }
}
?>
Posted Dec 8, 2008 by Cosmin Cimpoi
 

Comment

8 Re: $results in afterFind() can contain differently formatted arrays

...
And fortunately, the method afterFind() above has already handled the other scheme's. Notice the if statement:
if (isset($val[$model->name]))
It'll ignore the 1st and 2nd scheme :)

I may be wrong but I think that you need to use empty() or is_array() even earlier because the line with
foreach ($results as $key => $val) { would trigger Notice level error for case 2 (a model association).

Also I think that it's better to use $this->alias instead of $this->name because "name" wouldn't work properly for associated models that have been given an alias.

I hope that I'm not completely wrong :-)
Petr

P.S.: Oh how I miss "preview" in this forum...
Posted Dec 8, 2008 by Petr 'PePa' Pavel
 

Comment

9 foreach

I may be wrong but I think that you need to use empty() or is_array() even earlier because the line with
foreach ($results as $key => $val) { would trigger Notice level error for case 2 (a model association).

...

Umm... all these three schemes are arrays right? So I think it won't be a problem to try to iterate over them using foreach :)

and I never use $alias in my model, so I don't really know how it works XD... I think I'll agree with you to use $this->alias instead of $this->name.

And a great job there at Comment#7: possible extension. But let's keep both of simple version (mine) and extended version (yours), so, for those who want to learn how this behavior works, they can learn the simple first, then (if they need to) they can learn the extended later.
Posted Dec 10, 2008 by Daniel Albert
 

Question

10 Can't get the joins to work

Good job Cosmin Cimpoi, but I can't get it to work :( I can't get the tables to join.

Could anyone take a look at my code? :)

http://bin.cakephp.org/view/2110425695
Thanks ! (and sorry, I did not translate these french vars!)
Posted Dec 26, 2008 by Jimmy Bourassa
 

Comment

11 :)

Basic but useful idea
Posted Jan 1, 2009 by Giuliano Barberi
 

Comment

12 enhancement

i did not manage to get the extension of Cosmin to work...
but i managed to implement another enhancement:


    function setup(&$model, $config = array()) {
        $default = array(
            'fields' => array($model->name.'.first_name', $model->name.'.last_name'),
            'defaults' => array(),
            'pattern' => '%s %s'
        ); 
        $this->config[$model->name] = $default;
        
        if(isset($config['fields'])) {
            $this->config[$model->name]['fields'] = $config['fields'];
        }
        if(isset($config['pattern'])) {
            $this->config[$model->name]['pattern'] = $config['pattern'];
        }
        # MOD 2009-01-06 ms
        if(isset($config['defaults'])) {
            $this->config[$model->name]['defaults'] = $config['defaults'];
        }
    }
    
    function afterFind(&$model, $results) {
        // if displayFields is set, attempt to populate
        foreach ($results as $key => $val) {
            $displayFieldValues = array();

            if (isset($val[$model->name])) {
                // ensure all fields are present
                $fields_present = true;
                foreach ($this->config[$model->name]['fields'] as $field) {
                    if (array_key_exists($field,$val[$model->name])) {
                        $fields_present = $fields_present && true;
                        $displayFieldValues[] = $val[$model->name][$field]; // capture field values
                    } else {
                        $fields_present = false;
                        break;
                    }
                }

                // if all fields are present then set displayField based on $displayFieldValues and displayFieldPattern
                if ($fields_present) {
                    $params = array_merge(array($this->config[$model->name]['pattern']), $displayFieldValues);
                    
                    # MOD 2009-01-06 ms
                    $string = '';
                    if (!empty($this->config[$model->name]['defaults'])) {
                        foreach ($params as $k => $v) {
                            if ($k > 0) {
                                if (isset($this->config[$model->name]['defaults'][$k-1]) && empty($v)) {
                                    $params[$k]=$this->config[$model->name]['defaults'][$k-1];
                                    $string = $params[$k];
                                } elseif (!empty($string)) {    // use the previous string if available (e.g. if only one value is given for all)
                                    $params[$k] = $string;                        
                                }
                            }
                        }
                    }
                    //pr ($params);
                    
                    $results[$key][$model->name][$model->displayField] = call_user_func_array('sprintf', $params );
                }
            }
        }
        return $results;
    }
(only the 2 altered functions copied)

you are now able to submit default values, if the specific fields may contain empty values.
i had used | as separator, but this did not look very nice, if the middle field was missing

this would be the new syntax:

var $actsAs = array('MultipleDisplayFields' => array(
        'fields' => array('zip_code', 'city', 'street'),
        'defaults' => array('---','---','----'),
        'pattern' => '%s | %s | %s'
    ));

Note that you could only specify one default value for all fields as well:

var $actsAs = array('MultipleDisplayFields' => array(
        'fields' => array('zip_code', 'city', 'street'),
        'defaults' => array('---'),
        'pattern' => '%s | %s | %s'
    ));
Posted Jan 6, 2009 by Mark
 

Comment

13 by the way

did anyone manage to get it to work with pagination?
on edit/add actions it works like a charm
but using it in index view (with the paginator), the multipleDisplayField behaviour does not seem to be triggered

mark
Posted Jan 6, 2009 by Mark
 

Comment

14 Found what wasn't working with Cosmin Cimpoi's extension

I found what wasnt working with Cimpoi's extension.

The problem is that $queryData in beforeFind has recursive set to -1 (Not sure why, but whatever).

So the quick fix would be to simply add $queryData['recursive']=1; in the beforeFind method of this behavior.

Hope it helps!
Posted Jan 7, 2009 by Jimmy Bourassa
 

Comment

15 extension problem

great :) thx
works like a charm

only that it now does not only retrieve the user list (for the dropdown select), but also all (and these are 12 db tables with all hasMany information on them (> 300 entries sometimes) due to recursive=1, i guess :)
if there is a way to get rid of some those relations (containable behaviour?) that would be really nice

like the original behavior it (of course^^) can still retrieve the pure users etc. but only if i leave out the hard coded recursive=1.
but than again the left joined lists won't work.

maybe there is a way to switch between the higher and lower recursive level depending on what is needed.

e.g. if you need only users and user_addresses to get a dropdown menu,
what good does it do to have 10 other db entries connected to the user as well :)

i am interested in any ideas on that topic

i guess that a good solution could be (besides the containable) to get the current model recursive value (as it could be set in the controller method of that specific action).
tested that, but seems to be totally ignored :)
Posted Jan 8, 2009 by Mark
 

Comment

16 Possible fixes

Well, I don't have any clear answer on that, but I might have some clues to help you out.

First of all, you could try with recursive 0, see whats joined and whats not.

You could also append some code to the behavior to do specific actions for different models list, like unbinding the models (http://api.cakephp.org/class_model.html#0b969d5264205cd3a425980dd53e9658), or removing the joins from $queryData.

Again, I know these solutions are not optimal, but it's all I can think of on the top of my head ;)

great :) thx
works like a charm

only that it now does not only retrieve the user list (for the dropdown select), but also all (and these are 12 db tables with all hasMany information on them (> 300 entries sometimes) due to recursive=1, i guess :)
if there is a way to get rid of some those relations (containable behaviour?) that would be really nice

like the original behavior it (of course^^) can still retrieve the pure users etc. but only if i leave out the hard coded recursive=1.
but than again the left joined lists won't work.

maybe there is a way to switch between the higher and lower recursive level depending on what is needed.

e.g. if you need only users and user_addresses to get a dropdown menu,
what good does it do to have 10 other db entries connected to the user as well :)

i am interested in any ideas on that topic

i guess that a good solution could be (besides the containable) to get the current model recursive value (as it could be set in the controller method of that specific action).
tested that, but seems to be totally ignored :)
Posted Jan 10, 2009 by Jimmy Bourassa
 

Comment

17 Not to be dense, but isn't this overkill?

CakePHP makes this a 1 line of code process thanks to the combine function in the set class. I'm fairly certain that displayField is intended for the common case of list creation where it really is just a single field and id.

Rather than using the behavior, just use Set::combine(). Here's an example.

Say you have a users table and a user_types tables. You might like to list the user's first name, last name and user type all together in your select box.

Here's our sample tables:

users table:
-id
-first_name
-last_name
-user_type_id

user_types table:
-id
-name

Now, in our controller assuming the belongsTo association is setup we'll do:


$users = $this->User->find('all');

Which produces:


Array (
  [0] => Array(
     [User] => Array(
        [id] => 23,
        [first_name] => 'John',
        [last_name] => 'Doe',
        [user_type_id] => 1
     ),
     [UserType] => array(
        [id] => 1,
        [name] => 'Cool User'
     )
  ),
  .....etc
)

Now all the code that we really need is:


$users = $this->User->find('all');
$users = Set::combine(
               $users,
               '{n}.User.id',
               array(
                 '{0}, {1} ({2})',
                 '{n}.User.last_name',
                 '{n}.User.first_name',
                 '{n}.UserType.name'
               )
        );
$this->set('users',$users);

That will produce a list like this:


Array(
  [23] => 'Doe, John (Cool User)'
)

The Set class is probably heavily under-utilized because it's not heavily featured in the documentation, however, Set::format, Set::extract, and Set::combine are the three exceptionally handy functions that will make your life easier.

There's a whole lot more that you can do with combine too.

[url=http://book.cakephp.org/view/662/combine]http://book.cakephp.org/view/662/combine
Posted Jan 18, 2009 by Barry
 

Comment

18 Re: [..] isnt this overkill?

Yes it is Barry. And if you have a desire to have the logic in the model, you could simply implement afterFind or overwrite the 'list' part of find. or a custom list() function.
Posted Jan 18, 2009 by Alexander Morland
 

Comment

19 It's funny that you replied to this

I just found my way over to your Google Code page and was looking at Loggable, Multilingual, and Revisions because I wrote behaviors for all 3 of those myself (and integrated them) and was looking for inspiration to improve them.

Going to have to try yours out and compare.
Posted Jan 18, 2009 by Barry
 

Comment

20 Re: Barry

I appologize for the thread hijack.

Barry, please look me up in the irc channel so we can chat. My nick there is "alkemann"
Posted Jan 18, 2009 by Alexander Morland
 

Comment

21 Thanks!

CakePHP makes this a 1 line of code process thanks to the combine function in the set class. I'm fairly certain that displayField is intended for the common case of list creation where it really is just a single field and id.

Tried using this behavior and it broke some out-of-the-box components in Cake. Barry's method works perfectly. Thanks for the tip, Barry.
Posted Mar 20, 2009 by Nathan Sweet
 

Comment

22 How to obtain the same with 4 lines of code

Just put this in AppModel

Model Class:

<?php 
class AppModel extends Model {
    function 
find($conditions NULL$fields = array (), $order NULL$recursive NULL) {
        if (
$conditions == 'list' && is_array($this->displayField))
            return 
Set::combine($this->find('all'$fields$order$recursive), "{n}.{$this->name}.{$this->primaryKey}"$this->displayField);
        else
            return 
parent::find($conditions$fields$order$recursive);
    }
}
?>

Then you can use


Model Class:

<?php 
class User extends AppModel
{
    var 
$displayField = array("%s %s""{n}.User.name""{n}.User.secondname");
}
?>
}
Posted Apr 2, 2009 by Arialdo Martini
 

Comment

23 Works Great

Thanks.
Posted Apr 12, 2009 by Trevor Gau
 

Comment

24 Very nice.

Even better is the default case, as I suspect that'll be what most people are going after.
Posted Apr 14, 2009 by Jacob Miller
 

Comment

25 Comment 7 works like a charm!

So far comment 7 (extended version of the code in this post) has worked the best for me.
Just in case it doesn't work for someone...as mentioned by Cosmin Cimpoi we need to ensure that the recursive is set to at least 0...but, even though I forced $recursive=1 everywhere, for some reason it just assumed $recurive=-1...so this is what I did instead.


$this->Contact->Address->find('list', array('recursive' => 1));    

It took me around half an hour to figure this out...I'm sure its silly, still my 2 cents.
Posted Jun 23, 2009 by Gayatri
 

Comment

26 Comment 22 works great

Comment 22 worked great for me - thanks!
Posted Jan 9, 2010 by Tomba
 

Comment

27 Thank you!

Hello,
i tryed some hours to add multiple fields to an 'list'-output but it doesn't worked. With your article, it worked in about 2 minutes :-)
So thanks a lot!!!!

Addition:
If someone want to change the kind of output (e.g.: add brackets or something else, change the pattern field in the model.
example:
=======
var $actsAs = array('MultipleDisplayFields' => array(
'fields' => array('your_first_field', 'your_second_field'),
'pattern' => '%s (%s)'
));
=======
Posted Jan 27, 2010 by Sebastian