Stop ACTA

Simple hitcount behavior for CakePHP

Posted in CakePHP on 07.04.2008.

While developing Neutrino, I've decided to implement a simple hitcount behavior. So here it is.

For those few who don't know, behaviors are "extensions" for models. In other words, they are reusable model logic. They help you keep your code DRY, easily maintainable and portable. In this case, we're creating a hit counter behavior, hitcount for short.

When I first began writing this behavior, I was a bit scared because I didn't have any idea where to start from. Then I've looked at some other behaviors and the Cake core itself and decided this was as easy as anything else written in Cake.

My initial version wasn't really as good, and I needed some help. Luckily for me, I did get some help.

But, let us take a look at the behavior itself.

To begin, create the file

/app/models/behaviors/hitcount.php

and type in the following:

class HitcountBehavior extends ModelBehavior
{
    var $name = 'Hitcount';
    var $__defaultOptions = array('keyField' => 'id', 'hitField' => 'hitcount');
    var $mapMethods = array('/hit/' => 'hit');

    function setup(&$Model, $settings = array())
    {
        if (!isset($this->settings[$Model->alias]))
        {
            if (empty($settings))
            {
                $this->settings[$Model->alias] = $this->__defaultOptions;
            }
            else if (is_array($settings))
            {
                $this->settings[$Model->alias] = array_merge($this->__defaultOptions, $settings);
            }
        }
    }

    function hit(&$Model, $key = null, $settings = null)
    {
        $_settings = array();

        if (isset($this->settings[$Model->alias]))
            $_settings = $this->settings[$Model->alias];

        if (is_array($settings))
            $_settings = array_merge($_settings, $settings);

        if (!isset($_settings['hitField']) || !isset($_settings['keyField']))
        {
            $this->log('Miscofigured hitcount behavior!', LOG_WARNING);
            return;
        }

        if (!$Model->hasField($_settings['keyField']))
            return;

        if (!$Model->hasField($_settings['hitField']))
            return;

        if (empty($key))
            $key = $Model->id;

        if (empty($key))
        {
            $this->log('Invalid call to hitcount behavior!', LOG_WARNING);
            return;
        }

        $Model->updateAll
            (
                array ($_settings['hitField'] => $Model->alias.'.'.$_settings['hitField'].' + 1'),
                array ($Model->alias.'.'.$_settings['keyField'] => $key)
            );
    }
}

And now for some naughty bits.

1. $mapMethods bit

var $mapMethods = array('/hit/' => 'hit');

This is a little something I've learned from a presentation about behaviors by Mariano Inglesias, called "Behaviors - Making Your Models Behave". Apparently, by using the $mapMethods variable, you can create magic methods, just like the model does with findBy*.

This means that you can call the hit method of this behavior by calling the $Model->hit(...) which will be mapped to $Model->Hitcount->hit(...) Pretty handy, right?

2. UpdateAll bit

$Model->updateAll
    (
        array ($_settings['hitField'] => $Model->alias.'.'.$_settings['hitField'].' + 1'),
        array ($Model->alias.'.'.$_settings['keyField'] => $key)
    );

As mentioned in previously mentioned discussion, updateAll(...) is used for something completely different than the regular model calls. This is a nice thing to know, I really had no clue about it. One of the tricky parts was the $Model->alias prefix, which has to be used exactly as above.

Usage

I won't elaborate on how to use this behavior, I will just show you how do I use it in Neutrino, to count article reads and download count.

Article example:

// use it in model
class Article extends AppModel
{
    var $actsAs = array('Hitcount');
}

// call it in controller
class ArticlesController extends AppController
{
    var $name = 'Articles';

    function view($slug = null)
    {
        // snip..

        // separate rss stats, with custom field
        if (isset($this->passedArgs['from']) && $this->passedArgs['from'] == 'rss')
            $this->Article->hit($article['Article']['id'], array('hitField' => 'hitcount_rss'));
        // regular hitcount
        $this->Article->hit($article['Article']['id']);

        // snip..
    }
}

Download example:

// use it in model, with default custom hitField
class Download extends AppModel
{
    var $name = 'Download';
    var $actsAs =
        array
        (
            'Hitcount' => array('hitField' => 'downloaded')
        );
}

// call it in controller
class DownloadsController extends AppController
{
    var $name = 'Downloads';

    function get($slug = null)
    {
        // snip..
        $this->Downloads->hit($download['Download']['id']);
        // snip..
    }
}

You can download the behaviour here: Hitcount behavior for CakePHP

I hope you find it useful in some way! If you find any bugs or have an advice, I'd be glad to hear it!

Happy baking!

Article comments — View · Add


Page 1 of 2
1 · 2

bwilt :: 31.05.2008 22:56:16
Thanks for the article, been thinking of implementing something of this nature for a while. I like how you passed a parameter to get rss specific hits.
lecterror :: 02.06.2008 02:18:12
Named params are my favorites ;-) I like their readability, they kind-of replace the old style URL params, i.e. ?stuff=value becomes /stuff:value.

Thanks for commenting, appreciate it!
Khaled :: 19.04.2009 03:57:51
Thanks, used it and it works smoothly, try to contact Cake's team to include it in next Cake releases
lecterror :: 19.04.2009 08:39:01
Heh, that flatters me, but I don't think so :)
Ultimate One :: 30.06.2009 07:04:02
Hi,
great article - thanks.
Useful tip for reference if you see some strange errors when trying this out...

Remember the behaviour will need <?php and ?> around the source code provided for hitcount.php

Other than that - thank you for this great work!

UltimateOne