It all started so simple, a basic competition mechanic quickly became two different mechanics with more variations in the future.  It could have quickly became a mess of code, multiple controllers each supporting different mechanics and with shared code being copied and pasted.  Very easy to go down that road, especially when the time scales are short.  A little bit of thinking and a look at what is available in the Yii framework soon led us away from this route.  By combining features we developed a flexible system that enabled code re-use, these were:

  • CAction - you extend this class to create stand-alone classes that are responsible for one action and can be re-used in many controllers.
  • CBehaviour - provides support for the mixin pattern, allowing the alteration of behaviour at runtime.

 

We created one action class for each of the steps that can happen across every mechanic, this would allow us to switch which actions are available based on the mechanic that is currently live.  These actions fire events before and after the action that allow logic that are mechanic specific to be defined in the code responsible for co-ordinating one mechanic.  The actions would look like:

class EnterCompetition extends CompetitionActionBase
{
    public function run()
    {
        if ( ! $this->onBeforeAction() )
        {
            return false;
        }

        // Action logic

        $this->onAfterAction( $eventParams );
    }
}

CompetitionActionBase defines the before and after action event handling code, this is boiler plate code and in Yii's documentation.

Then we created a behaviour for each mechanic, the behaviour has the sole responsibility of providing logic related to the mechanic and defining which actions are available.  This was achieved by defining a CController::actions() compatible method that the controller will call in its implementation.  This method defines what CActions are loaded in and what urlPath they will respond to.  If the CAction has properties these can also be defined here, such as event handlers.  The behaviour would look like:

class BasicCompetitionBehaviour extends CBehavior
{
    public function actions()
    {
        return array(
            'index' => 'alias.to.actions.CompetitionHome',
            'enter' => array(
                'class' => 'alias.to.actions.EnterCompetition',
                'afterAction' => array( $this, 'onAfterEnter' )
            ),
            'thanks' => 'alias.to.actions.CompetitionThanks'
        );
    }

    public function onAfterEnter( CEvent $event )
    {
        // Handle the post enter logic, i.e. redirect to thanks.
    }
}

The actions method returns as documented in the framework docs.  Notice the afterAction key for enter, this binds the onAfterEnter method of the behaviour as an event listener for afterAction, this code gets run after the action has done its logic.  This is how we decouple the mechanic from the action, one mechanic may just redirect to thanks while another perhaps runs some other logic and redirects to another page.  If the user had won an instant prize and you needed to take address details from them that you wouldn't take on the first step.

Finally the controller brings the logic together, it loads the correct behaviour and proxies the actions() call that the framework makes to it to the behaviour.  As actions() is defined in a parent class you can't just let the framework automatically call the behaviour as it would do for a method undefined in the controller class.  The code would look like:

class CompetitionController extends Controller
{
    public function init()
    {
        $competition = $this->getCompetitionModel();
        $this->attachBehaviour(
            'competitionBehaviour',
            array(
                'class' => $competition->behaviour
            )
        );
    }

    public function actions()
    {
        return $this->competition->actions();
    }
}

The end result being a tiny controller whose behaviour is completely defined by the type of competition the user is entering and it's all loosely-coupled.  Much better than where we could have ended up!

Making Your Search Forms Keep State

February 27, 2012 1:36 PM

We roll out so many CRUD (Create Read Update Delete) pages in the course of our projects and life for that matter.  And so often there's a list or grid view for all the existing entries and of course, there's the obligatory search form at the top.  How this should behave is probably up for debate but in my opinion the best way is that while you're in that section of the site the search data should remain.  So you filter the list, edit one of the records and then when you go back to the list the search data is still there.  If you go elsewhere and come back, well, the search data resets.

So, what's a good pattern for dealing with this requirement, a real simple way is to simply store these values in your flash data.  If you're unfamiliar with flash data, this is extremely volatile data that is only available during the current and next request.  So, we just need to set this data, with the user submitted values, when they do a search and then persist it while we remain in this section of the site.  If you're using MVC (Model View Controller) then this becomes simpler, in your action, set the data, examples here are based on development with Yii! :

public function actionIndex()
{
    $model = $user->getFlash( self::searchFlash, null, new SearchForm() );
    $form = new CForm( 'application.views.upload._forms.searchForm', $model );
    if ( isset( $_GET['SearchForm'] ) )
    {
        $model->attributes = $_GET['SearchForm'];
        Yii::app()->user->setFlash( self::searchFlash, $model );
    }

    ...

}

Then we persist this by running some code during controller initialisation:

public function init()
{
    parent::init();
    if ( Yii::app()->user->hasFlash( self::searchFlash ) )
    {
        $user->setFlash( self::searchFlash, $user->getFlash( self::searchFlash, null, false ) );
    }
}

 

Custom Yii Url Rules

January 10, 2012 9:31 PM

Recently I had the requirement to have a route that was powered by the database, the Yii docs cover this, but having the create and parse run at the same point was problematic.  The situation was that the route was just a controller and an action with search form parameters, of course the route parsed to createUrl() would be just "controller/action"; there was a normal "<controller>/<action>" rule as well.

If I wanted the createUrl() method to return the database powered slugs then the custom url rule would need to be above the generic rule.  The danger there is that if anyone entered a slug that matched an existing url this would prevent the normal page from displaying.  So I needed the createUrl() to be above the generic url but I needed the parseUrl() rule to be below it.  I had a couple of solutions, create two classes, or add an attribute that controlled behaviour, I opted for the attribute as it is a cleaner solution.

First, we create the class:

class DbUrlRule extends CBaseUrlRule
{
   public $connectionID = 'db';
   public $mode;

   public function createUrl( $manager, $route, $params, $ampersand )
   {
      if ( 'create' != $this->mode )
      {
         return false;
      }

      // Check the route and params and return the URL string if valid.
   }

   public function parseUrl( $manager, $request, $pathInfo, $rawPathInfo )
   {
      if ( 'parse' != $this->mode )
      {
         return false;
      }

      // Check the pathInfo and request to see if we match, if so set $_GET
      // variables and return route.
   }
}

Then, in our rules, we simply create two rules, same class but with different modes:

'urlManager' => array(
   ...
   'rules' => array(
      ...
      array( 'class' => 'DbUrlRule', 'mode' => 'create' ),
      '<controller>/<action>' => 'controller/action',
      array( 'class' => 'DbUrlRule', 'mode' => 'parse' ),
      ...
   )
)

And there you have it, one class that encapsulates the behaviour of the rule that can be used in multiple places in the rules.  One place to ensure that you get a nice URL if you createUrl() with the appropriate values and the parseUrl() in a place that ensures system integrity.

Holidays and Open Source

January 6, 2012 1:16 PM

I noticed over Christmas that open source projects seem to get work done when the developers are not at work.  This makes perfect sense but I'd not noticed it really until now, two projects that I use both got updates over the holidays.  K9 Mail, a mail client for Android, got updated, bugs fixed and features added that I never new I was missing.  The new feature to go to the next message when you delete is nice though, thanks for that K9 team.

The other project that got an update was the Yii Framework, a PHP Framework that brings us the good bits of Rails onto the PHP platform.  In this release are two bug fixes that I submitted patches for, take a look at the changelog, see those two entries with "mcheale" against them, those are mine.  Pretty chuffed that I've beed able to feed back into a project and that my patches were accepted.  These two bugs have been in the issue tracker for at least six months but no-one has had the time to look into them, until Christmas that is.

So there I was, Christmas Eve, and these two bugs were annoying me, rattling around my head.  So I sat down and investigated, came up with some solutions and added patch files to the issues.  So there you have it, open source, you get involved and then you find yourself putting in time when you're on holiday to make it better.  I guess it proves the pleasure/pain theory!

Site Development

December 7, 2011 4:33 PM

Well here we go then!  My site has languished with no love nor attention for many years.  To be honest I had very little use for it, nor any desire to spend time working on it.  Now, however, it's time it became something more than it was, for a start, my recent review at work said that I had to start blogging on tech related topics, so I had to get a blogging platform.  Here's the thing though, I just couldn't bring myself to just roll YAW, I'm not denying that Wordpress is a very good platform, it's just not one that I want to learn at the moment.  So instead, here we are in with a real simple CMS, build with the Yii! Framework, which I think it brilliant btw, and a shoddy looking design.

Regarding the design, I aimed for a layout that meets my requirements and I'm happy with that. I however can't do design work, I plan to try and convince my brother to give it a worthy skin!