Version 8, last updated by pagchen at November 07, 2011 11:58 UTC

This wiki has been updated for version 0.7-alpha4

1. Redirecting after controller actions using command chains

Below is an example of one of the most powerful features in Nooku: using command chains to manipulate controller actions.

The goal is to redirect back to the referrer after an item was saved. For example, imagine we are in a view that shows a list of captains, with their boats. When you click a captain, you get an edit form. When you click save, we are redirected to the captains view. So far so good, this is standard behavior.

Now we click on a boat in the captains view. Again, we get an edit form for this boat, but when we save it, we are redirected to the boats view. This is confusing for the user, he expects to return to the captains view.

To solve this, we could write a lot of code in our controller, but that isn't very pretty. Instead, let's create a command first:

//administrator/components/com_harbour/commands/redirect.php

<?php
class HarbourCommandRedirect extends KCommandHandler
{
    // this method will execute before _actionRead in any controller we attach this command to
    public function _controllerBeforeRead(KCommandContext $context)
    {
        // the notifier is the controller, the identifier will be something
        // like com://admin/harbour.controller.boats or .captains
        $identifier = (string) $context->caller->identifier;
        $referrer = (string) KRequest::referrer();

       //we store the referrer in the session, using the identifier to namespace it
       KRequest::set('session.harbour.redirect.'.$identifier, $referrer);
       return true;
   }

    protected function _controllerAfterSave(KCommandContext $context)
    {
        return $this->_redirect($context);
    }    

    protected function _controllerAfterCancel(KCommendContext $context)
    {
        return $this->_redirect($context);
    }

    protected function _redirect($context)     
    {
        // again we get the identifier from the notifiyng controller
        $identifier = (string) $context->caller->identifier;
        if( $url = KRequest::get('session.atlas.redirect.'.$identifier, 'string') ) {
        // tell the controller to redirect to this url
             $context->caller->setRedirect($url);
        }
        return true;
    }
}

Next we need to attach this command to all controllers that we want to give the redirect behavior to. We could do this in the controller's constructor, or in a system plugin, or anywhere we please. The principle is always the same:

// get the command object
$command = KService::get('com://admin/harbour.command.redirect');
// get the controller and enqueue the command 
KService::get('com://admin/harbour.controller.captain')
    ->getCommandChain()->enqueue($command);

If you want to do this for all controllers in your extension, you can put this code in the dispatcher:

// /adminsitrator/components/com_harbour/dispatcher.php
class HarbourDispatcher extends KDispatcherAbstract
{
    public function getController(array $options = array())     
    {
        $command = KService::get('com://admin/atlas.command.redirect');

        $controller = parent::_getController($options);
        $controller->getCommandChain()->enqueue($command);

        return $controller;
    }
}

This is just a simple example of how you can attach behaviors to existing code at runtime. There are many benefits: the whole redirecting behavior is contained in one file, that can not only be reused throughout your component, but also in any other component. It's a good example of the OOP principle 'Favour composition over inheritance'. Other interesting uses for command chains could be to add logging to some controller actions, send emails, check permissions, ...

2. Return an empty rowset

When there is a large dataset, the initial page of rows is not always required until the user has filtered the rowset to show meaningful results.

To do this, you can check that the state is empty and then reset the model before display. This can be achieved by:

protected function _actionBrowse(KCommandContext $context)
{
    //check if the user state is empty
    if($this->getModel()->getState()->isEmpty(array('limit','direction'))){
        $this->getModel()->reset(false);
    }
    return parent::_actionBrowse($context);
}

The array in IsEmpty() contains the state fields that are to be ignored in the IsEmpty() check.

We are passing FALSE to the reset function to reset the model states to NULL, by default this parameter is TRUE in which case the model states will be reset using their defaults.

4. Forcing a Browse in HMVC context

Sometimes you might end up with the controller doing a Read instead of a Browse when used in HMVC context. For example, consider the following code:

<?= KService::get('com://site/clothes.controller.product')
    ->id(json_decode($favorite->fav_ids, true))
    ->set('gender', '')
    ->layout('products')
    ->display();
?>

Here is the problem: setting the id state variable (second line) makes the model's state unique. Let's explain that a bit. The controller passes on the state information to the model. When display() is called, the controller asks the model whether its state is unique, i.e. it uniquely identifies a single record in the database. If the model state is unique, the controller performs a Read for that unique item. If the model state is not unique, it performs a browse. Since the id state variable is magic (it maps to your table's auto_increment unique ID field), passing it a value will cause the model state to be unique, therefore enforcing a Read to be executed.

All is not lost. All you have to do is a bit of manipulation. First, append a non-unique state variable called "ids" to your model:

public function __construct(KConfig $config)
{
    parent::__construct($config);

    $this->_state
        ->insert('ids'  , 'array');
}

Then, just make your model's _buildQueryWhere() method to take into account the ids state variable. Finally, change your HMVC call to read:

<?= KService::get('com://site/clothes.controller.product')
    ->ids(json_decode($favorite->fav_ids, true))
    ->set('gender', '')
    ->layout('products')
    ->display();
?>

Now the model state is not unique any more and Nooku Framework will perform a Browse instead of Read.