In some of my projects I had a requirement to prevent deletion of the records, which had children. That’s why I create a small behaviour which checks these cases and prevent this.
Add this code in /app/models/behaviours/has_children.php
/**
* Prevent deletion if child record found
*
* @author Nik Chankov
* @url http://nik.chankov.net
*/
class HasChildrenBehavior extends ModelBehavior {
/**
* Empty Setup Function
*/
function setup(&$model) {
$this->model = $model;
}
/**
* Run the check and cancel the deletion if child is found
* @access public
*/
function beforeDelete(){
if(isset($this->model->hasMany)){
foreach($this->model->hasMany as $key=>$value){
$childRecords = $this->model->{$key}->find('count', array('conditions'=>array($value['foreignKey']=>$this->model->id)));
if($childRecords > 0){
return false;
}
}
}
//Checking habtm relation as well, thanks to Zoltan
if(isset($this->model->hasAndBelongsToMany)){
foreach($this->model->hasAndBelongsToMany as $key=>$value){
$childRecords = $this->model->{$key}->find('count', array('conditions'=>array($value['foreignKey']=>$this->model->id)));
if($childRecords > 0){
return false;
}
}
}
return true;
}
}
?>
What that code doing? In the function beforeDelete check all models in the hasMany array /defined in your model/ and check if there are records related to the current model. if there is any return false, if there are none – return true which will continue deletion.
How to activate it?
if you want this behaviour to be valid for all models in your application, just add it in AppModel class with:
Or if you want only few models to be affected, just put this $actsAs only in them.
Now it will work, but unfortunately when we do Delete the normal code (generated from the /console/cake) in that action is something like:
...
function delete($id = null) {
if (!$id) {
$this->Session->setFlash(__('Invalid id for Category', true));
$this->redirect(array('action'=>'index'), null, true);
}
if ($this->MyModel->del($id)) {
$this->Session->setFlash(__('MyModel #', true).$id.__(' deleted', true));
$this->redirect(array('action'=>'index'), null, true);
}
}
...
?>
Which when receive “return false” wont enter in the second if and the page will remain blank. To prevent such issue your delete actions should look like:
...
function delete($id = null) {
if (!$id) {
$this->Session->setFlash(__('Invalid id for Category', true));
$this->redirect(array('action'=>'index'), null, true);
}
if ($this->MyModel->del($id)) {
$this->Session->setFlash(__('MyModel #', true).$id.__(' deleted', true));
$this->redirect(array('action'=>'index'), null, true);
} else {
$this->Session->setFlash(__(' The record cannot be deleted!', true));
$this->redirect(array('action'=>'index'), null, true);
}
}
...
?>
The inconvenience coming from that, because you should make this in all of your controllers, but if you starting a new project and you modify your templates before start baking it shouldn’t be so big pain.
shouldnt this last
return false;
be true, instead?
otherwise this behavior function always will return false, wont it?
mark
Mark, you are right. It’s my fault and I did it while I test the behavior. I will correct it right now.
Cheers!
Nik,
What about using type Innodb in MySql? That way referential integrity would be preserved and no parent could be deleted?! Is it possible for cake to work with InnoDB type and how would Cake react to that errors that it would get back for such queries from MySql?
I’m new in Cake so if my idea has some holes please inform me 🙂
by,
Luka
Luca,
sorry but I haven’t used Cake with MySQL InnoDB. I’ve used it with Oracle where the strict relations can be used as well. Basically the Mysql or Oracle throw the error on the web page in the same way as normal PHP script do – just on the top of the page.
That’s what observed even today when I’ve set an unique constraint of one “email” field. The validation in the model was set to check the uniqueness only “on Create”. action.
So I try to edit an existing record and changed the e-mail with once which I know it’s in the database. The result was mysql error on the screen and the script throw message that the record cannot be saved.
So I could say that the database could stop the deletion as well. The errors from the database are not visible if you set your debug level in core to 0. I also doesn’t like to know that errors are just hidden, but…
Hope this answer partly to your question.
Thanks,
that’s what I thought but I was not sure how would Cake throw out that message. On top of layout wouldn’t be very nice to see even in admin interface…
thank you very much
i’ll stay tuned 🙂
Well disabling the errors in core.php is a solution, depends if you like it or not. 🙂
Nik,
As far as I see, there is no reason to fetch the associated records, it’s enough if you simply count them. findAll is deprecated, you can use find(‘count’) instead. This way fetching associated records can be avoided and code would run much faster.
Code needs to be completed in order to handle hasAndBelongsToMany relations.
Although it’s an old post you have right. I will change the code, in order to help to someone else 🙂
Thanks
works like a charm!
The only thing for make it run on cake php 1.3.11 I had to do:
change line 25 and 34
from:
$childRecords = $this->model->{$key}->find(‘count’, array(‘conditions’ => array($value[‘foreignKey’] => $this->model->id)));
to:
$childRecords = $this->model->{$key}->find(‘count’, array(‘conditions’ => array($key.’.’.$value[‘foreignKey’] => $this->model->id)));
very helpfull behavior! 🙂
thanks from Argentina 🙂
Pablo
Thank , this is very helpful post
Hello Nik,
thank you for this good post.
I corrected a little bit the code because I had pbm if there are several hasMany relation. I gaves an sql error : #1052 – Column ‘country_id’ in where clause is ambiguous.
This is the changed code :
model = $model;
}
/**
* Run the check and cancel the deletion if child is found
* @access public
*/
function beforeDelete(){
if(isset($this->model->hasMany)){
foreach($this->model->hasMany as $key=>$value){
$childRecords = $this->model->{$key}->find(‘count’, array(‘conditions’=>array($key .”.”. $value[‘foreignKey’]=>$this->model->id)));
if($childRecords > 0){
return false;
}
}
}
//Checking habtm relation as well, thanks to Zoltan
if(isset($this->model->hasAndBelongsToMany)){
foreach($this->model->hasAndBelongsToMany as $key=>$value){
$childRecords = $this->model->{$key}->find(‘count’, array(‘conditions’=>array($key .”.”. $value[‘foreignKey’]=>$this->model->id)));
if($childRecords > 0){
return false;
}
}
}
return true;
}
}
?>
Thanks again,
Cheers,
Renato