Автор Тема: [MVC] CI, ACL и контекст на изгледа  (Прочетена 1884 пъти)

VladSun

  • Moderator
  • Напреднали
  • *****
  • Публикации: 2166
    • Профил
Малко объркано звучи заглавието, но искам да споделя как се справям с типа на изгледа (прим. HTML, PDF, JSON и т.н.) и достъпа до определени действия за отделните потребители.

Става въпрос за CodeIgniter рамката.

Първо създавам Контролер клас, който обслужва йерархията от класове:
Код
GeSHi (PHP):
  1. <?php
  2.  
  3. /**
  4.  * Remap enabled Controller class
  5.  *
  6.  *
  7.  * @package ExtCI
  8.  * @subpackage Libraries
  9.  * @category Libraries
  10.  * @author VladSun
  11.  *
  12.  */
  13.  
  14. abstract class Remap_Controller extends Controller
  15. {
  16. public function __construct()
  17. {
  18. parent::__construct();
  19. }
  20.  
  21. protected function preRemap($method, $arguments) {}
  22. protected function postRemap($method, $arguments) {}
  23.  
  24. protected function getRemapping($method, $arguments)
  25. {
  26. $remapping = new stdClass();
  27. $remapping->method = $method;
  28. $remapping->arguments = $arguments;
  29.  
  30. return $remapping;
  31. }
  32.  
  33. public function _remap($method)
  34. {
  35. $arguments = array_slice($this->uri->segment_array(), 3);
  36. $this->preRemap($method, $arguments);
  37.  
  38. $remapping = $this->getRemapping($method, $arguments);
  39.  
  40. if ( ! in_array(strtolower($remapping->method), array_map('strtolower', get_class_methods($this))))
  41. {
  42. show_404("{".get_class($this)."}/{$remapping->method}");
  43. }
  44. else
  45. {
  46. call_user_func_array(array($this, $remapping->method),  $remapping->arguments);
  47. $this->postRemap($remapping->method, $remapping->arguments);
  48. }
  49. }
  50.  
  51. }

Класът е прост и няма много нужда от обяснения - създава getRemapping метод за всички наследници, които трябва да го извикват след методът от родителския клас.

Второ - управление на изгледите:

Код
GeSHi (PHP):
  1. <?php
  2.  
  3. /**
  4.  * View context enabled Controller class
  5.  *
  6.  *
  7.  * @package ExtCI
  8.  * @subpackage Libraries
  9.  * @category Libraries
  10.  * @author VladSun
  11.  *
  12.  * @property string $requestedViewMode
  13.  * @property string $defaultViewMode
  14.  *
  15.  */
  16.  
  17. abstract class ViewContext_Controller extends Remap_Controller
  18. {
  19. protected $requestedViewMode = null;
  20. protected $defaultViewMode = 'json';
  21.  
  22. public function __construct()
  23. {
  24. parent::__construct();
  25. }
  26.  
  27. public function view($view, $data = null, $viewMode = null)
  28. {
  29. $this->load->view($this->getViewMode($viewMode).'/'.$view, $data);
  30. }
  31.  
  32. protected function setDefaultViewMode($viewMode)
  33. {
  34. $this->defaultViewMode = $viewMode;
  35. }
  36.  
  37. protected function getViewMode($viewMode = null)
  38. {
  39. if ($viewMode)
  40. return $viewMode;
  41.  
  42. if ($this->requestedViewMode)
  43. return $this->requestedViewMode;
  44.  
  45. if ($this->defaultViewMode)
  46. return $this->defaultViewMode;
  47.  
  48. return 'html';
  49. }
  50.  
  51. protected function getRemapping($method, $arguments)
  52. {
  53. $remapping = parent::getRemapping($method, $arguments);
  54.  
  55. $mv = explode('.', $remapping->method);
  56.  
  57. if (isset($mv[1]))
  58. {
  59. if (preg_match('#^[a-zA-Z]+$#',$mv[1]))
  60. $this->requestedViewMode = $mv[1];
  61. else
  62. throw new Exception ('Грешно зададен формат.');
  63. }
  64. else
  65. {
  66. $this->requestedViewMode = null;
  67. }
  68.  
  69. $remapping->method = $mv[0];
  70.  
  71. return $remapping;
  72. }
  73. }

Класът определя въз основа на URL-a, какъв изглед трябва да се зареди. Прим. ако е изивкан http://example.com/user/get.html ще се зареди изглед с HTML формат. Ако е извикан http://example.com/user/get.pdf ще се зареди PDF формата. Това става с извикването на методa view(), който зарежда изгледа от директорията съответстваща на разширението. Прим.:

http://example.com/user/get.html => $this->view('user/get') => /html/user/get.php

Създаваме абстрактен клас за контрол на достъпа (и документиране на действията):

Код
GeSHi (PHP):
  1. /**
  2.  * Authenthification check enabled Controller class
  3.  *
  4.  * Checks for proper authenthification if $noRoleCheck is false.
  5.  *
  6.  * @package ExtCI
  7.  * @subpackage Libraries
  8.  * @category Libraries
  9.  * @author VladSun
  10.  *
  11.  * @property bool $permissionCheckRequired
  12.  * @property bool $loggingRequired
  13.  *
  14.  */
  15.  
  16. abstract class BaseAuth_Controller extends ViewContext_Controller
  17. {
  18. const WITHOUT_PERMISSION_CHECK = false;
  19. const WITH_PERMISSION_CHECK = true;
  20. const WITHOUT_LOGGING = false;
  21. const WITH_LOGGING = true;
  22.  
  23. private $loggingRequired = true;
  24. private $permissionCheckRequired = true;
  25.  
  26. public function __construct()
  27. {
  28. parent::__construct();
  29. }
  30.  
  31. protected function suspendLogging()
  32. {
  33. $this->loggingRequired = false;
  34. }
  35.  
  36. protected function resumeLogging()
  37. {
  38. $this->loggingRequired = true;
  39. }
  40.  
  41. protected function suspendPermissionCheck()
  42. {
  43. $this->permissionCheckRequired = false;
  44. }
  45.  
  46. protected function resumePermissionCheck()
  47. {
  48. $this->permissionCheckRequired = true;
  49. }
  50.  
  51. protected function getRemapping($method, $arguments)
  52. {
  53. $remapping = parent::getRemapping($method, $arguments);
  54.  
  55. if (!$this->isPertmitted($remapping->method, $remapping->arguments))
  56. {
  57. $remapping->arguments = array($remapping->method, $remapping->arguments);
  58. $remapping->method = 'notPermitted';
  59. }
  60.  
  61. return $remapping;
  62. }
  63.  
  64. protected function postRemap($method, $arguments)
  65. {
  66. if ($this->loggingRequired)
  67. {
  68. $this->prepareToLog($method, $arguments);
  69. }
  70. }
  71.  
  72. protected function hasPermission($method, $arguments)
  73. {
  74. return false;
  75. }
  76.  
  77. protected function notPermitted($method, $arguments)
  78. {
  79. exit();
  80. }
  81.  
  82. private function prepareToLog($method, $data)
  83. {
  84. $this->log(get_class($this), $method, $data);
  85. }
  86.  
  87. protected function log($module, $action, $data)
  88. {
  89. }
  90.  
  91. private function isPertmitted($method, $arguments)
  92. {
  93. if ($this->permissionCheckRequired === self::WITHOUT_PERMISSION_CHECK)
  94. return true;
  95.  
  96. return $this->hasPermission($method, $arguments);
  97. }
  98. }

За простота ще покажа класовете необходими за една единствена роля (т.е. идентифициран и неидентифициран потребител):

Код
GeSHi (PHP):
  1. <?php
  2.  
  3. /**
  4.  * Authenthification check enabled Controller class
  5.  *
  6.  * Checks for proper authenthification if $noRoleCheck is false.
  7.  *
  8.  * @package ExtCI
  9.  * @subpackage Libraries
  10.  * @category Libraries
  11.  * @author VladSun
  12.  *
  13.  * @property CurrentUser_Model $currentUser
  14.  * @property Logger_Model $logger
  15.  *
  16.  */
  17.  
  18. abstract class Auth_Controller extends BaseAuth_Controller
  19. {
  20. function __construct()
  21. {
  22. parent::__construct();
  23. $this->load->model('application/CurrentUser_Model', 'currentUser');
  24. $this->load->model('application/Logger_Model', 'logger');
  25.  
  26. $this->logger->setCurrentUser($this->currentUser);
  27. }
  28.  
  29. protected function hasPermission($method, $arguments)
  30. {
  31. return $this->currentUser->isLogged;
  32. }
  33.  
  34. protected function notPermitted($method, $arguments)
  35. {
  36. Error::register('Не сте автентифицирали пред системата.');
  37. $this->view('error/notLogged');
  38. }
  39.  
  40. protected function log($module, $action, $data)
  41. {
  42. if (!$data)
  43. $data = array();
  44. elseif (!is_array($data))
  45. $data = array($data);
  46.  
  47. $this->logger->log($module, $action, array_merge($data, $_POST));
  48. }
  49. }

Моделът за текущия потребител:
Код
GeSHi (PHP):
  1. class CurrentUser_Model extends Model
  2. {
  3. public $isLogged = false;
  4. public $userRecord = null;
  5.  
  6. public function __construct()
  7. {
  8. parent::__construct();
  9.  
  10. $this->isLogged = empty($_SESSION) ? false : (empty($_SESSION['user_logged']) ? false : true);
  11.  
  12. if ($this->isLogged)
  13. {
  14. if (!$this->userRecord = Doctrine::getTable('RUser')->find($_SESSION['user_id']))
  15. {
  16. unset($_SESSION['user_id']);
  17. unset($_SESSION['user_logged']);
  18. $_SESSION = array();
  19. unset($this->userRecord);
  20. $this->userRecord = null;
  21. $this->isLogged = false;
  22. }
  23. }
  24. }
  25.  
  26. public function getId()
  27. {
  28. return $this->userRecord ? $this->userRecord->id : 0;
  29. }
  30.  
  31. public function getUsername()
  32. {
  33. return $this->userRecord ? $this->userRecord->username : null;
  34. }
  35.  
  36. public function doLogin()
  37. {
  38. $this->load->library('validation');
  39.  
  40. $rules = array();
  41. $fields = array();
  42.  
  43. $rules['user_name'] = "xss_clean|trim|required|min_length[3]|max_length[20]|htmlspecialchars";
  44. $rules['user_password'] = "xss_clean|trim|required|min_length[3]|max_length[20]|htmlspecialchars";
  45. $this->validation->set_rules($rules);
  46.  
  47. $fields['user_name'] = 'Потребител';
  48. $fields['user_password'] = 'Парола';
  49. $this->validation->set_fields($fields);
  50.  
  51. if ($this->isLogged)
  52. Error::register('Вече сте влезли в системата.');
  53. elseif ($this->validation->run() == false)
  54. Error::register($this->validation->error_string);
  55. elseif (!$this->login($this->validation))
  56. Error::register('Грешни потребителско име и/или парола.');
  57. else
  58. {
  59. $this->isLogged = true;
  60. $_SESSION['user_id'] = $this->userRecord->id;
  61. $_SESSION['user_logged'] = true;
  62. }
  63. }
  64.  
  65. public function doLogout()
  66. {
  67. unset($_SESSION['user_id']);
  68. unset($_SESSION['user_logged']);
  69. $_SESSION = array();
  70.  
  71. $this->isLogged = false;
  72. }
  73.  
  74. protected function login($data)
  75. {
  76. $this->userRecord = Doctrine::getTable('RUser')->findOneByUsername($data->user_name);
  77.  
  78. if ($this->userRecord)
  79. {
  80. return ($this->userRecord->userpass === $data->userpass && $this->userRecord->active);
  81. }
  82. return false;
  83. }
  84.  
  85. }


Моделът за документиране:
Код
GeSHi (PHP):
  1. <?php
  2.  
  3. class Logger_Model extends Model
  4. {
  5. private $currentUserModel = null;
  6.  
  7. public function __construct()
  8. {
  9. parent::__construct();
  10. }
  11.  
  12. public function setCurrentUser($currentUser)
  13. {
  14. $this->currentUserModel = $currentUser;
  15. }
  16.  
  17. public function log($module, $action, $data)
  18. {
  19. $record = new RAdministratorLog();
  20.  
  21. $record->FK_user_id = $this->currentUserModel->getId();
  22.  
  23. if (!$record->FK_user_id)
  24. return;
  25.  
  26. $record->module = $module;
  27. $record->action = $action;
  28. $record->data = json_encode($data);
  29.  
  30. $record->save();
  31. }
  32.  
  33. }

Изполвал съм Doctrine за ORM и DAL. По ваш избор можете да замените съответния кода за работа с ДБ.

Контролерът за login/logout:

Код
GeSHi (PHP):
  1. <?php
  2.  
  3. class CurrentUser extends Auth_Controller
  4. {
  5. function __construct()
  6. {
  7. parent::__construct();
  8.  
  9. $this->suspendPermissionCheck();
  10. }
  11.  
  12. public function login()
  13. {
  14. $this->currentUser->doLogin();
  15. $this->view('currentUser/login');
  16. }
  17.  
  18. public function logout()
  19. {
  20. $this->currentUser->doLogout();
  21. $this->view('currentUser/logout');
  22. }
  23. }
  24.  

Всеки друг Контролер, изискващ идентификация трябва да е наследник на Auth_Controller.

Вижда се, че използвам статичен Error клас за съхраняване на съобщенията за грешки - пак променяйте по ваш избор.
« Последна редакция: Aug 24, 2010, 08:53 от VladSun »
Активен

KISS Principle ( Keep-It-Short-and-Simple )
http://openfmi.net/projects/flattc/
Има 10 вида хора на този свят - разбиращи двоичния код и тези, които не го разбират :P

VladSun

  • Moderator
  • Напреднали
  • *****
  • Публикации: 2166
    • Профил
Re: CI, ACL и контекст на изгледа
« Отговор #1 -: Aug 24, 2010, 00:46 »
Ще прощавате, ако има грешки при зареждането на изгледите, но при мен всичко това са AJAX заявки и JSON отговори ... а и ми се спи вече :)
Активен

KISS Principle ( Keep-It-Short-and-Simple )
http://openfmi.net/projects/flattc/
Има 10 вида хора на този свят - разбиращи двоичния код и тези, които не го разбират :P

vm13

  • Напреднали
  • *****
  • Публикации: 43
  • Distribution: Ubuntu 10.04
  • Window Manager: Gnome
    • Профил
Re: [MVC] CI, ACL и контекст на изгледа
« Отговор #2 -: Aug 24, 2010, 10:29 »
Поздравления за добре направения пример!

Не съм работил с CI, а само със ZF и ми се струва, че там Authentication/Authorization са реализирани по-близо до идеите на ООР.

Цитат
Всеки друг Контролер, изискващ идентификация трябва да е наследник на Auth_Controller.

Това означава, че например класа Документ трябва да е наследник на класа Auth, което не е много логично. Не би трябвало да има такава връзка. Нямам точен пример, но например класа Фактура е логично да бъде наследник на класа Документ, но не и на класа Достъп.

При Zend Framework за тази цел могат да се използват плъгини, като съответния плъгин се изпълнява преди да се изпълни кода на изискания контролер. Например ако имам Me_Controller_Plugin_Auth и отделно имам заявка за /invoice/id/2/format/pdf, преди да бъде показана изисканата фактура, ще се изпълни кода от плъгина за достъп който ще определи дали потребителя има право на достъп до този ресурс или не. По този начин, контролера се грижи само и единствено за това за което е създаден - управление на ресурса, а не на правилата за достъп до ресурса, което е нещо съвсем различно.
Активен

VladSun

  • Moderator
  • Напреднали
  • *****
  • Публикации: 2166
    • Профил
Re: [MVC] CI, ACL и контекст на изгледа
« Отговор #3 -: Aug 24, 2010, 22:28 »
Поздравления за добре направения пример!

Мерси :)

Цитат
Всеки друг Контролер, изискващ идентификация трябва да е наследник на Auth_Controller.

Това означава, че например класа Документ трябва да е наследник на класа Auth, което не е много логично. Не би трябвало да има такава връзка. Нямам точен пример, но например класа Фактура е логично да бъде наследник на класа Документ, но не и на класа Достъп.

Ми не съвсем, твоят Document е наследник на Controller, а тук би бил наследник на Auth_Controller - който е пак Controller и то от базов тип (абстрактен клас).

При Zend Framework за тази цел могат да се използват плъгини, като съответния плъгин се изпълнява преди да се изпълни кода на изискания контролер. Например ако имам Me_Controller_Plugin_Auth и отделно имам заявка за /invoice/id/2/format/pdf, преди да бъде показана изисканата фактура, ще се изпълни кода от плъгина за достъп който ще определи дали потребителя има право на достъп до този ресурс или не.

public function _remap($method) методът прави приблизително същото. В CI той се намира в базовия Contrller клас и се извиква от Front Controller-a.

По този начин, контролера се грижи само и единствено за това за което е създаден - управление на ресурса, а не на правилата за достъп до ресурса, което е нещо съвсем различно.

Почти съм съгласен с тези съждения. Бих могъл да използвам предложените от архитектурата на CI възможности за hooks и remap и да зареждам plugin-и, които да променят веригата на изпълнението, но това би означавало добавяне на елементи към архитектурата (което само по себе си не е лошо), а не добавяне на възможности чрез имплементация.

В случая исках да покажа прост начин за управление  на изглед, контрол на достъпа и документиране на действията. Реализирано е решение, което позволява наследниците на тези базови класове да са "тънки" контролери с добавена напълно прозрачна за тях функционалност. Всичко това без хакове на CI-a :)

Това с което не съм съгласен е, че тъй като контролът за достъп и документирането на действията са cross-cutting concerns (дайте добър превод), то това означава, че и трите елемента на MVC триадата ще (или поне е възможно) съдържат някаква част от имплементацията им. Обикновено това се решава чрез използването на AOP, а не на OOP, т.е. не бих казал, че подходът на ZF e по- ООП ;)
Активен

KISS Principle ( Keep-It-Short-and-Simple )
http://openfmi.net/projects/flattc/
Има 10 вида хора на този свят - разбиращи двоичния код и тези, които не го разбират :P

VladSun

  • Moderator
  • Напреднали
  • *****
  • Публикации: 2166
    • Профил
Re: [MVC] CI, ACL и контекст на изгледа
« Отговор #4 -: Aug 24, 2010, 22:35 »
Всъщност бих добавил, че според мен ACL не трябва да има нищо общо с бизнес слоя ;)
Имам няколко опита за решаване на този проблем - даже имах няколко теми във форума за това.
Активен

KISS Principle ( Keep-It-Short-and-Simple )
http://openfmi.net/projects/flattc/
Има 10 вида хора на този свят - разбиращи двоичния код и тези, които не го разбират :P