We have moved our forum to GitHub Discussions. For questions about Phalcon v3/v4/v5 you can visit here and for Phalcon v6 here.

Best approach to allow model access

How do you handle this situation:

  1. You have a user that work in a task
  2. Task is an model in your project
  3. You have another model with relation to Task: TaskPermission that has task_id and user_id

Of course you can always query manualy database (detailed steps below - sorry for any typo! I can't validate code right now):

$task = Task::findFirstById($id);
foreach($task->getTaskPermission() as $permission){
  $allow = ($this->session->auth->user_id == $permission->getUserId())?true:false;
}

Or extend a method

public static function findFirstById($id){
  $task = parent::findFirstById($id);
  $permission = Permission::find([
    'task_id = :tid: AND user_id = :uid:',[
      'tid' => $task->getId(),
      'uid' => DI::get('session')->auth->user_id
    ]);
    if($permission) {
      return $task;
    }else{
      return false; //or throw an exception
    }
}

There are a thousand of different approaches. What is yours? Anyone using behavior? I am really curious about it.



33.8k

That looks like a M:N relationship, isn't it? I just create another model file for that type of tables and work with it. About knowing if an user has a permission to work on a task, you don't need to extend the method (if you know the task'sid, and the user'sid, as you show, you don't need to search previously for any task/user):

$clausule = 'task_id = :task_id: AND user_id = :user_id:';
$bind = [
    'task_id' => $taskId,
    'user_id' => $userId
];
$bindTypes = [
    Phalcon\Db\Column::BIND_PARAM_INT,
    Phalcon\Db\Column::BIND_PARAM_INT
];

if (TaskPermission::findFirst([$clausule, $bind, $bindTypes])) {
    // Do stuff.
} else {
    // Do other stuff.
}

Well, maybe you could create a new static method on the new model, hasPermission($taskId, $userId), that returns the result of the IF (if you need to repeat that sequence in other part of the program):

public static boolean hasPermission($taskId, $userId) {
    return (TaskPermission::findFirst([
            'task_id = :task_id: AND user_id = :user_id:',
             [ 'task_id' => $taskId, 'user_id' => $userId ],
             [ Phalcon\Db\Column::BIND_PARAM_INT,  Phalcon\Db\Column::BIND_PARAM_INT ]
         ] != false);
}

I was thinking about using afterFetch inside a behavior. Them I attach this behavior to models I want to respect this relation.What do you think?



33.8k

Why you should use custom behaviors with afterFetch inside? I didn't work with then (even didn't know of), but after reading the docs I see it innecessary (unless you are doing other specifical things).

You just need to know if an user has permission over a task. TaskPermission reflects that logic: if an user has permission, it will appear a row with his id an the task's id.

But maybe I'm not getting the whole idea.

We are talking about Tasks. What if I add this Permission check in another model called Projects? With the same logic, but instead of task_id use project_id ? The whole idea here is to think about a reusable code, that can be used in many models.



33.8k
edited Mar '15

With the same logic, but instead of task_id use project_id?

You said it, the logic is the same. The only thing that may change is the column to search in. Maybe you could do the following.

Use a base abstract MyModel class. This'll have common methods for all the models (so all of then'll extend it). Include inside of it the hasPermission with a few changes:

protected boolean hasPermission($id, $userId) {
    $clausule = '';
    $model = get_called_class();

    switch($model) {
        case "Task":
            $clausule = 'task_id;
            break;
        case "Project":
            $clausule = 'project_id;
            break;
    }

    return ($model::findFirst([
            $clausule . ' = :id: AND user_id = :user_id:',
             [ 'id' => $id, 'user_id' => $userId ],
             [ Phalcon\Db\Column::BIND_PARAM_INT,  Phalcon\Db\Column::BIND_PARAM_INT ]
         ] != false);
}

So, whenever you need permissions on another model, just add it to the switch. This is what I normally do when I've redundant code on some models.

Good job! Looks much better now.

What if...

class SuperModel extends \Phalcon\Mvc\Model 
{
  protected function setPermissionFiel($field)
  {
    $this->permission_field = $field;
    return $this;
  }

  public static function hasPermission($id = null, $userId = null) 
  {
    $model = get_called_class();

    if(is_null($userid) $userid = 1; //Get user from session
    if(is_null($id) AND $this->id) $id = $this->id;

    if($this->permission_field)
    {
          return ($model::findFirst([
            $this->permission_field . ' = :id: AND user_id = :user_id:',
             [ 'id' => $id, 'user_id' => $userId ],
             [ Phalcon\Db\Column::BIND_PARAM_INT,  Phalcon\Db\Column::BIND_PARAM_INT ]
         ] != false);
    }else{
      throw new Exception('Model does not have valid permission field');
    }
  }
}

And

class Task extends SuperModel 
{
    public function initialize()
    {
      $this->setPermissionField('task_id');
    }
}

Then, both should work and you don't have to modify your SuperModel:

$task = Task::FindFirstById(1000);
$task->hasPermission();
#or 
$task = Task::hasPermission(1000);


33.8k

SuperModel looks good, but I would make this changes:

  1. Make the class abstract => We don't want anyone to create an instance of the class (I should add it as a plugin).
  2. Modify hasPermission => I should first do the check of permission_field, so you don't have to do adittional work if it is null.
  3. Change permission_field => This should be an alternative to using a variable for the field. You can make an abstract function in SuperModel (so inherited models have the obligation of adding it) that directly returns the field, so you can skip using a var and declaring a setter for it in the parent model.
public boolean hasPermission($id = null, $userId = null) {
    if (!is_null($this->permission_field)) {
      $model = get_called_class();

      if (is_null($userId)) {
          //Get user from session
          $userId = 1;
      }
      if(is_null($id) && $this->id) {
          $id = $this->id;
      }

        return ($model::findFirst([
            $field . ' = :id: AND user_id = :user_id:',
            [ 'id' => $id, 'user_id' => $userId ],
            [ Phalcon\Db\Column::BIND_PARAM_INT,  Phalcon\Db\Column::BIND_PARAM_INT ]
        ] != false);
    } else {
        throw new Exception('Model does not have valid permission field');
    }
}