Second Draft Proposal
tl;dr Instead of bit based action determination use an array of class const strings. Let a user backend return a list of supported actions (can be changed during runtime). Use a user backend proxy to have a single place in the code where supported actions are checked (and exceptions thrown). Let code that deals with users only have access to the user backend proxy.
changes from Draft 1: concrete UserBackends only implements a subset of Interfaces. There is an single-method interface for each action. UserProxy implements all of them.
Code
Action Definitions (same as draft 1)
class UserBackendAction {
public const CREATE_USER = 'create user';
public const DELETE_USER = 'delete user';
public const GET_EMAIL_ADDRESS = 'get e-mail address';
// TODO: add other actions
}
This is very similar to OCP\UserInterface\Backend
but the capabilities are just const attributes not const attributes with number values and then an additional list with names.
New UserBackend Interface (no action signatures at all)
namespace OCP;
interface IUserBackend {
public function getBackendName(): string;
public function getSupportedActions() : iterable;
}
Contains getSupportedActions()
but no actual action interfaces. They are implemented indivudually.
Single-method example Interface IUserBackendGetEMailAddress
interface IUserBackendGetEMailAddress {
public function getEMailAddress(string $username) : string;
}
Single-method example Interface IUserBackendCreateUser
interface IUserBackendCreateUser {
public function createUser(string $username, string $password): void;
// or return string if you want to return the uid to signal that it was successful
}
Single-method Interfaces (the rest)
...
Example Backend Implementation (only the action interfaces it supports at most)
class ExampleUserBackend implements IUserBackend, IUserBackendGetEMailAddress, IUserBackendCreateUser {
public function getBackendName(): string {
return "example Backend";
}
private $supportedActions = array(
UserBackendAction::GET_EMAIL_ADDRESS,
UserBackendAction::CREATE_USER,
); // default values for illustration or could be empty
// and only set in constructor
public function __construct() {
$enabled_actions = readConfig(); // readConfig is the app dev's implementation
foreach ($enabled_actions as $action) {
array_push($supportedActions, $action);
}
}
public function getSupportedActions(): array {
return $this->supportedActions;
}
public function getEmailAdress(string $username): string {
// actually retrieve the email address
}
public function createUser(string $username, string $password): void {
// actually create user
}
}
This should not be accessible from UserManager but only the Proxy (see below). This is to enforce the action checks and throwing of exceptions.
User Backend Proxy
class BackendProxy implements \OCP\IUserBackend, /**and ALL possbile Interfaces**/ {
private $actualBackend;
public function __construct(\OCP\IUserBackend $backend) {
$this->actualBackend = $backend;
}
public function getBackendName() {
return $this->actualBackend->getBackendName();
}
public function supportsAction($action): bool {
return in_array($action, $this->actualBackend->supportedActions());
}
//for each user backend actions that nextcloud supports
// as in `UserBackendAction`, here for UserBackendAction::GET_EMAIL_ADDRESS
public function getEmailAddress($username) {
if ($this->supportsAction(UserBackendAction::GET_EMAIL_ADDRESS)) {
return $this->actualBackend->getEmailAdress($username);
}
else {
throw new UnsupportedUserBackendActionException("This user backend does not support GET_EMAIL_ADDRESS");
}
}
//TODO: add methods for all other actions
}
The âuglyâ part is now here, where UserBackendProxy
must implement all action interfaces, because this class has to handle all of them potentially.
Usage in User.php
try {
$this->backendProxy->setEMailAddress("user@example.com");
}
catch (UnsupportedUserBackendActionException $e) {
// handle exception
}
Considerations/Summary
- concrete user backend implmementations can set an upper limit to the action they support by only implementing a subset of action interfaces
- the Proxy must implement all action interfaces, because it must be able to handle all action method calls
- I understand that itâs not the most straight forward solution, but I still think the ability to have runtime handling of available actions is worth it and âthere is no free lunchâ. Having compile time type safety and contracts with interfaces is great, but it just doesnât fit a web application well where everything is decided on the go.
- I also looked into
eval()
which could create custom classes during runtime but it is supposed to be slow and maybe introduce some security issues (although by app developers) which have access to everything anyway.
I am âdevelopingâ this in my head. Maybe I should do a POC to be able to see possible improvements or show problems that I did not think about.