So afterFind works fine and dandy when I'm within the corresponding model/controller. However, when calling an associated model, the data sent to the afterFind callback is formatted differently. This causes afterFind to crap out because it can't find the same array indexes it did when just working within the original model/controller.
Anyone know why, or what a fix might be?
$primary may not be very helpful; I've found that it is always false when using ContainableBehaviour beyond the first depth:
$this->Model->find('first', array(
'contain' => array(
'SecondaryModel' => array(
'TertiaryModel',
),
),
));
If you're setting a value based on a related model, you can check for its presence to deal with either structure like this:
function afterFind($results, $primary) {
if (isset($results['TertiaryModel'])) {
$results['secondary_model_field'] = 'value';
}
else {
foreach ($results as &$result) {
if (is_array($result) && isset($result['TertiaryModel'])) {
$result[$this->alias]['secondary_model_field'] = 'value';
}
} unset($result);
}
return $results;
}
Alternately you may be able to just check for the location of a field on the model itself. If the field doesn't exist at the top level, you will need to iterate over the set of results.
This is what the second parameter to afterFind callback is for.
$primary tells if you if the find was called from this model directly (true), or if it was called by an associated model (false).
A note from the book:
The $primary parameter indicates whether or not the current model was
the model that the query originated on or whether or not this model
was queried as an association. If a model is queried as an
association the format of $results can differ;
Code expecting $primary to be true will probably get a "Cannot use
string offset as an array" fatal error from PHP if a recursive find
is used.
So you may need different processing logic depending on the value of $primary
It appears that Cake 2.6 includes a fix for this, ensuring that all $results arrays are consistently formatted. I've done a little testing with the RC release and it does seem to work, with arrays all being passed in the format {n}.ModelName.data.
The fix is enabled by default, but you can also revert to the legacy behaviour if need be. Just add the following to your model (or AppModel) definition:
public $useConsistentAfterFind = false;
Related
I can't believe I don't remember how to do this, but how do I specify in my model the default fields that are returned with the find() methods? I can't find on google how to do this, or at least I don't know the wording to search for.
What you will most likely need to do, is to check if the fields key exists in a beforeFind() method in your model.
If the fields key is not set, you can set it to $this->fields in your native models, and create the beforeFind() in your AppModel, then you can instruct that method to use the $this->fields array from your models.
UPDATE
// AppModel.php
parent function beforeFind($queryData = array()) {
if (empty($queryData['fields']) && !empty($this->fields)) {
$queryData['fields'] = $this->fields;
}
return $queryData;
}
// And in your Model:
public $fields = array(
'Alert.id'
);
This will check for existence of a fields array, and will then check for existence of a $this->fields property. If it does exist, it will apply it to the query data and return that modified query data to the beforeFind() - this will change your find.
Adjust it to fit your needs, and good luck!
I've implemented CakePHP's Translate Behavior, and all went fairly smooth, but I've now noticed that my translated data from the i18n table doesn't exist when I contain() a model that is supposed to be translated.
Does the Translate Behavior not work for contained models? If so, doesn't that near-completely remove any usefulness of this behavior? (Or maybe it's just me - but I use Containable for just about everything).
Is there a different "CakePHP-way" to do translations fairly easily if I plan to use Containable a lot?
I've encountered similar problem, read dozens of pages from Google, but couldn't find a simple solution to my problem. After some debugging I've created this workaround snippet. Please take in consideration that this is jus a hack. It was mostly written for Croogo, so related models will appear translated on site. But i've browsed Translate behavior and it should work for it as well. Basically paste it in your AppModel Class. It's for Cake 2.x
// DIRTY LITTLE HACKS, FORCING TRANSLATE BEHAVIOR AFTERFIND CALLBACK
/**
* Hacking the afterFind so it will call the afterFind() from
* behavior
* Pase this in your AppModel Class
*
* #param array $results
* #param bool $primary
* #return array
*/
public function afterFind(array $results, $primary = false) {
parent::afterFind($results, $primary);
# calling only if not primary model, as they get translated pretty well
if (!$primary) {
# iterating behaviors to look for one that has something to do
# with translations ( Translate for cake general behavior, CroogoTranslate for Croogo based apps )
foreach ($this->Behaviors->enabled() as $behavior) {
if (preg_match('/(.*)[T|t]ranslate(.*)/', $behavior)) {
# setting locale, not sure if it gets set on secondary models
$this->locale = Configure::read('Config.language');
# hacking the result set to match behaviours requirments
# so basically creating the result set to look like called from originated model
# $k => array('ModelAlias' => array $results)
$results_tmp = array(
0 => array(
$this->alias => $results,
)
);
# if we find such behavior we force it's afterFind with prepared data
$results = $this->Behaviors->{$behavior}->afterFind($this, $results_tmp, true); # forcing true on primary - CroogoTranslate requires that
# restoring orginal structure like nothing ever happened
$results = $results[0][$this->alias];
# not sure if should break or not ?
# on one hand what's the point of having multiple translate behaviors in one app ?
# on the other i've seen more weird stuff that multiple translate behaviors
break;
}
}
}
return $results;
}
Apparently this is a common issue. The CakePHP Cookbook has some hints about how to handle it:
http://book.cakephp.org/2.0/en/core-libraries/behaviors/translate.html
I have custom validation rule:
public function customRule($check)
{
}
Inside this rule I would like to access some model data (in database). Of course I can do it like this:
$this->id = 23;
$this->read();
But then all the data in current model will be overidden by read function (I mean $this->data[$this->alias][...] is overridden.
How I can get this data?
Use a regular
$result = $this->find('first', array('conditions' => array($this->alias . '.' . $this->primaryKey => $id));
with the id in the find conditions. And work with the result, it is not overriding the data property.
Just to note that if you want to get the full record of the data that is currently being validated it is always accessible in $this->data inside the validation rule as opposed to $check which contains only the data in the currently validated field.
If you need to validate based on something that is stored in the DB, you can use $this->find() or any of the Model's functions as you are in the Model.
I support #burzum 's answer +1.
I follow in book.cake and I don't know I should send something to the parameters.
function beforeSave() {
if (!empty($this->data['Article']['create_dt']) && !empty($this->data['Article']['modified_dt'])) {
$this->data['Article']['create_dt'] = $this->dateFormatBeforeSave($this->data['Article']['create_dt']);
$this->data['Article']['modified_dt'] = $this->dateFormatBeforeSave($this->data['Article']['modified_dt']);
}
return true;
}
I try to search example but don't found.
I need many example
somebody can help me to find big resource
thank for suggest
beforeSave is called automatically by Cake before it saves data. In it, you can do whatever you want to do before each save. Typically this means altering $this->data, which is the data that is about to be saved.
The method is passed one parameter: an array of the form array('validate' => true/false, ('fieldList' => array(...)). This corresponds to the two extra parameters you can supply to save():
$this->Model->save($this->data, false, array('foo', 'bar'));
In this case the array would look like
array('validate' => false, 'fieldList' => array('foo', 'bar')).
You can accept this array by specifying an argument:
public function beforeSave($options) { ... }
$options will look like described above. You can use this information any way you want.
If you don't return true from beforeSave, the save operation will be canceled altogether.
That's all.
try using created and modified magic fields with datetime type in table cake would automatically handle them
i want to mention, that beforeSave() should be used carefully, because it is used on every time when data is saved with this model.
if you forget that it is used, you will get unexpected results.
Happens to me several times... ;)
If I have a person model with first_name and last_name, how do I create and display a full_name? I would like to display it at the top of my Edit and View views (i.e. "Edit Frank Luke") and other places. Simply dropping echoes to first_name and last_name isn't DRY.
I'm sorry if this is a very simple question, but nothing has yet worked.
Thank you,
Frank Luke
Edit for clarity: Okay, I have a function on the person model.
function full_name() {
return $this->Person->first_name . ' ' . $this->Person->last_name;
}
In the view, I call
echo $person['Person']['full_name']
This gives me a notice that I have an undefined index. What is the proper way to call the function from the view? Do I have to do it in the controller or elsewhere?
If what you are wanting is just to display a full name, and never need to do any database actions (comparisons, lookups), I think you should just concatenate your fields in the view.
This would be more aligned with the MVC design pattern. In your example you just want to view information in your database in a different way.
Since the action of concatenating is simple you probably don't save much code by placing it in a separate function. I think its easiest to do just in the view file.
If you want to do more fancy things ( ie Change the caps, return a link to the user ) I would recommend creating an element which you call with the Users data.
The arrays set by the save() method only return fields in the datbase, they do not call model functions. To properly use the function above (located in your model), you will need to add the following:
to the controller, in the $action method:
$this->set( 'fullname', $this->Person->full_name();
// must have $this-Person->id set, or redefine the method to include $person_id
in the view,
echo $fullname;
Basically, you need to use the controller to gather the data from the model, and assign it to the controller. It's the same process as you have before, where you assign the returned data from the find() call to the variable in the view, except youre getting the data from a different source.
There are multiple ways of doing this. One way is to use the afterFind-function in a model-class.
See: http://book.cakephp.org/view/681/afterFind.
BUT, this function does not handle nested data very well, instead, it doesn't handles it al all!
Therefore I use the afterfind-function in the app_model that walks through the resultset
function afterFind($results, $primary=false){
$name = isset($this->alias) ? $this->alias : $this->name;
// see if the model wants to attach attributes
if (method_exists($this, '_attachAttributes')){
// check if array is not multidimensional by checking the key 'id'
if (isset($results['id'])) {
$results = $this->_attachAttributes($results);
} else {
// we want each row to have an array of required attributes
for ($i = 0; $i < sizeof($results); $i++) {
// check if this is a model, or if it is an array of models
if (isset($results[$i][$name]) ){
// this is the model we want, see if it's a single or array
if (isset($results[$i][$name][0]) ){
// run on every model
for ($j = 0; $j < sizeof($results[$i][$name]); $j++) {
$results[$i][$name][$j] = $this->_attachAttributes($results[$i][$name][$j]);
}
} else {
$results[$i][$name] = $this->_attachAttributes($results[$i][$name]);
}
} else {
if (isset($results[$i]['id'])) {
$results[$i] = $this->_attachAttributes($results[$i]);
}
}
}
}
}
return $results;
}
And then I add a _attachAttributes-function in the model-class, for e.g. in your Person.php
function _attachAttributes($data) {
if (isset($data['first_name']) && isset($data['last_name'])) {
$data['full_name'] = sprintf("%s %s %s", $data['first_name'], $data['last_name']);
}
return $data;
}
This method can handle nested modelData, for e.g. Person hasMany Posts then this method can also attachAttributes inside the Post-model.
This method also keeps in mind that the linked models with other names than the className are fixed, because of the use of the alias and not only the name (which is the className).
You must use afterFind callback for it.
You would probably need to take the two fields that are returned from your database and concatenate them into one string variable that can then be displayed.
http://old.nabble.com/Problems-with-CONCAT-function-td22640199.html
http://teknoid.wordpress.com/2008/09/29/dealing-with-calculated-fields-in-cakephps-find/
Read the first one to find out how to use the 'fields' key i.e. find( 'all', array( 'fields' => array( )) to pass a CONCAT to the CakePHP query builder.
The second link shows you how to merge the numeric indexes that get returned when you use custom fields back into the appropriate location in the returned results.
This should of course be placed in a model function and called from there.