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.
Download code
Download code
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.
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 1Download code
$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:
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
Question
1 Call me dense but...
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]
Comment
2 instruction 4 newbie (like me!!!)
and put the class MultipleDisplayFieldsBehavior code inside
comment line :
$model->varDump($queryData);
and everything will works!!!
Comment
3 oops
The line:
$model->varDump($queryData);
was used for debugging...
forgot to take it out -_-;
fixed.
Bug
4 $results in afterFind() can contain differently formatted arrays
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
Comment
5 ...
http://cakeexplorer.wordpress.com/2007/09/10/autofield-behavior-or-how-to-add-additional-columns-to-your-models/
Comment
6 wow
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 :)
Comment
7 possible extension
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;
}
}
?>
Comment
8 Re: $results in afterFind() can contain differently formatted arrays
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...
Comment
9 foreach
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.
Question
10 Can't get the joins to work
Could anyone take a look at my code? :)
http://bin.cakephp.org/view/2110425695
Thanks ! (and sorry, I did not translate these french vars!)
Comment
11 :)
Comment
12 enhancement
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'
));
Comment
13 by the way
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
Comment
14 Found what wasn't working with Cosmin 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!
Comment
15 extension problem
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 :)
Comment
16 Possible fixes
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 ;)
Comment
17 Not to be dense, but isn't this overkill?
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
Comment
18 Re: [..] isnt this overkill?
Comment
19 It's funny that you replied to this
Going to have to try yours out and compare.
Comment
20 Re: Barry
Barry, please look me up in the irc channel so we can chat. My nick there is "alkemann"
Comment
21 Thanks!
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.
Comment
22 How to obtain the same with 4 lines of code
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");
}
?>
Comment
23 Works Great
Comment
24 Very nice.
Comment
25 Comment 7 works like a charm!
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.
Comment
26 Comment 22 works great
Comment
27 Thank you!
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)'
));
=======