Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I refactor my setup of multiple user types using the same table differently/in a more Cake way?

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:

  1. fighter
  2. referee
  3. manager

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.

like image 272
meder omuraliev Avatar asked Apr 12 '18 14:04

meder omuraliev


2 Answers

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.

like image 113
Alex Avatar answered Nov 11 '22 04:11

Alex


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.

like image 31
Derek Fulginiti Avatar answered Nov 11 '22 04:11

Derek Fulginiti