Behaviors

Behaviors are a way to organize and enable horizontal re-use of Model layerlogic. Conceptually they are similar to traits. However, behaviors areimplemented as separate classes. This allows them to hook into thelife-cycle callbacks that models emit, while providing trait-like features.

Behaviors provide a convenient way to package up behavior that is common acrossmany models. For example, CakePHP includes a TimestampBehavior. Manymodels will want timestamp fields, and the logic to manage these fields isnot specific to any one model. It is these kinds of scenarios that behaviors area perfect fit for.

Using Behaviors

Behaviors provide an easy way to create horizontally re-usable pieces of logicrelated to table classes. You may be wondering why behaviors are regular classesand not traits. The primary reason for this is event listeners. While traitswould allow for re-usable pieces of logic, they would complicate binding events.

To add a behavior to your table you can call the addBehavior() method.Generally the best place to do this is in the initialize() method:

  1. namespace App\Model\Table;
  2.  
  3. use Cake\ORM\Table;
  4.  
  5. class ArticlesTable extends Table
  6. {
  7. public function initialize(array $config): void
  8. {
  9. $this->addBehavior('Timestamp');
  10. }
  11. }

As with associations, you can use plugin syntax and provide additionalconfiguration options:

  1. namespace App\Model\Table;
  2.  
  3. use Cake\ORM\Table;
  4.  
  5. class ArticlesTable extends Table
  6. {
  7. public function initialize(array $config): void
  8. {
  9. $this->addBehavior('Timestamp', [
  10. 'events' => [
  11. 'Model.beforeSave' => [
  12. 'created_at' => 'new',
  13. 'modified_at' => 'always'
  14. ]
  15. ]
  16. ]);
  17. }
  18. }

Core Behaviors

Creating a Behavior

In the following examples we will create a very simple SluggableBehavior.This behavior will allow us to populate a slug field with the results ofText::slug() based on another field.

Before we create our behavior we should understand the conventions forbehaviors:

  • Behavior files are located in src/Model/Behavior, orMyPlugin\Model\Behavior.
  • Behavior classes should be in the App\Model\Behavior namespace, orMyPlugin\Model\Behavior namespace.
  • Behavior class names end in Behavior.
  • Behaviors extend Cake\ORM\Behavior.

To create our sluggable behavior. Put the following intosrc/Model/Behavior/SluggableBehavior.php:

  1. namespace App\Model\Behavior;
  2.  
  3. use Cake\ORM\Behavior;
  4.  
  5. class SluggableBehavior extends Behavior
  6. {
  7. }

Similar to tables, behaviors also have an initialize() hook where you canput your behavior’s initialization code, if required:

  1. public function initialize(array $config): void
  2. {
  3. // Some initialization code here
  4. }

We can now add this behavior to one of our table classes. In this example we’lluse an ArticlesTable, as articles often have slug properties for creatingfriendly URLs:

  1. namespace App\Model\Table;
  2.  
  3. use Cake\ORM\Table;
  4.  
  5. class ArticlesTable extends Table
  6. {
  7. public function initialize(array $config): void
  8. {
  9. $this->addBehavior('Sluggable');
  10. }
  11. }

Our new behavior doesn’t do much of anything right now. Next, we’ll add a mixinmethod and an event listener so that when we save entities we can automaticallyslug a field.

Defining Mixin Methods

Any public method defined on a behavior will be added as a ‘mixin’ method on thetable object it is attached to. If you attach two behaviors that provide thesame methods an exception will be raised. If a behavior provides the same methodas a table class, the behavior method will not be callable from the table.Behavior mixin methods will receive the exact same arguments that are providedto the table. For example, if our SluggableBehavior defined the followingmethod:

  1. public function slug($value)
  2. {
  3. return Text::slug($value, $this->_config['replacement']);
  4. }

It could be invoked using:

  1. $slug = $articles->slug('My article name');

Limiting or Renaming Exposed Mixin Methods

When creating behaviors, there may be situations where you don’t want to exposepublic methods as mixin methods. In these cases you can use theimplementedMethods configuration key to rename or exclude mixin methods. Forexample if we wanted to prefix our slug() method we could do the following:

  1. protected $_defaultConfig = [
  2. 'implementedMethods' => [
  3. 'superSlug' => 'slug',
  4. ]
  5. ];

Applying this configuration will make slug() not callable, however it willadd a superSlug() mixin method to the table. Notably if our behaviorimplemented other public methods they would not be available as mixinmethods with the above configuration.

Since the exposed methods are decided by configuration you can alsorename/remove mixin methods when adding a behavior to a table. For example:

  1. // In a table's initialize() method.
  2. $this->addBehavior('Sluggable', [
  3. 'implementedMethods' => [
  4. 'superSlug' => 'slug',
  5. ]
  6. ]);

Defining Event Listeners

Now that our behavior has a mixin method to slug fields, we can implementa callback listener to automatically slug a field when entities are saved. We’llalso modify our slug method to accept an entity instead of just a plain value. Ourbehavior should now look like:

  1. namespace App\Model\Behavior;
  2.  
  3. use ArrayObject;
  4. use Cake\Datasource\EntityInterface;
  5. use Cake\Event\EventInterface;
  6. use Cake\ORM\Behavior;
  7. use Cake\ORM\Entity;
  8. use Cake\ORM\Query;
  9. use Cake\Utility\Text;
  10.  
  11. class SluggableBehavior extends Behavior
  12. {
  13. protected $_defaultConfig = [
  14. 'field' => 'title',
  15. 'slug' => 'slug',
  16. 'replacement' => '-',
  17. ];
  18.  
  19. public function slug(EntityInterface $entity)
  20. {
  21. $config = $this->getConfig();
  22. $value = $entity->get($config['field']);
  23. $entity->set($config['slug'], Text::slug($value, $config['replacement']));
  24. }
  25.  
  26. public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
  27. {
  28. $this->slug($entity);
  29. }
  30.  
  31. }

The above code shows a few interesting features of behaviors:

  • Behaviors can define callback methods by defining methods that follow theLifecycle Callbacks conventions.
  • Behaviors can define a default configuration property. This property is mergedwith the overrides when a behavior is attached to the table.

To prevent the save from continuing, simply stop event propagation in your callback:

  1. public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
  2. {
  3. if (...) {
  4. $event->stopPropagation();
  5. $event->setResult(false);
  6. return;
  7. }
  8. $this->slug($entity);
  9. }

Alternatively, you can return false from the callback. This has the same effect as stopping event propagation.

Defining Finders

Now that we are able to save articles with slug values, we should implementa finder method so we can fetch articles by their slug. Behavior findermethods, use the same conventions as Custom Finder Methods do. Ourfind('slug') method would look like:

  1. public function findSlug(Query $query, array $options)
  2. {
  3. return $query->where(['slug' => $options['slug']]);
  4. }

Once our behavior has the above method we can call it:

  1. $article = $articles->find('slug', ['slug' => $value])->first();

Limiting or Renaming Exposed Finder Methods

When creating behaviors, there may be situations where you don’t want to exposefinder methods, or you need to rename finders to avoid duplicated methods. Inthese cases you can use the implementedFinders configuration key to renameor exclude finder methods. For example if we wanted to rename our find(slug)method we could do the following:

  1. protected $_defaultConfig = [
  2. 'implementedFinders' => [
  3. 'slugged' => 'findSlug',
  4. ]
  5. ];

Applying this configuration will make find('slug') trigger an error. Howeverit will make find('slugged') available. Notably if our behavior implementedother finder methods they would not be available, as they are not includedin the configuration.

Since the exposed methods are decided by configuration you can alsorename/remove finder methods when adding a behavior to a table. For example:

  1. // In a table's initialize() method.
  2. $this->addBehavior('Sluggable', [
  3. 'implementedFinders' => [
  4. 'slugged' => 'findSlug',
  5. ]
  6. ]);

Transforming Request Data into Entity Properties

Behaviors can define logic for how the custom fields they provide aremarshalled by implementing the Cake\ORM\PropertyMarshalInterface. Thisinterface requires a single method to be implemented:

  1. public function buildMarshalMap($marshaller, $map, $options)
  2. {
  3. return [
  4. 'custom_behavior_field' => function ($value, $entity) {
  5. // Transform the value as necessary
  6. return $value . '123';
  7. }
  8. ];
  9. }

The TranslateBehavior has a non-trivial implementation of this interfacethat you might want to refer to.

Removing Loaded Behaviors

To remove a behavior from your table you can call the removeBehavior() method:

  1. // Remove the loaded behavior
  2. $this->removeBehavior('Sluggable');

Accessing Loaded Behaviors

Once you’ve attached behaviors to your Table instance you can introspect theloaded behaviors, or access specific behaviors using the BehaviorRegistry:

  1. // See which behaviors are loaded
  2. $table->behaviors()->loaded();
  3.  
  4. // Check if a specific behavior is loaded.
  5. // Remember to omit plugin prefixes.
  6. $table->behaviors()->has('CounterCache');
  7.  
  8. // Get a loaded behavior
  9. // Remember to omit plugin prefixes
  10. $table->behaviors()->get('CounterCache');

Re-configuring Loaded Behaviors

To modify the configuration of an already loaded behavior you can combine theBehaviorRegistry::get command with config command provided by theInstanceConfigTrait trait.

For example if a parent (e.g. an AppTable) class loaded the Timestampbehavior you could do the following to add, modify or remove the configurationsfor the behavior. In this case, we will add an event we want Timestamp torespond to:

  1. namespace App\Model\Table;
  2.  
  3. use App\Model\Table\AppTable; // similar to AppController
  4.  
  5. class UsersTable extends AppTable
  6. {
  7. public function initialize(array $options): void
  8. {
  9. parent::initialize($options);
  10.  
  11. // e.g. if our parent calls $this->addBehavior('Timestamp');
  12. // and we want to add an additional event
  13. if ($this->behaviors()->has('Timestamp')) {
  14. $this->behaviors()->get('Timestamp')->setConfig([
  15. 'events' => [
  16. 'Users.login' => [
  17. 'last_login' => 'always'
  18. ],
  19. ],
  20. ]);
  21. }
  22. }
  23. }