Creating the Custom Category Object

Prepare Controller

When creating our permission category, we first ensure that our package controller autoloads its classes in the recommended way, by adding

$pkgAutoloaderMapCoreExtensions = true;

to our controller.php

Create the Assignment Database Table

As part of installing our package, we're going to need to create a database table to store the assignments of Products to Permission Keys to Permission Access Objects. Assuming our Product object's primary key is productID, such a table would look like

CREATE TABLE `SuperStoreProductAssignments` (
  `productID` int(10) unsigned NOT NULL DEFAULT '0',
  `pkID` int(10) unsigned NOT NULL DEFAULT '0',
  `paID` int(10) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`cID`,`pkID`,`paID`),
  KEY `paID` (`paID`,`pkID`),
  KEY `pkID` (`pkID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

How you install this database is up to you. Full documentation on creating database tables as part of a package can be found in the packages documentation

Install the Permission Category and Keys in the Controller

Next, we'll actually need to ensure that the permission category we want is installed in the controller's install() method, and install the relevant permissions we've described above. First, add the Category use statement to the header of the controller:

use Concrete\Core\Permission\Category\Category;
use Concrete\Core\Permission\Key\Key;
use Concrete\Core\Permission\Access\Entity\Type as AccessEntityType;

Now, install the category, and the permission keys

public function install()
{
    $pkg = parent::install();
    $category = Category::getByHandle('product');
    if (!is_object($category)) {
        $category = Category::add('product', $pkg);
        $category->associateAccessEntityType(AccessEntityType::getByHandle('group'));
        $category->associateAccessEntityType(AccessEntityType::getByHandle('user'));
    }

    $permission = Key::getByHandle('view_product');
    if (!is_object($permission)) {
        $view = Key::add('product', 'view_product', 'View Product', '', false, false, $pkg);
    }
    $permission = Key::getByHandle('edit_product');
    if (!is_object($permission)) {
        $view = Key::add('product', 'edit_product', 'Edit Product', '', false, false, $pkg);
    }
    $permission = Key::getByHandle('delete_product');
    if (!is_object($permission)) {
        $view = Key::add('product', 'delete_product', 'Delete Product', '', false, false, $pkg);
    }

}

This should be pretty self-explanatory: we're adding a completely new permission category, with the handle "product." Remember the handle – it's important. We then associate the access entity types that should be option to assign permissions through. Then, we add our three permissions, taking care to add it to our new permission category.

Implement Concrete\Core\Permission\ObjectInterface

In order to add permissions functionality to SuperStore\Product\Product object, we need ensure that the Product object implements the permissions object interface. This tells Concrete CMS how to load the permission response and category information for this particular object.

namespace SuperStore\Product
use Concrete\Core\Permission\ObjectInterface;

class Products implements ObjectInterface
{

}

Now we have to actually implement the methods specified by the interface:

  • getPermissionResponseClassName()
  • getPermissionAssignmentClassName()
  • getPermissionObjectKeyCategoryHandle()
  • getPermissionObjectIdentifier()

Here's how that might look:

class Products implements ObjectInterface
{
    public function getPermissionObjectIdentifier()
    {
        return $this->productID;
    }

    public function getPermissionObjectKeyCategoryHandle()
    {
        return 'product';
    }

    public function getPermissionResponseClassName()
    {
        return '\\Concrete\\Package\\SuperStore\\Permission\\Response\\ProductResponse';
    }

    public function getPermissionAssignmentClassName()
    {
        return '\\Concrete\\Package\\SuperStore\\Permission\\Assignment\\ProductAssignment';
    }
}

The first method returns the field of the object that corresponds to its unique identifier. The second returns the category handle added in the controller. The next two methods return a fully qualified class name pointing to the object's Permission Response and Permission Assignment classes.

Create the Classes

While the interface only mentions two classes, you actually need to create five classes as part of creating a custom permission category:

Don't worry: only one of them actually needs to have anything in it!

Key

<?php
namespace Concrete\Package\SuperStore\Permission\Key;
use Concrete\Core\Permission\Key\Key;
class ProductKey extends Key {

}

Access

<?php
namespace Concrete\Package\SuperStore\Permission\Access;
use Concrete\Core\Permission\Access\Access;
class ProductAccess extends Access {

}

Access List Item

<?php
namespace Concrete\Package\SuperStore\Permission\Access\ListItem;
use Concrete\Core\Permission\Access\ListItem\ListItem;
class ProductListItem extends ListItem {

}

Response

<?php
namespace Concrete\Package\SuperStore\Permission\Response;
use Concrete\Core\Permission\Response\Response;
class ProductResponse extends Response {

}

Assignment

Finally, we get to the one class that needs something in it. That's because the assignment class actually has to handle querying our custom SuperStoreProductAssignments database for the various access and key objects. If you are not needing to set permissions directly on an instance of an item (similar to task permissions), this class may be left blank as well.

<?php
namespace Concrete\Package\SuperStore\Permission\Assignment;

use Concrete\Core\Permission\Acess\Access;
use Database;

class ProductAssignment extends Assignment
{

    public function getPermissionAccessObject()
    {
        $db = Database::connection();
        $r = $db->GetOne('select paID from SuperStoreProductAssignments where productID = ? and pkID = ?', array(
            $this->getPermissionObject()->getProductID(),
            $this->pk->getPermissionKeyID()
        ));
        return Access::getByID($r, $this->pk);
    }

    public function clearPermissionAssignment()
    {
        $db = Database::connection();
        $db->Execute('update SuperStoreProductAssignments set paID = 0 where pkID = ? and productID = ?',
            array($this->pk->getPermissionKeyID(), $this->getPermissionObject()->getWorkgetProductIDflowID()));
    }

    public function assignPermissionAccess(Access $pa)
    {
        $db = Database::connection();
        $db->Replace('SuperStoreProductAssignments', array(
            'productID' => $this->getPermissionObject()->getProductID(),
            'paID' => $pa->getPermissionAccessID(),
            'pkID' => $this->pk->getPermissionKeyID()
        ), array('productID', 'pkID'), true);
        $pa->markAsInUse();
    }

    public function getPermissionKeyToolsURL($task = false)
    {
        return parent::getPermissionKeyToolsURL($task) . '&productID=' . $this->getPermissionObject()->getProductID();
    }

}

There's a fair amount happening here, but it should be relatively easy to understand. getPermissionAccessObject() is responsible for querying our custom database table for a permission access object based on the product ID of our permission object, and the permission key that's saved. clearPermissionAssignment() clears an entire permission assignment. assignPermissionAccess() assigns an Access object to a combination of a key and permission object.

That's it! At this point you have a completely functioning permission system! Unfortunately, there's currently no way to actually assign permissions in any kind of graphical way! That's coming next – along with an explanation of the only confusing bit, getPermissionKeyToolsURL(). This is actually a method that's used in conjunction with the standard permission user interface code. This code makes use of tools, which is the pre-Concrete 5.7 way of handling AJAX requests. (Version 5.7 of Concrete contains an entire dedicated routing and MVC framework for these types of AJAX requests.) The permissions subsystem hasn't been updated to take advantage of this, however; it still uses tools. How is this going to work, since packages in 5.7 actually can't use tools? Read on for more information.