Attach & detach behaviors at run-time in CakePHP Models

Update Cakebaker blog tells us that attaching and detaching behaviors on the fly is now built in CakePHP core (we obviously have been doing something right)

Behaviors are one of the best things that have been added to CakePHP 1.2, they allow you to add functionality to your models in a very elegant and modular fashion. They also promote a lot of code reuse.

Perhaps a real life example would illustrate it better. In Cheesecake Photoblog when a new photo was added it was the controller which checked if a proper file was uploaded, resized it and then move it, once this was done the EXIF vendor class was used to extract the EXIF info and store that in the database. The code for the photos/add method was fairly bloated and ugly. However with behaviors, in Cheesecake Photoblog 2.0, we were able to move these two task away from the action and now all that the add action code does is call $this->Photo->save() the attached ‘file’ and ‘exif’ behaviors automagically take care of the rest. Also with the file upload handling moved code to a behavior it can be reused with any model which needs to handle file uploads

While writing code for V2.0 I needed to detach some behaviors at run-time. More specifically I did not want the File Upload and EXIF behavior to act during an edit action when the picture was not being replaced! This functionality was not built in to CakePHP 1.2 last I checked and I wanted it NOW – solution? code it!

Detaching behavior was simple in the controller I did

  1. unset($this->Photo->behaviors['file']);
  2. unset($this->Photo->behaviors['exif']);

This got me thinking… why not have a method which will do it more elegantly. The result was

  1. /**
  2. * Method used to un-set behaviors at run-time
  3. *
  4. * @access public
  5. */
  6. function dontActAs()
  7. {
  8. // Get method arguments
  9. $behaviors = func_get_args();
  10.  
  11. // Loop through method arguments
  12. foreach($behaviors as $index => $behavior)
  13. {
  14. // If method agrument is an array
  15. if (is_array($behavior))
  16. {
  17. // If method agrument contains more than one element then merge it with method arguments
  18. if (count($behavior) > 0)
  19. {
  20. $behaviors = array_merge($behaviors, $behavior);
  21. }
  22.  
  23. // Unset method argument from method arguments
  24. unset($behaviors[$index]);
  25. }
  26. }
  27.  
  28. // Loop through passed behaviors
  29. foreach ($behaviors as $behavior)
  30. {
  31. // Un-set the behavior
  32. unset($this->behaviors[$behavior]);
  33. }
  34. }

If you want to use the method in your controller – do

  1. $this->MyModel->dontActAs('mybehav1', 'mybehav2');

or

  1. $behaviorsToDetach = array('mybehav1', 'mybehav2');
  2.  
  3. $this->MyModel->dontActAs($behaviorsToDetach);

The benefit of being allowed to code on Open Source projects during work is that you can do what you want and as you want. While just detaching behaviors at runtime would be enough for most projects, For safety I wanted a method to attach a behavior at runtime.

A bit of delving in to cake’s core model’s constructor code (related to behavior attachment) I ended up with following code

  1. /**
  2. * Method used to set behaviors at run-time
  3. *
  4. * @access public
  5. * @param array $behaviors List of behaviors to set at run-time
  6. */
  7. function nowActsAs($behaviors = array())
  8. {
  9. // If passed behaviour is array and contain more than one element
  10. if (is_array($behaviors) && count($behaviors) > 0)
  11. {
  12. // Initialize behavior's callback methods and then normalize passed behaviors array
  13. $callbacks = array('setup', 'beforeFind', 'afterFind', 'beforeSave', 'afterSave', 'beforeDelete', 'afterDelete', 'afterError');
  14. $behaviors = Set::normalize($behaviors);
  15.  
  16. // Loop through passed behaviors
  17. foreach ($behaviors as $behavior => $config)
  18. {
  19. // Build behavior's class name
  20. $className = $behavior . 'Behavior';
  21.  
  22. // Behavior's class file included successfully
  23. if (loadBehavior($behavior))
  24. {
  25. // If behavior's class object is in registry
  26. if (ClassRegistry::isKeySet($className))
  27. {
  28. // If PHP5 then get object directly
  29. if (PHP5)
  30. {
  31. $this->behaviors[$behavior] = ClassRegistry::getObject($className);
  32. }
  33. else
  34. {
  35. // If not PHP5 then get object by reference
  36. $this->behaviors[$behavior] =& ClassRegistry::getObject($className);
  37. }
  38. }
  39. else
  40. {
  41. // If PHP5 then create/get object directly
  42. if (PHP5)
  43. {
  44. $this->behaviors[$behavior] = new $className;
  45. }
  46. else
  47. {
  48. // If not PHP5 then create/get object by reference
  49. $this->behaviors[$behavior] =& new $className;
  50. }
  51.  
  52. // Store object in registry
  53. ClassRegistry::addObject($className, $this->behaviors[$behavior]);
  54. }
  55.  
  56. // Call behavior's 'setup' method and then get behavior's map methods
  57. $this->behaviors[$behavior]->setup($this, $config);
  58. $methods = $this->behaviors[$behavior]->mapMethods;
  59.  
  60. // Loop through behavior's map methods
  61. foreach ($methods as $method => $alias)
  62. {
  63. // If map method is not already added in array then add it
  64. if (!array_key_exists($method, $this->__behaviorMethods))
  65. {
  66. $this->__behaviorMethods[$method] = array($alias, $behavior);
  67. }
  68. }
  69.  
  70. // Get behaviour's and model behaviour's class methods
  71. $methods = get_class_methods($this->behaviors[$behavior]);
  72. $parentMethods = get_class_methods('ModelBehavior');
  73.  
  74. // Loop through behavior's class methods
  75. foreach ($methods as $m)
  76. {
  77. // If behavior's class method is not present in model behaviour's class methods then proceed further
  78. if (!in_array($m, $parentMethods))
  79. {
  80. // If behavior's class method is not protected/private, it is not in behavior methods and also not in callback methods
  81. if (strpos($m, '_') !== 0 && !array_key_exists($m, $this->__behaviorMethods) && !in_array($m, $callbacks))
  82. {
  83. // Store behavior's class method details
  84. $this->__behaviorMethods[$m] = array($m, $behavior);
  85. }
  86. }
  87. }
  88. }
  89. }
  90. }
  91. }

You can use above method in controller like same as you define your actsAs array

  1. $behaviorsToAttach = array
  2. (
  3. 'mybehav1' => array
  4. (
  5. 'setting11' => 'value11'
  6. , 'setting12' => 'value12'
  7. )
  8. , 'mybehav2' => array()
  9. );
  10.  
  11. $this->MyModel->nowActsAs($behaviorsToAttach);

Of Course – you all know that the above two methods should go into your app_model.php – right?

P.S. Thanks to Dr. Tarique Sani for his help with this article

About Amit Badkas

Amit Badkas is Zend certified PHP5 and Zend Framework engineer, and has been working in SANIsoft for past 10 years, his present designation is 'Technical Manager'

10 Responses to Attach & detach behaviors at run-time in CakePHP Models

  1. Zoltan November 13, 2007 at 8:25 am #

    Just used this on a project where I’m occasionally updating fields , but don’t want the regular behaviours to fire – thanks a lot.

  2. Tarique Sani November 13, 2007 at 9:27 am #

    Hey! somebody actually used it :) thanks for the comment

  3. Thierry January 23, 2008 at 8:15 pm #

    Interesting…
    I had checked cake 3 years ago, but didn’t used it.

    I’m more of a “self doer” than to rely on framework, but the idea of relying to behaviors attached to elements surely looks nice.

    I’ll try to put some time on cake in the next week, I could be agreeably surprised.

    Thanks for your article.

  4. iivs March 1, 2009 at 4:36 am #

    Great code. Thanks! :) But there are several problems.
    1) there is no loadBehavior in version 1.2 so I had to change ir to App::import('Behavior', $behavior);
    2) It doesn’t work at all. To remove all (hopefully) behaviours you have to loop through attachments like this foreach ($behaviors as $behavior) // Loop through passed behaviors
    {
    unset($this->actsAs);
    foreach ($this->Behaviors->_attached as $key => $attached)
    {
    if ($attached == $behavior)
    unset($this->Behaviors->_attached[$key]);
    }
    unset($this->Behaviors->$behavior);
    unset($this->locale);
    }

    3) cakephp caches queries. You can’t attach and detach behaviours in the next line. I’m now trying to figure out how to rebuild model shema.

  5. iivs March 1, 2009 at 4:58 am #

    Oh, I found it. $this->schema(true)

  6. Tarique Sani March 1, 2009 at 9:09 am #

    @iivs When the post was written there was a loadBehavior() thanks for the updated code

  7. iivs March 1, 2009 at 4:37 pm #

    I found another solution. For example I have a model with a variable var $actsAs = array('i18n' => array('fields'=>array('name')) );
    I can detach that beavior any time and attach back again. I’ve written 4 functions to do that. 1) private function to merge behavior array
    function __behaviors($behaviors = null)
    {
    if ($behaviors)
    {
    foreach ($behaviors as $index => $behavior) // Loop through method arguments
    {
    if (is_array($behavior)) // If method agrument is an array
    {
    if (count($behavior) > 0) // If method agrument contains more than one element then merge it with method arguments
    {
    $behaviors = array_merge($behaviors, $behavior);
    }
    unset($behaviors[$index]); // Unset method argument from method arguments
    }
    }

    return $behaviors;
    }
    }

    2) private function checks if behavior is disabled or not. if it is disabled, return array index else return false.
    function __is_disabled($behavior)
    {
    $disabled = false;
    if (isset($this->Behaviors->_disabled) and count($this->Behaviors->_disabled) > 0)
    {
    foreach ($this->Behaviors->_disabled as $key => $val)
    {
    if ($val == $behavior)
    {
    $disabled = $key;
    break;
    }
    }
    }

    return $disabled;
    }

    3) adds behaviors in $this->Model->Behaviors->_disabled array
    function dontActAs()
    {
    $behaviors = $this->__behaviors(func_get_args());

    if ($behaviors and count($behaviors) > 0)
    {
    foreach ($behaviors as $behavior)
    {
    if ($this->__is_disabled($behavior) == false)
    $this->Behaviors->_disabled[] = $behavior; //add new one
    }

    $this->schema(true); //rebuild model schema
    }

    }

    4) removes from $this->Model->Behaviors->_disabled array
    function nowActsAs()
    {
    $behaviors = $this->__behaviors(func_get_args());

    if ($behaviors and count($behaviors) > 0)
    {
    foreach ($behaviors as $behavior)
    {
    $key = $this->__is_disabled($behavior);
    unset($this->Behaviors->_disabled[$key]);
    }

    $this->schema(true); //rebuild model schema
    }
    }

    I also found some functions in cake/libs/model/behavior.php “detach”, “disable” “enable” etc. not sure how they work. I’m still new to this cakephp thing.

  8. Everton Yoshitani January 13, 2011 at 10:17 pm #

    Just an update for CakePHP 1.3.x we can use BehaviorCollection

    http://api13.cakephp.org/class/behavior-collection

Trackbacks/Pingbacks

  1. PHPDeveloper.org - June 26, 2007

    Sanisoft.com: Attach & detach behaviors at run-time in CakePHP Models…

  2. developercast.com » Sanisoft.com: Attach & detach behaviors at run-time in CakePHP Models - June 27, 2007

    [...] guys over on Sanisoft.com dropped us a line today to let us know about a new entry on their blog showing how to implement attach and detach behaviors in CakePHP models. Behaviors are one of the [...]

Leave a Reply