Creating A New Workflow: Approval Workflow

One of the ideas behind the Istarel Workshop Application Framework is workflows that represent common ways to interact with data. As I work on various projects, new workflows often suggest themselves. In most cases, these potential workflows are really pretty specific to an application. Sometimes, however, a workflow presents itself that could have fairly wide applicability. When that happens, I tend to build the workflow class and add it to the framework.

Approval Workflow

One common workflow on the web is a managed approval process. That means the workflow will consist of four modes: List, View, Approve, and Reject. (Specific implementations might be a lot more complex, but I tend to go with the simplest concept for code intended to be part of the framework.)

Listing: /workflow/IWApprovalWorkflow.php

class IWApprovalWorkflow extends IWListWorkflow
{
    function initModeDiagram()
    {
        $this->mode[IWListMode::ID]    = new IWListMode($this);   // default
        $this->mode[IWViewMode::ID]    = new IWViewMode($this);
        $this->mode[IWApproveMode::ID] = new IWApproveMode($this);
        $this->mode[IWRejectMode::ID]  = new IWRejectMode($this);
    }

    function viewAction()
    {
        return new IWViewAction($this->mode(IWViewMode::ID)->url());
    }

    function approveAction()
    {
        return IWAction::createAction($this->mode(IWApproveMode::ID)->url(), '_do_approve', 'Approve', 'shouldAllowApproval');
    }

    function rejectAction()
    {
        return IWAction::createAction($this->mode(IWRejectMode::ID)->url(), '_do_reject', 'Reject', 'shouldAllowRejection');
    }

    function actions()
    {
        return array(
            IWViewMode::ID    => $this->viewAction(),
            IWApproveMode::ID => $this->approveAction(),
            IWRejectMode::ID  => $this->rejectAction()
        );
    }

    function commands() { }

    function standardDestination()
    {
        // Initialize delegates (which update navigation elements)
        $this->mode(IWMode::MODE_LIST)->initController();
        $this->mode(IWMode::MODE_LIST)->initDelegates();

        // Ensure that the list mode is the transitional destination
        $this->url()->setParameter($this->modeIdentifier(), IWMode::MODE_LIST);

        // Return the navigation-updated url
        return $this->url();
    }

    function approveDestination()
    {
        return $this->standardDestination();
    }

    function rejectDestination()
    {
        return $this->standardDestination();
    }
}

Because the central idea is a list of objects which can be approved or rejected, it seemed a natural fit to extend the already-existing IWListWorkflow. For any workflow, you define the "modes" for the workflow. Many modes are already supported in the framework, but this workflow introduces two new ones: IWApproveMode and IWRejectMode. With the modes defined, one critical part of the workflow class is defining the transitions for modes that end in a redirect. In this case, both approvals and rejections will return the user to the list mode.

The actions() method defines the links (actions) attached to each object in the list. Some established modes have their own Action objects (like the one associated with the view mode), but a dynamic action can be created where its metadata is presented via parameters to the creation method. The arguments when creating a dynamic action are: the base url for the destination mode, a name for the column in the list (by convention, they begin with an underscore), a label for the action (link), and a permission method which allows the application module to determine if an action should be allowed on an object-by-object basis.

Approve Mode

All that happens in the approve mode is that the application module is given a chance to define what approval means before returning the user to the list mode.

However, by letting the application module dictate the action that takes place, it gives the developer a lot of freedom for how that should happen. For example, it could be as simple as changing the state of an object. Or, It could send the user to a complex form for additional processing (which would preclude the default behavior or redirecting to the list mode).

Listing: /mode/IWApproveMode.php

class IWApproveMode extends IWMode
{
    const ID = 51;

    function modeId() { return IWApproveMode::ID; }

    function run()
    {
        $this->workflow()->module()->approve();
        header('Location: ' . $this->destination());
    }

    function destinationMethod() { return 'approveDestination'; }
}

Reject Mode

All that happens in the reject mode is that the application module is given a chance to define what rejection means before returning the user to the list mode.

Listing: /mode/IWRejectMode.php

class IWRejectMode extends IWMode
{
    const ID = 52;

    function modeId() { return IWRejectMode::ID; }

    function run()
    {
        $this->workflow()->module()->reject();
        header('Location: ' . $this->destination());
    }

    function destinationMethod() { return 'rejectDestination'; }
}

Obviously, approval workflows can be far more complex that what this base workflow represents. But, because both the approve and reject mode defer to the application module, almost any behavior is possible. Or, if a particular application has exacting needs (and especially if those needs occur for many model objects), you could create a custom workflow.