Multiple Display Field

This article is also available in the following languages:
By 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.


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


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

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

  • Posted 07/06/10 03:07:35 AM
    ok guys here is another solution which uses one afterFind in the user model. To make it newbie safe I paste both models and controllers as well as the SQL for the tables.

    In the afterFind function you see 2 x if(isset ... there you handle one time the first_name and one time the last_name.

    Have fun.
    Frank

    ***************************
    user model :

    class User extends AppModel {
    var $name = 'User';
    var $displayField = 'full_name';


    function afterFind($results) {
    foreach ($results as $key => $val) {

    if(isset($val['User'])){
    $results[$key]['User']['full_name']= $val['User']['first_name']. " ". $val['User']['last_name'];
    }

    if(isset($val['ReceivingUser'])){ //fh this checks if the receiving user exists in the array
    $results[$key]['ReceivingUser']['full_name']= $val['ReceivingUser']['first_name']. " ". $val['ReceivingUser']['last_name'];
    }
    return $results;
    }
    }

    var $hasMany = array(
    'MessageSent' => array(
    'className' => 'Message',
    'foreignKey' => 'sending_user_id'
    ),
    'MessageReceived' => array(
    'className' => 'Message',
    'foreignKey' => 'receiving_user_id'
    )
    );
    }
    ?>


    ***************************
    Message model :

    class Message extends AppModel {
    var $name = 'Message';
    var $displayField = 'message';

    var $belongsTo = array(
    'User' => array(
    'className' => 'User',
    'foreignKey' => 'sending_user_id',
    ),
    'ReceivingUser' => array( //fh 'User' Part of 'ReceivingUser' here is going to be the label of the auto-buttons (left sidebuttons from scaffolds)
    'className' => 'User',
    'foreignKey' => 'receiving_user_id',
    )
    );
    }
    ?>
    ***************************
    User controller :
    class UsersController extends AppController {

    var $name = 'Users';
    var $scaffold;

    }
    ?>

    ***************************
    Message controller :

    class MessagesController extends AppController {

    var $name = 'Messages';
    var $scaffold;

    }
    ?>
    ***************************
    Table SQL :


    CREATE TABLE IF NOT EXISTS `messages` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `sending_user_id` int(11) NOT NULL,
    `receiving_user_id` int(11) NOT NULL,
    `message` varchar(255) NOT NULL,
    PRIMARY KEY (`id`)
    ) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=5 ;

    --
    -- Dumping data for table `messages`
    --

    INSERT INTO `messages` (`id`, `sending_user_id`, `receiving_user_id`, `message`) VALUES
    (1, 1, 2, 'comin'),
    (2, 2, 1, 'out'),
    (3, 1, 1, 'still'),
    (4, 2, 2, 'boy');

    -- --------------------------------------------------------

    --
    -- Table structure for table `users`
    --

    CREATE TABLE IF NOT EXISTS `users` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `first_name` varchar(64) NOT NULL,
    `last_name` varchar(64) NOT NULL,
    `character` varchar(64) NOT NULL,
    PRIMARY KEY (`id`)
    ) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=3 ;

    --
    -- Dumping data for table `users`
    --

    INSERT INTO `users` (`id`, `first_name`, `last_name`, `character`) VALUES
    (1, 'Alice', 'Axe', 'forward'),
    (2, 'Bob', 'Flinch', 'careful');





  • Posted 07/03/10 05:53:03 AM
    Inserted the AppModel code in AppModel and user model code in user model ... user names now blank in index.ctp table.

    Then changed "{n}.User.name", "{n}.User.secondname" to "{n}.User.first_name", "{n}.User.last_name"

    still not working.
  • Posted 01/27/10 02:29:05 AM
    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 01/09/10 08:02:20 AM
    Comment 22 worked great for me - thanks!
  • Posted 06/23/09 11:15:53 PM
    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 04/14/09 01:01:52 AM
    Even better is the default case, as I suspect that'll be what most people are going after.
  • Posted 04/12/09 03:58:12 PM
    Thanks.
  • Posted 04/02/09 07:16:20 AM

    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 01/18/09 05:34:32 AM
    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 01/18/09 08:22:11 AM
      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 01/18/09 08:31:38 AM
        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 01/18/09 02:55:32 AM
    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 03/20/09 03:15:08 PM
      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 01/08/09 04:55:03 PM
    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 01/10/09 10:21:24 AM
      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 01/07/09 07:21:04 PM
    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 01/06/09 02:48:31 PM
    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 01/06/09 02:41:41 PM
    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 01/01/09 09:01:07 PM
    Basic but useful idea
  • Posted 12/26/08 03:34:31 PM
    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 12/08/08 01:40:25 PM
    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 11/16/08 10:57:44 AM
    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 12/05/08 04:19:46 AM
      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 12/08/08 02:08:29 PM
        ...
        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 12/10/08 11:31:18 PM
          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 11/09/08 09:12:28 PM
    ah, you're right..

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

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

    fixed.
  • Posted 11/06/08 04:11:58 AM
    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 10/23/08 10:06:11 AM
    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]

Comments are closed for articles over a year old