Creating Custom Workflow Requests

Once you've added a permission that has workflow enabled, you'll actually need to modify your underlying code protected by that permission, and change that code to use a workflow request. That sounds complicated; an example might make this easier to understand. Let's say this is the controller code in our add-on that handles checking our delete permission, and deleting products:

$p = Product::getByID($productID);
$pp = new Permissions($p);
if ($pp->canDeleteProduct()) {
    $p->delete();
}

Pretty simple, right? We're instantiating a product object, getting a permissions Checker object populated with that Product object, and checking the "delete_product" permission for that particular Product. If it's true, we run the delete() method on the product itself, and remove the product from the system.

In order to make this permission workflowable, we're going to have to swap out the $p->delete() method, with a workflow request. That's how workflow works: when attaching workflow to a permission, your permission is now protected by another level. You give full access to everyone you want to be able to initiate the workflow, and then you use the workflow to determine who gets to complete it. So, if you want Administrators to approve the delete action, but Editors to be able to initiate it, you'd give "Editors" full access to the "Delete Product" permission, and you'd give "Administrators" access to approve the "Standard Workflow", and you'd apply the "Standard Workflow" to the "Delete Product" permission. If there weren't any workflow, the Editors would simply be able to delete the product wholesale; but once workflow is applied to the permission, another level of protection will be in place.

Of course, that's only if you modify the code above to use a workflow request, rather than running $p->delete(). If you don't modify the code, the Editors group will be able to delete the product. So let's modify the code. To do this, we need to create a custom "Delete Product" workflow request class in our package. This will be found at Concrete\Package\SuperStore\Workflow\Request\DeleteProductRequest and in the filesystem at packages/super_store/src/Concrete/Workflow/Request/DeleteProductRequest.php. It will look like this:

<?php
namespace Concrete\Package\SuperStore\Workflow\Request;

use Config;
use \SuperStore\Product\Product;
use \Concrete\Core\Workflow\Workflow;
use \Concrete\Core\Workflow\Description as WorkflowDescription;
use \Concrete\Core\Permission\Key\Key;
use \Concrete\Core\Workflow\Progress\Progress as WorkflowProgress;
use \Concrete\Core\Workflow\Progress\Action\Action as WorkflowProgressAction;
use \Concrete\Core\Workflow\Progress\Response as WorkflowProgressResponse;

class DeleteProductRequest extends Request
{

    protected $productID;

    public function __construct()
    {
        $pk = Key::getByHandle('delete_product');
        parent::__construct($pk);
    }

    public function setRequestedProduct(Product $product)
    {
        $this->productID = $product->getProductID();
    }

    public function getWorkflowRequestDescriptionObject()
    {
        $d = new WorkflowDescription();
        $p = Product::getByID($this->productID);
        $link = $p->getLinkToProdut();
        $d->setEmailDescription(t("\"%s\" has been marked for deletion. View the product here: %s.",
            $p->getProductName(), $link));
        $d->setInContextDescription(t("This product has been marked for deletion. ", $item));
        $d->setDescription(t("<a href=\"%s\">%s</a> has been marked for deletion. ", $link, $p->getProductName()));
        $d->setShortStatus(t("Pending Delete"));
        return $d;
    }

    public function getWorkflowRequestStyleClass()
    {
        return 'danger';
    }

    public function getWorkflowRequestApproveButtonClass()
    {
        return 'btn-danger';
    }

    public function getWorkflowRequestApproveButtonInnerButtonRightHTML()
    {
        return '<i class="fa fa-trash-o"></i>';
    }

    public function getWorkflowRequestApproveButtonText()
    {
        return t('Approve Delete');
    }

    public function addWorkflowProgress(Workflow $wf)
    {
        $pwp = WorkflowProgress::add($wf, $this);
        $r = $pwp->start();
        $pwp->setWorkflowProgressResponseObject($r);
        return $pwp;
    }

    public function trigger()
    {
        $p = Product::getByID($this->productID);
        $pk = Key::getByID($this->pkID);
        $pk->setPermissionObject($p);
        return parent::triggerRequest($pk);
    }

    public function cancel(WorkflowProgress $wp)
    {
        $p = Product::getByID($this->productID);
        $wpr = new WorkflowProgressResponse();
        $wpr->setWorkflowProgressResponseURL(\URL::to('/dashboard/super_store/products'));
        return $wpr;
    }

    public function approve(WorkflowProgress $wp)
    {
        $p = Product::getByID($this->productID);
        $p->delete();
        $wpr = new WorkflowProgressResponse();
        $wpr->setWorkflowProgressResponseURL(\URL::to('/dashboard/super_store/products'));
        return $wpr;
    }

}

While there's a fair amount going on, this class is pretty self-explanatory. The constructor is responsible for hooking into the relevant permission key. We have product-specific setters, and we have some styling classes that are necessary for ensuring that the buttons generated by this workflow request action convey the necessary severity (e.g: btn-danger). Finally, we have our approve() method, which you'll notice is now what holds the product deletion code.

Now, all that's left to do is swap out our previous code, with code that calls the workflow response instead. Instead of this:

$p = Product::getByID($productID);
$pp = new Permissions($p);
if ($pp->canDeleteProduct()) {
    $p->delete();
}

Put this in your controller:

$p = Product::getByID($productID);
$pp = new Permissions($p);
if ($pp->canDeleteProduct()) {
    $u = new User();
    $pkr = new DeleteProductWorkflowPrequest();
    $pkr->setRequestedProduct($p);
    $pkr->setRequesterUserID($u->getUserID());
    $response = $pkr->trigger();
    if ($response instanceof \Concrete\Core\Workflow\Progress\Response) {
        // we only get this response if we have skipped workflows and jumped straight in to an approve() step.
        // This means the product is officially deleted now.
    } else {
        // Otherwise, workflow triggered. Show the proper workflow message.
    }
}

That's it! You've successfully made the "Delete Product" permission workflowable.