Is it safe to do query with Model while in Behavior's BeforeSave callback? - cakephp

I don't see this documented anywhere, so I ask you, my dear Cake-eaters.
Inside a CakePHP's Behavior::BeforeSave(&$Model) method, I read and write changes to $Model->data array. Before I am finished, I need to read some other records from the database. I am worried that, if I use $Model->find(), it will overwrite the current data within the model, which is about to be saved.
Viewing the source code, the Model::find() function clearly resets the Model::$id variable. This is the same variable I later use to check if a field is being updated.
Here's an example:
<?php
class UniqueBehavior extends ModelBehavior {
function beforeSave(&$Model){
$value = $Model->data[$Model->alias]['unique_field'];
$query = array('conditions' => array('unique_field' => $value));
if ($Model->find('first', $query){
// Does $Model::find() reset the internal $Model->data array?
$Model->data[$Model->alias]['unique_field'] = "..."
//... some other code here
}
//ALSO...
if ($Model->exists()) // Returns true if a record with the currently set ID exists.
$slug = $Model->field('slug');
// this should fetch the slug of the currently updated Model::id from the database
// if I do find()'s, can I trust that the record I'm getting is the right one?
}
}
?>

you can always store the current id in $tmp and assign this stored id back to the model after you are finished
$tmp = $Model->id;
// ...
$Model->id = $tmp;
This way you don't run into problems using the Model-id.
If it is save or not depends on how you work in your model.
I - for example - never rely on this id. I always assign the id to the model manually prior to any update or delete call etc. But this is not necessary, of course. You have to be more careful then, though.

Related

Create an instance of a Model in its Controller

This is probably a simple question but I'm having a hard time trying to create an object of the Model. I want to create a temporary variable that is the same as the model so I can update its internal values according to the data I am posting to the action.
My data is successfully posted to the action in the controller.
Currently what I am doing is grabbing all the items in the database and then assigning it the first item from the array. This way I can overwrite existing value, but if the table is empty it won't work.
I am using cakephp 2.x, I am new to this stuff which will explain why I couldn't figure this out.
Here is the code from the controller:
public function lcadd()
{
if ($this->request->is('post'))
{
// Create a new Local Clock object
$temp = $this->LocalClock->find('all'); // THIS IS THE PROBLEM AREA
$temp = $temp[0];
$temp['LocalClock']['name'] = $this->request->data['LocalClock']['Name'];
debug($temp); // THIS DEBUG ONLY DISPLAYS SOMETHING IF THE DATABASE TABLE IS NOT EMPTY
debug($this->request->data); // THE DATA COMING IN IS CORRECT AND I CAN ACCESS IT WITHOUT A PROBLEM
if($this->request->data['LocalClock']['Time Zone'] == 0 or $this->request->data['LocalClock']['Time Zone'] == -1)
{
//debug('Hello');
$temp['LocalClock']['auto_offset'] = 1;
}
else if($this->request->data['LocalClock']['Time Zone'] == 1)
{
$temp['LocalClock']['auto_offset'] = 0;
}
// Check the value posted from the DST
if($this->request->data['LocalClock']['DST Definition'] == -1 or $this->request->data['LocalClock']['DST Definition'] == 0)
{
$temp['LocalClock']['in_month'] = 0;
}
else if($this->request->data['LocalClock']['DST Definition'] == 2)
{
$temp['LocalClock']['in_month'] = $this->request->data['LocalClock']['InMonth'];
}
if ($this->LocalClock->save($temp))
{
$this->set('localClocks', $this->request->data);
$this->set('islcAddValid', true);
$this->set('lcaddValidationErrors', false);
}
}
}
I can't figure out how to make a simple object from the model. The model is LocalClock, and the controller is LocalClocksController.
The function above is in the LocalClocksController.
Thanks in advance.
A few things I noticed...
FIRST ISSUE:
// Create a new Local Clock object
$temp = $this->LocalClock->find('all'); // THIS IS THE PROBLEM AREA
$temp = $temp[0];
That's not creating a new Local Clock object. The first line returns an array (ie, not an object) containing ALL records in your Local Clock database table. Then, the second line says 'take the first row of the results, and store it in $temp'. So, now $temp holds an array (still NOT an object), containing the data of the first record in the LocalClock table.
You actually don't need an object, though, since the 'save' method that you call later on doesn't accept an object - it just accepts an array of data. You also don't need to get anything from the database in order to save a new record. So the code above should be replaced with:
$temp = array(); // Create an empty array. We will populate it as we go, with the data for the new record.
All the stuff in the middle of your method, up until you try to save the record, should work fine as it is.
SECOND ISSUE:
if ($this->LocalClock->save($temp))
Before calling 'save', you'll first need to call 'create' -
$this->LocalClock->create(); // prepare the Model to save a new record
if ($this->LocalClock->save($temp)) // This should now save a new record successfully.
Make sense? Let me know if you have any more questions.
Also, understanding how to retrieve, manipulate and save data from Models is very crucial to using CakePHP, and if you learn how to do it properly from the beginning, you'll save yourself countless hours of frustration in future. I'd recommend reading the following pages end-to-end:
http://book.cakephp.org/2.0/en/models/retrieving-your-data.html
http://book.cakephp.org/2.0/en/models/saving-your-data.html

CakePHP User Lookup based on their ID

I thought this would be a relatively common thing to do, but I can't find examples anywhere, and the Cookbook's section on find() was not clear in the slightest on the subject. Maybe it's just something that's so simple Cake assumes you can just do it on your own.
All I'm looking to do here is retrieve a User's name (not the currently logged-in user…a different one) in Cake based on their ID passed to my by an array in the view.
Here's what I've got in the controller:
public function user_lookup($userID){
$this->User->flatten = false;
$this->User->recursive = 1;
$user = $this->User->find('first', array('conditions' => $userID));
//what now?
}
At this point, I don't even know if I'm on the right track…I assume this will return an array with the User's data, but how do I handle those results? How do I know what the array's gonna look like? Do I just return($cakeArray['first'].' '.$cakeArray['last'])? I dunno…
Help?
You need to use set to take the returned data, and make it accessible as a variable in your views. set is the main way you send data from your controller to your view.
public function user_lookup($userID){
$this->User->flatten = false;
$this->User->recursive = 1;
// added - minor improvement
if(!$this->User->exists($userID)) {
$this->redirect(array('action'=>'some_place'));
// the requested user doesn't exist; redirect or throw a 404 etc.
}
// we use $this->set() to store the data returned.
// It will be accessible in your view in a variable called `user`
// (or what ever you pass as the first parameter)
$this->set('user', $this->User->find('first', array('conditions' => $userID)));
}
// user_lookup.ctp - output the `user`
<?php echo $user['User']['username']; // eg ?>
<?php debug($user); // see what's acutally been returned ?>
more in the manual (this is fundamental cake stuff so might be worth having a good read)

CakePHP user session not updating but database yes

I'm developing with cakePhP and I have the following problem:
When a user logs in with his name and password to the account system that I've created, he can save items (images) as favorites. This is saved in a text field into the database. What is saved is the image ID.
The saving process works perfectly, the user clicks on the images and they're added to that field (it actually saves all the IDs as a text array that I process later).
The problem comes when removing images. When the user does it (I'll post the code below), the images is removed correctly from the database (I go to PHP MyAdmin and I see it). This means that the array that holds the favorite images IDs is updated instantly. However, when I reload that array from the website, it hasn't been updated. It's like it's stored in the caché or something. Then, if the user logs out and logs in again, then he can see the correct one. The thing is that I have other things in my website that work in a similar way and they all get updated instantly, so I can't see why this doesn't.
This is the code that I use to remove the ID from the database:
function remove_favorite($pictureID) {
$this->User->id = $this->Auth->User('id'); //We get the ID of the current user
$favoritesArray = $this->User->deleteFavoritePicture($this->User->id, $pictureID); //This function retrieves the array (string) of pictures from the user's table, and deletes all the images with the ID passed as parameter, returning the updated array (string)
$fields = array('images_favorites' => $favoritesArray, 'modified' => true); //We indicate the field that we're going to update in the users table
//We save the new string that doesn't contain the deleted image anymore
if($this->User->save($fields, false, array('images_favorites'))) {
$this->Session->setFlash(__('The image has been removed from your favorites', true));
} else {
$this->Session->setFlash(__('Error removing image from favorites, please try again', true));
}
$this->redirect(array('action' => 'manage_favorites',$this->User->id));
}
This is how the deleteFavoritePicture function looks like:
function deleteFavoritePicture($userID, $pictureID) {
$userInfo = $this->find("id = $userID");
$favoritePicturesString = $userInfo['User']['images_favorites'];
$favoritePicturesArray = explode(",", $favoritePicturesString); //Array
$i = 0;
while ($i < count($favoritePicturesArray)) {
//We remove from the array the images which ID is the one we receive to delete
if ($favoritePicturesArray[$i] == $pictureID) unset($favoritePicturesArray[$i]);
$i++;
}
$favoritePicturesString = implode(",", $favoritePicturesArray); //String
return ($favoritePicturesString);
}
That's it. Does anyone now what can be going on? Thanks so much in advance for any clue!
EDIT
Ok, I think I found something that may give a clue of what's going on here:
This is the code for the manage_favorites action:
function manage_favorites($id) {
//$user = $this->User->find("id = $id");
$user = $this->Auth->user();
$this->set('user', $user);
}
That is the action that is called for the page when a user wants to modify his favorites. The same action is called once the user removes a favorite. Here's the thing:
If I use the $id parameter in the manage_favorites function and the $user = $this->User->find("id = $id"); line (the one quoted now), then the problem does not exist! This is how I used to have it. HOWEVER, I had to change it because it was a big security flaw, since the user id ($id) was a visible parameter who anyone could change, and then access other users accounts. What I did was changing the way I obtain the user array of favorite images, using the following line: $user = $this->Auth->user();. This is how I have it now (well, and also without the $id parameter in the function header), so the user information (including the favorites array) comes from the Auth component, instead directly from the database.
So, the problem is clear: when the user deletes a favorite, it's doing it on the array in the database. WHen I show the result of that operation, the array I'm retrieving is not the one in the DB, it's the one in the session. That's why it's not showing the changes.
How can I avoid this without using a non-secure method like the one I had before?
When you save, the array passed to the save method of the model should look like this:
[User] => array(
[field] => value,
[field2] => value2,
...
)
In your example, you clearly haven't added the [User] key.
Also, is your modified field actually the default Cake modified field? That is, the DATETIME field which changes to the current time when the row is updated?
Lastly, maybe you have debugging set to 2 in config.php. try changing this to 0 (as in production) and see if caching persists.
Hope some of the points I have mentioned above will solve your problem. Please let me know!
There could be two things wrong with this.
What does your deleteFavoritePicture method look like? There could be something being done wrong there.
You're passing false as the second parameter to the User::save method, which means that you don't want to validate. Unless there is a SQL error, then this will return true even if it doesn't validate properly, I believe. Try changing this false to true and see if your results differ.

Need to make full names in cakePHP

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.

What is the equivalent to getLastInsertId() in Cakephp?

If I do getLastInsertId() immediately after a save(), it works, but otherwise it does not. This is demonstrated in my controller:
function designpage() {
//to create a form Untitled
$this->Form->saveField('name','Untitled Form');
echo $this->Form->getLastInsertId(); //here it works
}
function insertformname() {
echo $this->Form->getLastInsertId(); //this doesnt echo at all
}
Please suggest a way to get the functionality I want.
CakePHP has two methods for getting the last inserted id: Model::getLastInsertID() and Model::getInsertID().
Actually these methods are identical so it really doesn't matter which method you use.
echo $this->ModelName->getInsertID();
echo $this->ModelName->getLastInsertID();
This methods can be found in cake/libs/model/model.php on line 2768
Just use:
$this->Model->id;
In Cake, the last insert id is automatically saved in the id property of the model. So if you just inserted a user via the User model, the last insert id could be accessed via $User->id
id - Value of the primary key ID of
the record that this model is
currently pointing to. Automatically
set after database insertions.
Read more about model properties in the CakePHP API Docs: http://api.cakephp.org/2.5/class-AppModel.html
Edit: I just realized that Model::getLastInsertID() is essentially the same thing as Model->id
After looking at your code more closely, it's hard to tell exactly what you're doing with the different functions and where they exist in the grand scheme of things. This may actually be more of a scope issue. Are you trying to access the last insert id in two different requests?
Can you explain the flow of your application and how it relates to your problem?
You'll need to do an insert (or update, I believe) in order for getLastInsertId() to return a value. Could you paste more code?
If you're calling that function from another controller function, you might also be able to use $this->Form->id to get the value that you want.
Try using this code in your model class (perhaps in AppModel):
function get_sql_insert_id() {
$db =& ConnectionManager::getDataSource($this->useDbConfig);
return $db->lastInsertId();
}
Caveat emptor: MySql's LAST_INSERT_ID() function only works on tables with an AUTO_INCREMENT field (otherwise it only returns 0). If your primary key does not have the AUTO_INCREMENT attribute, that might be the cause of your problems.
this is best way to find out last inserted id.
$this->ModelName->getInsertID();
other way is using
$this->ModelName->find('first',array('order'=>'id DESC'))
There are several methods to get last inserted primary key id while using save method
$this->loadModel('Model');
$this->Model->save($this->data);
This will return last inserted id of the model current model
$this->Model->getLastInsertId();
$this->Model-> getInsertID();
This will return last inserted id of model with given model name
$this->Model->id;
This will return last inserted id of last loaded model
$this->id;
Try to use this code. try to set it to a variable so you can use it in other functions. :)
$variable = $this->ModelName->getLastInsertId();
in PHP native, try this.
$variable = mysqli_insert_id();
This will return last inserted id of last loaded model
$this->id;
This will return last inserted id of model with given model name
$this->Model->id;
This will return last inserted id of the model current model
CakePHP has two methods for getting the last inserted id:
Model::getLastInsertID() and Model::getInsertID().
echo $this->ModelName->getInsertID();
echo $this->ModelName->getLastInsertID();
Below are the options:
echo $this->Registration->id;
echo $this->Registration->getInsertID();
echo $this->Registration->getLastInsertId();
Here, you can replace Registration with your model name.
Thanks
Use this one
function designpage() {
//to create a form Untitled
$this->Form->saveField('name','Untitled Form');
echo $this->Form->id; //here it works
}
You can get last inseted id with many ways.Like Model name is User so best way to fetch the last inserted id is
$this->User->id; // For User Model
You can also use Model function but below code will return last inserted id of model with given model name for this example it will return User model data
$this->User->getLastInsertId();
$this->User->getInsertID();
When you use save(), the last insert ID is set to the model’s $id property. So:
if ($this->Model->save()) {
printf('Last insert ID was %s', $this->Model->id);
}
Each time a save method is called on a model, cake internally calls Model::getLastInsertId() and stores the result into model class attribute id, so after calling save() it is not necessary to call Model::getLastInsertId() or inserId(), as tha value can be directly accessed like this
$id = $this->id;// within a model
$id = $this->{$this->modelName}->id;// in a controller
After insertion of data, we can use following code to get recently added record's id:
$last_insert_id=$this->Model->id;
each time you perform an insert operation on any model, cake internally fetchesthe last insert Id and Sets to Model->id attribute.
so one can access it directly by $Model->id;,
no need to query again for lastInsertId.
I think it works with getLastInsertId() if you use InnoDB Tables in your MySQL Database. You also can use $this->Model->id
$Machinedispatch =
$this->Machinedispatch->find('first',array('order'=>array('Machinedispatch.id DESC')));
Simplest way of finding last inserted row. For me getLastInsertId() this not works.
Actually you are using the getLastInsertId or getInsertId in a wrong manner.
getLastInsertId() is meant to work only after save() method.
It will even not work after a manual insert, as cake engine is storing the mysql_insert_id under $this->_insertID inside the save method which can be retrieved via the getLastInsertId or getInsertId.
Now in your case
$this->Model->id
OR
$this->Model->find('first',array('order'=>'id DESC'))
Will do.
This is interesting, I also stumbled upon this issue. What you asked perhaps how to get the last ID of a certain model regardless of it's state, whether it's just been inserted or not. To further understand what getInsertID does, we need to take a look at the source:
Link 1: http://api20.cakephp.org/view_source/model#line-3375
public function getInsertID() {
return $this->_insertID
}
Yup, that's the only piece of code inside that function. It means that cakephp caches any last inserted ID, instead of retrieve it from the database. That's why you get nothing if you use that function when you haven't done any record creation on the model.
I made a small function to get the last ID of a certain table, but please note that this should not be used as a replacement of getLastID() or getLastInsertID(), since it has an entirely different purpose.
Add the function lastID() to the AppModel as shown below so that it can be used system wide. It has it's limit, which can't be used on model with composite primary key.
class AppModel extends Model {
public function lastID() {
$data = $this->find('first',
array(
'order' => array($this->primaryKey . ' DESC'),
'fields' => array($this->primaryKey)
)
);
return $data[$this->name][$this->primaryKey];
}
}
Original Source : Class Model
In CakePHP you can get it by:
Model::getInsertID() //Returns the ID of the last record this model inserted.
Model::getLastInsertID() //Alias to getInsertID().
$this->Model->field('id', null, 'id DESC')

Resources