Using callbacks in behaviors

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.

Getting ready

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

How to do it...

  1. Create a class named 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;
    }
    }
    ?>
    
  2. Now that we have created the behavior with its 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;
    }
    
  3. Let us now attach the behavior to the 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'
    )
    );
    }
    ?>
    
  4. Just like the recipe Creating a custom validation rule, entering a nonexistant Twitter account should display the error message for the twitter field shown in the following screenshot:
    How to do it...
  5. Let us now use other callbacks to get a certain number of tweets for each profile after a find operation is performed. Add the following 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;
    }
    
  6. Edit the 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:

How to do it...

How it works...

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.

There's more...

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.

Note

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.

See also

  • Adding multiple validation rules
  • Create a custom validation rule
  • Using behaviors to add new fields for saving
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.169.61