Check for existing children behaviour

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

<?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:

<?php
class AppModel extends Model{
...
var $actsAs = array('HasChildren');
...
?>

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:

<?php
...
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:

<?php
...
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.

Share it:
  • Facebook
  • Twitter
  • Digg
  • StumbleUpon
  • del.icio.us
  • Google Bookmarks
  • Yahoo! Buzz
  • Add to favorites
  • Identi.ca

11 thoughts on “Check for existing children behaviour

  1. shouldnt this last
    return false;
    be true, instead?

    otherwise this behavior function always will return false, wont it?

    mark

  2. 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

  3. 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.

  4. 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 🙂

  5. 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.

  6. 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

  7. 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

Leave a Reply

Your email address will not be published. Required fields are marked *