Multiple Display Field
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:
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.
The MultipleDisplayFieldsBehavior class is defined as below.
Save it as "multiple_display_fields.php" and put the file inside "app/models/behaviors/" folder.
Hope it helps you... :)
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:
- In your model, define that the model is acting as MultipleDisplayFieldBehavior. For example, see Figure 1.
- 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 2The 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... :)








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');
Then changed "{n}.User.name", "{n}.User.secondname" to "{n}.User.first_name", "{n}.User.last_name"
still not working.
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)'
));
=======
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.
Just put this in AppModel
Model Class:
<?phpclass 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");
}
?>
Going to have to try yours out and compare.
Barry, please look me up in the irc channel so we can chat. My nick there is "alkemann"
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
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.
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 :)
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 ;)
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!
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
but i managed to implement another enhancement:
(only the 2 altered functions copied)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;
}
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'
));
Could anyone take a look at my code? :)
http://bin.cakephp.org/view/2110425695
Thanks ! (and sorry, I did not translate these french vars!)
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;
}
}
?>
http://cakeexplorer.wordpress.com/2007/09/10/autofield-behavior-or-how-to-add-additional-columns-to-your-models/
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
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 :)
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...
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.
The line:
$model->varDump($queryData);
was used for debugging...
forgot to take it out -_-;
fixed.
and put the class MultipleDisplayFieldsBehavior code inside
comment line :
$model->varDump($queryData);
and everything will works!!!
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