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!

Comments

No comments yet, be the first to comment.