Best practices to handle multilpe roles and capabilities?

Answering my own question, if your system is not performance-critical, you can use a plugin for it, such as User Role Editor:

roles and capabilities best practices wordpress

It supports multi-site, multiple roles per user and a lot more.

Optionally, If I were to write it:

If I would like to have total control over it and write my custom code, I would handle roles management in a plugin, to keep it organized and logically separated from the rest of the code.

The main advantage of this structure right now is to give a dedicated folder to Roles, keeping it organized, and providing a basic OOP structure to work with. This could be extended with methods to help assigning custom post types capabilities for instance, etc.

plugins/custom-roles-manager/custom-roles-manager.php

<?php
/*
 * Plugin Name:  Custom Roles Manager
 * Description:  This plugin handles roles capabilities.
 * Requires PHP: 5.3
 */
defined('ABSPATH') OR exit;

/** Runs only when the plugin is activated */
register_activation_hook(__FILE__, 'custom_roles_manager_activate');
function custom_roles_manager_activate()
{
    require_once(__DIR__ . '/RoleInterface.php');

    // This is the folder we add roles on
    $roles_folder = __DIR__ . '/roles';

    // If it throws anything, WordPress will automatically disable the plugin and show the notice
    $roles = crm_get_roles_in_folder($roles_folder);

    // Give developer a chance to filter the roles for any reason
    $roles = apply_filters('crm_get_roles', $roles);

    foreach ($roles as $role) {
        // Filter might add $role that does not implement RoleInterface
        if ($role instanceof RoleInterface) {
            $role->add();
        } else {
            throw new Exception('All roles handled by Custom Roles Manager must implement RoleInterface interface.');
        }
    }
}

/**
 * Returns an array of concrete classes that implements RoleInterface
 * in a given directory
 * 
 * @param $dir
 *
 * @return array
 * @throws ReflectionException
 */
function crm_get_roles_in_folder($dir)
{
    $roles = array();

    $roles_files = new RegexIterator(
        new DirectoryIterator($dir),
        '/\.php$/',
        RegexIterator::MATCH
    );

    foreach ($roles_files as $role_file) {
        include_once($role_file->getPathname());
        $r = new ReflectionClass($role_file->getBasename('.php'));
        if ($r->implementsInterface(RoleInterface::class)) {
            $roles[] = $r->newInstance();
        }
    }

    return $roles;
}

plugins/custom-roles-manager/RoleInterface.php

<?php

interface RoleInterface
{
    /**
     * Function that has to be implemented for
     * each concrete class to actually add the role.
     */
    public function add();
}

plugins/custom-roles-manager/roles/PublicRelationsRole.php

<?php

class PublicRelationsRole implements RoleInterface
{
    public function add()
    {
        add_role(
            'public_relations',
            __('Public Relations'),
            [
                'read',
                'edit_post',
                'publish_post',
                'delete_post',
            ]
        );
    }
}

Example scenario: Creating “Some Other Role”

  1. Create file wp-content/plugins/custom-roles-manager/Roles/SomeOtherRole.php
  2. Make it implement RoleInterface and create add method in it, which calls add_role function from WordPress.
  3. Deactivate and activate plugin.

With time, your complex role system will have a dedicated folder for them, helping keep it organized, using OOP, which gives a few options on code re-use over time.