I have a my_users
table and I now need to refactor it by deleting the role
column in favor of supporting multiple roles per user. So for instance, the 3 user types I'm dealing with are:
So the table setup looks like this:
Users.php
use CakeDC\Users\Model\Table\UsersTable as BaseUsersTable;
class UsersTable extends BaseUsersTable
{
public function initialize(array $config)
{
parent::initialize($config);
$this->setEntityClass('Users\Model\Entity\User');
$this->belongsToMany('UserRoles', [
'through' => 'users_user_roles'
]);
$this->hasMany('UsersUserRoles', [
'className' => 'Users.UsersUserRoles',
'foreignKey' => 'user_id',
'saveStrategy' => 'replace',
]);
}
public function findRole(Query $query, array $options)
{
return $query
->innerJoinWith('UserRoles', function($q) use ($options) {
return $q->where(['role_key' => $options['role_key']]);
});
}
}
MyUsersTable.php
class MyUsersTable extends Table
{
public function initialize(array $config)
{
parent::initialize($config);
$this->setTable('my_users');
$this->setPrimaryKey('user_id');
$this->belongsTo('Users', [
'className' => 'Users.Users',
'foreignKey' => 'user_id',
]);
}
/**
* @param Cake\Event\Event $event
* @param Cake\ORM\Query $query
* @param ArrayObject $options
*/
public function beforeFind(Event $event, Query $query, \ArrayObject $options)
{
// set the role
if (defined(static::class.'::ROLE') && mb_strlen(static::ROLE) > 0) {
$role = static::ROLE;
// set user conditions
$query->innerJoinWith('Users', function($query) use ($role) {
return $query->find('role', [
'role_key' => $role,
]);
});
}
}
/**
* @param \Cake\ORM\Query $query
* @param array $options
*/
public function findByRegistrationCode(Query $query, array $options): Query
{
$query->where([
$this->aliasField('registration_no') => $options['registration_no']
]);
return $query;
}
}
FightersTable.php
use MyUsers\Model\Table\MyUsers;
class FightersTable extends MyUsersTable
{
const ROLE = 'fighter';
public function initialize(array $config)
{
parent::initialize($config);
$this->setEntityClass('Fighters\Model\Entity\Fighter');
}
/**
* @param Validator $validator
* @return Validator $validator
*/
public function validationDefault(Validator $validator): Validator
{
$validator = parent::validationDefault($validator);
$validator->allowEmpty('field');
}
}
RefereesTable.php
and ManagersTable.php
are similar to FightersTable
but have their own validation rules and may have their own special entity virtual properties and what not.
Question: Is there a more cake way of structuring this, or more specifically an alternate way to doing the beforeFind
to distinguish the role? Had the requirement for role
stayed 1:1 with users, I might have possibly done something like this:
$this->belongsTo('Fighters', [
'conditions' => [
'role' => 'fighter'
],
'foreignKey' => 'user_id',
'className' => 'MyUsers',
]);
I'd appreciate any insight into restructuring this.
You can define belongsTo
association to use findByRole
finder:
$this->belongsTo('Fighters', [
'foreignKey' => 'user_id',
'className' => 'MyUsers',
'finder' => ['byRole' => ['role' => 'fighter']]
]);
Of course, you'll have to define the finder in MyUsers
:
public function findByRole(Query $query, \ArrayObject $options)
{
$role = $options['role'];
// set user conditions
$query->innerJoinWith('Users', function($query) use ($role) {
return $query->find('role', [
'role_key' => $role,
]);
});
}
}
Also I'd extract beforeFind
logic into RoleBehavior
or MultiRoleBehavior
behavior, i.e.:
src/Model/Behavior/RoleBehavior.php
:
<?php
namespace App\Model\Behavior;
use Cake\ORM\Behavior;
use Cake\ORM\Query;
use Cake\Event\Event;
/**
* Role-specific behavior
*/
class RoleBehavior extends Behavior
{
/**
* @var array multiple roles support
*/
protected $_defaultConfig = [
'roles' => []
];
public function initialize(array $config)
{
parent::initialize($config);
if (isset($config['roles'])) {
$this->config('roles', $config['roles'], false /* override, not merge*/);
}
}
public function beforeFind(Event $event, Query $query, \ArrayObject $options, $primary) {
// set user conditions
$query->innerJoinWith('Users', function($query) use ($roles) {
return $query->find('role', [
'role_key' => $roles,
]);
});
}
}
It uses multiple roles, but you can simply change it for single role if you need. Here are some other thing to do - probably extract $query->innerJoinWith('Users'...
into a separate method in the behavior, and call it in MyUsers::findByRole(...)
...
Next, you can attach this behavior directly to extended classes, just replace static ROLE
with behavior config:
use MyUsers\Model\Table\MyUsers;
class FightersTable extends MyUsersTable
{
public function initialize(array $config)
{
parent::initialize($config);
$this->setEntityClass('Fighters\Model\Entity\Fighter');
$this->addBehavior('Role', [ 'roles' => ['fighter']]);
}
}
Or you can manage your role-specific logic right by attaching behavior to MyUsers
table (i.e. from a controller):
$this->MyUsers->addBehavior('Role', ['roles' => ['manager']])
You also can change the behavior setup on the fly:
$this->MyUsers->behaviors()->get('Role')->config([
'roles' => ['manager'],
]);
I hope it helps.
What I've typically done/seen in a situation where you have multiple roles is use one table to store the roles (let's call it UserRoles), and another lookup table (lets call this UserRoleAssignments) that connects the user to a specific role. So for instance your myUsersTable would contain a hasMany relationship to UserRoleAssignments. This would eliminate the need for the role field as well as the separate tables for each role. Your findRole() method would look something like this:
public function findRole(Query $query, array $options)
{
return $query
->innerJoinWith('UserRoleAssignments', function($q) use ($options) {
return $q->where(['user_id' => $options['user_id']]);
});
}
You could chain a first()
call to this if you are only expecting that a user will have one role; however, this setup is nice because if a User can have multiple roles than you can simply chain a toArray()
call and return an array of roles that belong to the user.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With