Jag ställdes nydligen inför ett scenario där jag ville isolera varje modul i ett ZF projekt ifrån varandra. Systemet skulle själv känna av vilka moduler som var installerade och de skulle i princip sköta sig själva. Alla javascript och andra saker som de behövde skulle de själva inkludera och initiera via sina Boostraps.
En del saker som skulle delas av hela systemet lades i /library/Core men utöver det så skulle ingen module behöva ha kännedom om någon annan modul. Detta är rellativt enkellt men då jag ändå ville att en del saker skulle se centraliserade ut för användaren så dök mitt lilla problem upp.
Låt oss tex säga att vi har en UserController i vår default modul. I denna har vi editAction som skall presentera settings för användaren som denna kan ändra. Så länge detta bara är ifrån grundobjektet för en User så är det ju inga problem alls. Men låt oss nu säg att vi även har en Messages modul som hanterar meddelanden. Användaren skall kunna göra inställningar för denna modul, men vi vill inte att användaren skall behöva gå in i Messages modulen och göra inställningarna där. Vi vill istället att alla inställningar som är kopplade till användaren, oavsett till vilken modul de hör, skall göras ifrån samma settings sida för vår användare.
Jag började med en lösning där alla formulär som hanterade en användares inställningar ärvde ifrån UserSettingsForm. Sedan skapade jag möjligheten att registrera dessa formulär på ett centralt ställe (jag la den i Zend_Registry i en array som hette user_settings_forms)’. Sedan loopade jag igenom denna lista av formulär och visade upp dem alla när man besökte /user/edit och således körde editAction i UserController.
Detta lilla koncept har jag dock vidareutvecklat och gjort mera generellt och presenterar det här nedan. Tanken är att man skall kunna ha ett centralt register där man kan lägga objekt som skall användas på ett centralt ställe. Om vi tar exemplet ovan så ser det nu ut enligt följande.
Jag utveckalr fortfarande detta koncept en del så några saker kan ändras eller behöva lite puttsning. Tex singleton hanteringen här kommer bli lite knas då flera olika Object_Registry’s kan ärva ifrån denna klass. Vi börjar med en Abstract klass som kallas Core_Object_Registry_Abstract.
1 <?php
2 /**
3 * Class that manages objects from several different modules that should be
4 * instanciated by a specific module but used by the entire system without
5 * actual knowledge about the object in question. The object is referenced by
6 * it's class name and ONLY instaciated after it has been called at least once.
7 * This is to avoid objects that aren't beeing used from beeing instanciated.
8 *
9 * @author danlil
10 *
11 */
12 abstract class Core_Object_Registry_Abstract
13 {
14 /**
15 * Array of names of objects or if the object has been requested at least once
16 * an instance of the object in question.
17 * @var array
18 */
19 private $_objects = array();
20
21 /**
22 * Instance of Core_Register_Abstract
23 * @var Core_Register_Abstract
24 */
25 static private $_instance = array();
26
27 /**
28 * Object type to accept for storrage in the registry. Can be a single string
29 * of the name of the class to accept or an array of string if several class
30 * types are to be accepted.
31 * @var mixed
32 */
33 protected $_acceptedObjectClasses = null;
34
35 /**
36 * Get instace of class
37 * @return Core_Register_Abstract
38 */
39 static public function getInstance() {
40
41 $calledClassName = get_called_class();
42
43 if(self::$_instance instanceof Core_Object_Registry_Abstract) {
44 return self::$_instance;
45 }
46 else {
47 $object = new $calledClassName;
48 self::$_instance = $object;
49 return $object;
50 }
51 }
52
53 public function get($name) {
54 if(empty($this->_objects[$name])) {
55 throw new Core_Object_Registry_Exception();
56 }
57
58 $object = $this->_objects[$name];
59
60 // If the object is already instantiated we return it
61 if(is_object($object)) {
62 return $object;
63 }
64 // Instantiate the class and return it
65 else {
66 if(class_exists($this->_objects[$name])) {
67 $object = new $this->_objects[$name];
68 $this->_objects[$name] = $object;
69 return $object;
70 }
71 else {
72 throw new Core_Object_Registry_Exception();
73 }
74 }
75 }
76
77 public function getAll() {
78 $objects = array();
79
80 foreach($this->_objects as $objectName => $objectClassName) {
81 $objects[] = $this->get($objectName);
82 }
83 return $objects;
84 }
85
86 /**
87 * Enter description here ...
88 */
89 public function __construct() {
90 }
91
92 /**
93 * Enter description here ...
94 * @param string $objectClassName
95 * @param string $objectName
96 */
97 public function register($objectClass, $objectName) {
98 if($this->_isValidObject($objectClass)) {
99 $this->_objects[$objectName] = $objectClass;
100 }
101 else {
102 throw new Core_Object_Registry_Exception();
103 }
104 }
105
106 /**
107 *
108 * Check if the given object class is in the array of accepted object classes.
109 * $objectClass can be either a stringname of an object class or an instance
110 * of the object of the class type. It may also be an array consisting of
111 * a multitude of the above in any order.
112 * @param mixed $objectClass
113 */
114 private function _isValidObject($objectClass) {
115 if(is_array($this->_acceptedObjectClasses)) {
116 foreach($this->_acceptedObjectClasses as $acceptedObjectClass) {
117 if(is_string($objectClass)) {
118 if($objectClass == $objectClass) {
119 return true;
120 }
121 }
122 else if($objectClass instanceof $acceptedObjectClass) {
123 return true;
124 }
125 }
126 }
127 else if(is_string($objectClass)) {
128 if($objectClass == $objectClass) {
129 return true;
130 }
131 }
132 else if($objectClass instanceof $this->_acceptedObjectClasses) {
133 return true;
134 }
135
136 return false;
137 }
138 }
Sedan så implementerar vi ett simpelt register för som bara accepterar forulär.
1 <?php
2 /**
3 * Class that manages forms from several different modules for display in a
4 * single place like the user settings page, admin settings page etc
5 * @author danlil
6 *
7 */
8 class Core_Form_Registry extends Core_Object_Registry_Abstract
9 {
10 protected $_acceptedObjectClasses = 'Zend_Form';
11 }
Sedan gör vi en specialicering av denna för våra formulär med användarens inställningar.
1 <?php
2 class Core_Form_User_SettingsRegistry extends Core_Form_Registry
3 {
4 protected $_acceptedObjectClasses = 'Core_User_SettingsForm';
5
6 private $_user = null;
7
8 public function setUser(Core_Model_User $user) {
9 $this->_user = $user;
10 }
11
12 public function get($name) {
13 $form = parent::get($name);
14 if($this->_user) {
15 $form->setUser($this->_user);
16 }
17 $form->setHeader($name);
18 return $form;
19 }
20 }
Det vi gör här är att vi säger att det enda som accepteras i detta register är objekt av typen Core_User_SettingsForm. Vi skapar även möjligheten att tala om för registret vilken användare det är vi skall jobba med och har implementerat funktionen setUser i alla formulär av typen Core_User_SettingsForm. Denna funktion skapar i grundklassen ett gömt element i formuläret med id på användaren för att identifiera denna och i specialiseringar i varje kalls så har den hand om att plocka ut nuvarande data för användaren och fylla i denna i formulären när vi presenterar dem. Core_User_SettingsForm låter oss även sätta överskrift på just detta formuläret. Jag visar alla formulär i en tabbcontainer så därför ville jag ha detta.
1 <?php
2 class Core_Form_User_SettingsForm extends Zend_Form
3 {
4 private $_user = null;
5 private $_header = '';
6
7 public function setHeader($header) {
8 $this->_header = $header;
9 }
10
11 public function getHeader() {
12 return $this->_header;
13 }
14
15 public function setUser($user) {
16 $this->_user = $user;
17 $user_id = new Zend_Form_Element_Hidden('user_id');
18 $user_id->setValue($user->user_id);
19 $user_id->setOrder(0);
20 $this->addElement($user_id);
21 }
22 }
Om vi sedan tittar på Messages_Form_UserSettingsForm så kan den tex se ut såhär.
1 <?php
2 class Messages_Form_UserSettingsForm extends Core_Form_User_SettingsForm
3 {
4 public function __construct($options = null) {
5 parent::__construct($options);
6
7 $request = Zend_Controller_Front::getInstance()->getRequest();
8
9 $this->setName('messageSettingsForm');
10 $this->setAction($this->getView()->url(array('module' => 'messages', 'controller' => 'usersettings', 'action' => 'update')));
11 $this->setMethod('post');
12
13 $notification_time = new Zend_Form_Element_Select('notification_timeout');
14 $notification_time->setLabel('Notification timeout');
15 $notification_time->setRequired()->addValidator('NotEmpty');
16
17 $notification_time->addMultiOption(5000, "5 sek");
18 $notification_time->addMultiOption(10000, "10 sek");
19 $notification_time->addMultiOption(30000, "30 sek");
20 $notification_time->addMultiOption(0, "Must be closed manually");
21
22 $save = new Zend_Form_Element_Submit('save');
23 $save->setLabel('Save');
24
25 $this->addElements(array($notification_time, $save));
26 }
27
28 public function setUser($user) {
29 parent::setUser($user);
30
31 // Get user settings
32 $userSettingsModel = new Messages_Model_UserSettings();
33 $where = $userSettingsModel->getAdapter()->quoteInto('user_id = ?', $user->user_id);
34 $userSettings = $userSettingsModel->fetchRow($where);
35
36 $this->getElement('notification_timeout')->setValue($userSettings->notification_timeout);
37 }
38 }
39
Som synes så hämtar vi ut nuvarande data för användaren gällande hans inställningar för meddelanden i funktionen setUser.
I Messages modulens bootstrap så lägger vi till
1 protected function _initUserSettingsForm() {
2 Core_Form_User_SettingsRegistry::getInstance()->register('Messages_Form_UserSettingsForm', 'Messages');
3 }
Detta görs sedan i varje modul som har inställningar som användaren skall kunna göra i sin sida för inställningar.
I vår default moduls editAction för vår UserController så har vi sedan
1 public function editAction() {
2 $userId = $this->_request->getParam('user_id');
3 $userSettingsRegistry = Core_Form_User_SettingsRegistry::getInstance();
4 $usersModel = Core_Model_UsersGateway::getInstance();
5
6 // Are we editing a different user than ourselves
7 if($userId) {
8 $user = $usersModel->getUser($userId);
9 }
10 // or a re we editing our own user
11 else {
12 $user = $usersModel->getCurrentUser();
13 $aclRoles = $user->getAclRoles();
14 }
15
16 // Notify the registry of the user
17 $userSettingsRegistry->setUser($user);
18
19 $panes = array();
20 foreach($userSettingsRegistry->getAll() as $form) {
21 $pane = array('header' => $form->getHeader(),
22 'content' => $form);
23 array_push($panes, $pane);
24 }
25
26 $this->view->panes = $panes;
27 }
Nu kommer kunna visa alla formulär som är till för att sätta användarens inställningar på ett och samma ställe, utan att en enda modul behöver känna till något om någon annan modul.
Jag använder ju nu detta till formulär, men detta kan appliceras på i princip allt som skall kunna presenteras eller på ett centralt ställe utan att själva kärnan i applikationen behöver känna till alla moduler och deras uppbyggnad.
Som synes så skapas inte objekten i registret innan de kallas vilket sparar prestanda, men det finns mera jobb att göra på detta.
Som avslutning vill jag nämna att man hade kunnat åstakomma ungefär samma beteende med ett observer-pattern, men jag kände att jag fick bättre kontroll på alla delarna på detta sätt.
