CakePHP behaviors are a great way to not only extend model functionality, but also share that functionality across different models, and applications. Using behaviors, we can keep our model code concise and to the point, extracting code that may not be directly related to our business logic, but still affect how our models behave.
In this recipe we will learn how to use model callbacks to automatically retrieve each profile's latest tweets, and how to add a custom validation method to the behavior.
We need some sample models to work with. Follow the Getting ready section of the recipe Adding multiple validation rules.
We will also need a method to list all profiles. Edit your app/controllers/profiles_controller.php
file and add the following index()
method to the ProfilesController
class:
public function index() { $profiles = $this->Profile->find('all'), $this->set(compact('profiles')); }
Create the respective view in a file named app/views/profiles/index.ctp
, with the following contents:
<?php foreach($profiles as $profile) { ?> <p> <?php echo $this->Html->link( $profile['Profile']['twitter'], 'http://twitter.com/' . $profile['Profile']['twitter'], array('title' => $profile['Profile']['twitter']) ); ?> </p> <?php } ?>
TwitterAccountBehavior
in a file named twitter_account.php
and place it in your app/models/behaviors
folder, with the following contents:<?php App::import('Core', 'HttpSocket'), class TwitterAccountBehavior extends ModelBehavior { protected static $httpSocket; public function setup($model, $config = array()) { parent::setup($model, $config); $this->settings[$model->alias] = array_merge(array( 'field' => 'twitter' ), $config); } protected function timeline($twitter, $count = 10, $returnStatus = false) { if (!isset(self::$httpSocket)) { self::$httpSocket = new HttpSocket(); } $content = self::$httpSocket->get('http://twitter.com/status/user_timeline/' . $twitter . '.json?count=' . $count); $status = self::$httpSocket->response['status']['code']; if (!empty($content)) { $content = json_decode($content); } if ($returnStatus) { return compact('status', 'content'), } return $content; } } ?>
setup()
method implemented and a helper timeline()
method to obtain tweets from a Twitter account, we can proceed to add the required validation.Add the following custom validation method to the TwitterAccountBehavior class:
public function validateTwitter($model, $data) { $field = $this->settings[$model->alias]['field']; if (!empty($data[$field])) { $value = $data[$field]; $result = $this->timeline($value, 1, true); if ($result['status'] == 404) { $result = false; } } return $result; }
Profile
model, and add the validation for the twitter
field. Open your app/models/profile.php
file and add the following actsAs
property and the twitter
field validation:<?php class Profile extends AppModel { public $actsAs = array('TwitterAccount'), public $validate = array( 'email' => array('rule' => 'notEmpty'), 'name' => array('rule' => 'notEmpty'), 'twitter' => array( 'rule' => 'validateTwitter', 'allowEmpty' => true, 'message' => 'This twitter account is not valid' ) ); } ?>
twitter
field shown in the following screenshot: beforeFind()
and afterFind()
methods to the TwitterAccountBehavior
class:public function beforeFind($model, $query) { $this->settings[$model->alias]['tweets'] = !isset($query['tweets']) ? true : $query['tweets']; return parent::beforeFind($model, $query); } public function afterFind($model, $results, $primary) { $rows = parent::afterFind($model, $results, $primary); if (!is_null($rows)) { $results = $rows; } if (!empty($this->settings[$model->alias]['tweets'])) { $field = $this->settings[$model->alias]['field']; $count = is_int($this->settings[$model->alias]['tweets']) ? $this->settings[$model->alias]['tweets'] : 10; foreach($results as $i => $result) { $twitter = $result[$model->alias][$field]; $tweets = array(); if (!empty($result[$model->alias][$field])) { $result = $this->timeline($twitter, $count); if (!empty($result) && is_array($result)) { foreach($result as $tweet) { $tweets[] = array( 'created' => date('Y-m-d H:i:s', strtotime($tweet->created_at)), 'source' => $tweet->source, 'user' => $tweet->user->screen_name, 'text' => $tweet->text ); } } } $results[$i]['Tweet'] = $tweets; } } return $results; }
app/views/profiles/index.ctp
view and make the following changes:<?php foreach($profiles as $profile) { ?>
<p>
<?php echo $this->Html->link(
$profile['Profile']['twitter'],
'http://twitter.com/' . $profile['Profile']['twitter'],
array('title' => $profile['Profile']['twitter'])
); ?>
<?php if (!empty($profile['Tweet'])) { ?>
<ul>
<?php foreach($profile['Tweet'] as $tweet) { ?>
<li>
<code><?php echo $tweet['text']; ?></code>
from <?php echo $tweet['source']; ?>
on <?php echo $tweet['created']; ?>
</li>
<?php } ?>
</ul>
<?php } ?>
</p>
<?php } ?>
After adding a valid Twitter account, browsing to http://localhost/profiles
would generate a listing, such as the one shown in the following screenshot:
We started with the skeleton for our TwitterAccountBehavior
, implementing the setup()
method, called automatically by CakePHP whenever the behavior is attached to a model, and the timeline()
method, which is nothing more than the validateTwitter()
method shown in the recipe Create a custom validation rule optimized for reutilization.
The beforeFind
callback is triggered by CakePHP whenever a find operation is about to be executed, and we used it to check the existence of the custom tweets
find setting. We use this setting to allow the developer to either disable the fetch of tweets, by setting it to false
:
$this->Profile->find('all', array('tweets' => false));
or specify how many tweets should be obtained. For example, if we wanted to obtain only the latest tweet, we would do:
$this->Profile->find('all', array('tweets' => 1));
The afterFind
callback is executed after a find operation is executed, and gives us an opportunity to modify the results. Therefore we check to make sure we are told to obtain the tweets, and if so we use the timeline()
method to obtain the specified number of tweets. We then append each tweet's basic information into the index Tweet
for each profile.
One thing that is clear in our implementation is that, unless we set the tweets
find option to false
; we are obtaining tweets for each profile record on every find
operation performed against the Profile
model. Adding caching support would greatly improve the performance of our find
operations, since we would only obtain the tweets when the cached information is no longer valid.
More information about caching through CakePHP's Cache class can be obtained at http://book.cakephp.org/view/1511/Cache.
We will allow the developer to specify what cache configuration to use when caching tweets. Open the TwitterAccountBehavior
class and make the following modifications to its setup()
method:
public function setup($model, $config = array()) {
parent::setup($model, $config);
$this->settings[$model->alias] = array_merge(array(
'field' => 'twitter',
'cache' => 'default'
), $config);
}
While editing the TwitterAccountBehavior
class, make the following modifications to its afterFind()
method:
public function afterFind($model, $results, $primary) { $rows = parent::afterFind($model, $results, $primary); if (!is_null($rows)) { $results = $rows; } if (!empty($this->settings[$model->alias]['tweets'])) { $field = $this->settings[$model->alias]['field']; $count = is_int($this->settings[$model->alias]['tweets']) ? $this->settings[$model->alias]['tweets'] : 10; $cacheConfig = $this->settings[$model->alias]['cache']; foreach($results as $i => $result) { $twitter = $result[$model->alias][$field]; $tweets = array(); if (!empty($cacheConfig)) { $tweets = Cache::read('tweets_' . $twitter, $cacheConfig); } if (empty($tweets) && !empty($result[$model->alias][$field])) { $result = $this->timeline($twitter, $count); if (!empty($result) && is_array($result)) { foreach($result as $tweet) { $tweets[] = array( 'created' => date('Y-m-d H:i:s', strtotime($tweet->created_at)), 'source' => $tweet->source, 'user' => $tweet->user->screen_name, 'text' => $tweet->text ); } } Cache::write('tweets_' . $twitter, $tweets, $cacheConfig); } $results[$i]['Tweet'] = $tweets; } } return $results; }
Finally, add the following beforeDelete
and afterDelete
callback implementations:
public function beforeDelete($model, $cascade = true) { $field = $this->settings[$model->alias]['field']; $this->settings[$model->alias]['delete'] = $model->field($field, array( $model->primaryKey => $model->id )); return parent::beforeDelete($cascade); } public function afterDelete($model) { if (!empty($this->settings[$model->alias]['delete'])) { $cacheConfig = $this->settings[$model->alias]['cache']; $twitter = $this->settings[$model->alias]['delete']; Cache::delete('tweets_' . $twitter, $cacheConfig); } return parent::afterDelete($model); }
Using beforeDelete()
we are storing the tweet that is to be deleted. If indeed the profile was deleted, the afterDelete()
method will remove its cached tweets.
3.21.169.61