Tracking navigation history of a user

By Jan Boonen (boonen)
Many times it can be very useful to track the navigation history of a user. Especially if you want to redirect the user to a page where he came from after some action (e.g. login). With this HistoryComponent, it's extremely easy to handle such actions.
Store the component code in: controllers/components/history.php.

Component Class:

Download code <?php 
/**
 * Maximum size of the array containing the user navigation history
 */
define('STUDIOSIPAK_MAX_HISTORY'10);
/*
 * HistoryComponent: User navigation history
 * @author: Studio Sipak
 * @website: http://webdesign.janenanneriet.nl
 * @license: MIT
 * @version: 0.1
 * */
class HistoryComponent extends Object
{
    var 
$data = array();
    var 
$started false;
    var 
$controller true;

    function 
startup(&$controller) {
        
// This test will prevent it from running twice.
        
if(!$this->started) {
            
$this->started true;
            
$this->controller $controller;
            
$this->data $controller->Session->read('User.history');
            if(
$controller->params['bare'] == 0) {
                
$this->_addUrl($controller->params);
            }
            
$controller->Session->write('User.history'$this->data);
        }
    }

    function 
goBack($step 1) {
        
$pos count($this->data) - $step 1;
        
$this->controller->redirect($this->data[$pos]);
        exit();
    }

    function 
show() {
        return 
$this->data;
    }

    function 
_addUrl($params) {
        
count($params['url']) ? $url '/'.$params['url']['url'] : $url '/';
        if(
count($this->data) == STUDIOSIPAK_MAX_HISTORY) {
            
$this->_deleteUrl();
        }
        
$this->data[] = $url;
    }

    function 
_deleteUrl($position 0) {
        if(
$position == 0) {
            
array_shift($this->data);
        }
        else {
            
array_splice($this->data$position1);
        }
    }

}
?>


Usage


The component will automatically be loaded by the Controller. In your controller you can use the functions goBack($position) and show(). The function goBack() will send the user x many pages back in his navigation history. If you don't specify a number, it will be the last page. The function show() gives an overview of the navigation history.
Download code
class YourController extends AppController
{
    function doSomething() {
        ...
        // Redirect user to previous page
        $this->History->goBack();
    }

}


Expansion


This version is just my first one, so it is might not be complete yet. It is not difficult to expand the component for more advanced usage. Comments regarding more functionality and bugs are welcome.

 

Comments 526

CakePHP Team Comments Author Comments
 

Comment

1 Nice component

I took a look and it's working for me except 2 things:
1. in my authentication Session variable 'User' is for storing User record and check if user is logged in is

if(!is_array($this->Session->read('User')))

well with your way this directly made User as array and ofcourse my Auth was broken. It is fine - my auth is not so complex and I could modify -it's mine, but third party components is difficult to touch - especially if you plan to update them regulary.
So, my first suggestion is: parametrize Session variable:
like:

$this->data = $controller->Session->read($this->variable.'.history');

and

$controller->Session->write($this->variable.'.history', $this->data);


and ofcourse $this->variable could be defaulted to 'User' in Class vars.

2. You don't track the same url. It's like when you press refresh of the page. In this case on 2-3 Refreshes of the page and you couldn't know which step you need to point for return. I'd suggest this fix:


function _addUrl($params) {
        count($params['url']) ? $url = '/'.$params['url']['url'] : $url = '/';
        if($url != $this->data[count($this->data)-1]){
            if(count($this->data) == STUDIOSIPAK_MAX_HISTORY) {
                $this->_deleteUrl();
            }
            $this->data[] = $url;
        }
    }

briefly it check if the current url is the same as the last one and if so, don't insert it into the array. It's possible to be improved further more because now when I access:
http://myserver/controller
and I submit let's say login which in action has /controller/
in the session var will be stored:
[0]=>'/controller'
[1]->'/controller/'
which again is duplication. If you make this as well I think this is component would be very usefull especially in Login actions where the user will be returned on the page which he requested before auth.

Hope this make sense.
Posted Sep 11, 2007 by Nik Chankov
 

Comment

2 Separate stacks for admin and site histories saving url params and page titles

Nice component, I should wrap my functionality up in one I guess. Here's how I do it. Feel free to comment.

In my AppCotnroller

<?php

class AppController extends Controller {

  var 
$savePageHistoryCms true;
  var 
$savePageHistorySite true;

  function 
beforeRender () {
    if(
$this->savePageHistoryCms && defined('CAKE_ADMIN') && isset($this->params[CAKE_ADMIN]) && $this->params[CAKE_ADMIN] = CAKE_ADMIN) {
      
$this->__savePageHistory('cms');
    } elseif(
$this->savePageHistorySite) {
      
$this->__savePageHistory('site');
    }
  }
  
/**
   * Handles adding or removing of page histories to session stack
   * Works for both site and cms page histories.
   *
   * @param string $path Prefix for session array, normally 'site' or 'cms'
   */
  
function __savePageHistory($path) {
    if(!empty(
$this->here)
    && 
$this->params['bare']==0) {
      
$url $this->here;
      
$params $this->params['url'];
      
$query = array();
      if(
count($params)>1) foreach ($params as $param => $value) if($param!='url'$query[] = $param.'='.$value//Adds query string params to url
      
if(count($query)) $url .= '?'.implode('&',$query);
      
$title strlen($this->pageTitle) > $this->pageTitle 'Previous page';
      if(
$this->Session->valid()
      && 
$this->Session->check($path.'_page_history')) {
        
$page_history $this->Session->read($path.'_page_history');
        if(isset(
$page_history[0]['url'])) {
          if(isset(
$page_history[1]['url']) && $url == $page_history[1]['url']) {
            
array_shift($page_history);
          } elseif(
$url != $page_history[0]['url']) {
            
array_unshift($page_history,array('url'=>$url,'title'=>$title));
          }
        } else {
          
$page_history = array(0=>array('url'=>$url,'title'=>$title));
        }
      } else {
        
$page_history = array(0=>array('url'=>$url,'title'=>$title));
      }
      
$this->Session->write($path.'_page_history',$page_history);
    }
  }
}


If you don't separate out the admin and site histories, and someone has the back end open in one browser tab, the front end open in another, and between loading an edit screen in the back end and submitting the form, the user has checked something on the front end in the other tab, when they do submit the back end form they end up getting redirected to the page in the front end!

If you store the title as well, you can have back links in your views that say "Back to <Page title>" (This is why I call it in beforeRender, so controller->pageTitle is set).

By the way, apologies for the incomprehensible title for this comment - commas are invalid apparently.
Posted Sep 12, 2007 by Neil Crookes
 

Comment

3 Agree with Neil

I just login to say exactly the same thing as Neil - you should give possibility this helper to store History for different part's of the site.
I already implemented your Helper with my proposed changed, and it's working so far, but that example drive me to write this:
I went to my site, let's say http://my.dommain.com/, then I decided directly to login into admin and I add http://my.domain.com/admin/ and when I fill user and pass I was redurected to http://my.domain.com/ it is because the Helper does'nt detect the part admin. The solution is either to follow the Neil's way or to add a function which will force storing the step determined by some condition.
Posted Sep 12, 2007 by Nik Chankov
 

Question

4 Any Updates

This looks like a really promising component.

Based on feedback from Neil, have there been updates to this component?

Neil, did you make changes to fix issues you addressed?

Can you share with us?

Thanks,
Mandy.
Posted Sep 20, 2007 by Mandy Singh
 

Comment

5 Re Any Updates

Neil, did you make changes to fix issues you addressed?

I haven't made any changes to the original component posted above by boonen, but the code I posted should work on its own. If you want to wrap it up inside a component, feel free and post it back here. I'm sure many people would appreciate it.
Posted Sep 20, 2007 by Neil Crookes
 

Comment

6 Updates

In the first place I want to thank everybody for your feedback and your compliments. As said this is just a quickly built component. I will try to implement the suggested changes.

The URL things as posted by Nik, were already known. I'll definitely change these. I'll have a closer look at Neil's comments to see how these can be included. Stay tuned for my update...
Posted Sep 25, 2007 by Jan Boonen
 

Question

7 1.2 Updates

Has there been any developments with this component lately? I was hoping to use this component with the latest 1.2 RC1 code but 'bare' is deprecated so this component generates errors.

What is the 1.2 equivalent to params['bare']?
Posted Jun 9, 2008 by Hugh foster