Arkiv för ‘PHP’ kategori

Förslag till en samling tutorials

30 maj, 2011

Jag har funderat lite på vad man skulle kunna göra en tutorial/kurs om. Det finns ju otroligt mycket bra information på nätet redan och även om detta mest hade varit en kul liten grej så hade det varit roligt om någon lärde sig något på samma gång. Jag tror att jag kommer rikta in mig på en webapplikation av enklare slag skriven i PHP. Med enklare menar jag då, inte som omfattande. Vi skall fortfarande använda oss av abstrakta databaslager, AJAX och annat trevligt. Mycket då det saknas en del bra information för den svenska publiken om PHP men också för att det händer mycket med språket. Det möjliggör också för många att testa på det då alla delar vi kommer använda oss av kommer vara gratis (eller ha en free trial) utan att bli beroende av tredjepartslösningar som exempelvis Mono om man nu sitter i exempelvis Linux. Jag är dock inte partisk utan kan mycket väl se att vi i ett senare skede gör om exakt samam applikation i .Net för att se likheter och skillnader.

Jag tänkte försöka utgå ifrån att inte förvänta mig en massa kunskap av de som tar del av materialet. Men naturligtvis så kommer det att hjälpa om man utvecklat tidigare då jag endast kort kommer gå igenom grunder och sedan gå in på lite mera avancerade områden. Jag tänker mig följande upplägg

  1. Sätta upp lokala utvecklingsverktyg och saker att tänka på (OS, IDE, användbara plugins etc)
  2. Sätta upp server (utvecklingsstack, databas, versions hantering etc)
  3. Lite kort om PHP, karakteristiska saker med språket. Styrkor och svagheter etc.
  4. Ett antal enklare mindre program som visar lite på olika sätt att koda under PHP (och många andra språk också)
  5. Enklare introduktion till OOP och hur PHP hanterar detta.
  6. Introduktion till Zend Framework
  7. Introduktion av MVC mönstret

Sedan börjar vi knacka på vår lilla applikation och börjar då titta på hur Zend Framework hanterar views etc. Sedan går vi in på användarhantering, åtkomstkontroll, osv.

Detta är bara en idé om punkter att ta upp och jag tror att det hela, dynamisk, växer fram med tiden. Finns det några idéer eller önskemål så kom gärna med förslag.

flattr this!

Abstrahera databaslogiken ytterligare en nivå

13 maj, 2011

Hanteringen och kopplingen till datakällorna går ofta igenom en mognadsprocess ju mer en utvecklare lär sig. Det är inte ovanligt att man ser kod i stil med

1 function getUsername($userId) { 2 $query = " 3 SELECT * FROM users 4 WHERE id=%s 5 LIMIT 1 6 "; 7 $result = mysql_queryf($query, $userId); 8 while($row = mysql_fetch_array($result)) 9 { 10 $output = $row[username]; 11 12 } 13 return $output; 14 }

Många som jobbat lite med databasdrivna applikationer börjar förr eller senare att abstrahera bort databaslogiken. I ett första steg kan detta te sig som så att man skapar objekt som har fint namngivna funktioner, exempelvis ett User objekt med funktioner så som getName() osv. I dessa funktioner sker sedan databasanrop annat. Men det publika interfacet av objektet blir mycket trevligare.

1 class User 2 { 3 private $_userId; 4 5 ... 6 7 function getUsername() { 8 $query = " 9 SELECT * FROM users 10 WHERE id=%s 11 LIMIT 1 12 "; 13 $result = mysql_queryf($query, $this->_userId); 14 while($row = mysql_fetch_array($result)) 15 { 16 $output = $row[username]; 17 18 } 19 return $output; 20 } 21 }

Nästa steg är ofta att hämta ut all relevant data i konstruktorn av User och således inte hämta den varje gång du anropar getUsername(). Sedan bygger man vidare med cachning av datan osv, osv.

Jag satt själv och funderade på detta för ett tag sedan och tänkte att man egentligen bör abstrahera det så pass att User objektet i detta fallet inte har någonting med databasen att göra. Data bör kunna sparas på olika ställen utan att vår User egentligen skall behöva förändras. Så all logik kopplad till lagring av data bör således inte ligga i klassen som definierar vår User.

Istället bestämde jag mig för att göra som så att min modell, User i detta fallet, endast innehöll data, samt funktioner för att manipulera denna data. Att sedan hämta och lagra datan hanteras istället av en annan klass. Jag valde att kalla dessa för Gateways. På detta sätt kan man skapa Gateways som jobbar emot olika datakällor utan att behöva pilla på våra modell klasser. Detta lämpar sig tex om man sitter och utvecklar ny funktionalitet och vill ha testdata lokalt i en XML fil istället för att jobba mot databasen. När det sedan är dags att jobba emot databasen så använder man sig bara av en annan Gateway och så är det löst.

För att uppnå detta skapar vi en basklass för våra modeller. I denna basklass har vi funktioner för att fylla på data, samt att hämta ut data. Vi inför en standard som säger att data i våra datakällor sparas i formatet ‘foo_id’ och att vår modell i så fall kommer innehålla funktioner som heter ‘setFooId()’ och ‘getFooId()’.

1 <?php 2 abstract class Core_Model_Abstract 3 { 4 protected $_isSaved = false; 5 6 public function __construct(array $options = null){ 7 if(is_array($options)){ 8 $this->setOptions($options); 9 } 10 } 11 12 public function __set($name, $value){ 13 $methodName = 'set'; 14 $nameParts = explode('_', $name); 15 16 foreach($nameParts as $namePart) { 17 $methodName .= ucfirst($namePart); 18 } 19 20 if((!method_exists($this, $methodName)){ 21 throw new Exception('Invalid property ' .$methodName . ' for ' . get_called_class()); 22 } 23 $this->$methodName($value); 24 } 25 26 public function __get($name){ 27 $methodName = 'get'; 28 $nameParts = explode('_', $name); 29 30 foreach($nameParts as $namePart) { 31 $methodName .= ucfirst($namePart); 32 } 33 34 if(('gateway' == $name) || !method_exists($this, $methodName)){ 35 throw new Exception('Invalid property ' .$methodName . ' for ' . get_called_class()); 36 } 37 return $this->$methodName(); 38 } 39 40 public function setOptions(array $options){ 41 $methods = get_class_methods($this); 42 foreach($options as $name => $value){ 43 $methodName = 'set'; 44 $nameParts = explode('_', $name); 45 46 foreach($nameParts as $namePart) { 47 $methodName .= ucfirst($namePart); 48 } 49 50 if(in_array($methodName, $methods)){ 51 $this->$methodName($value); 52 } 53 else { 54 Core_Log::log('Method ' . $methodName . ' does not exist for ' . get_called_class(), Core_Log::INFO); 55 } 56 } 57 return $this; 58 } 59 60 public function setIsSaved($isSaved) { 61 $this->_isSaved = $isSaved; 62 } 63 64 public function getIsSaved() { 65 return $this->_isSaved; 66 } 67 }

En klass för en User hade då sett ut i stil med.

1 <?php 2 class Core_Model_User extends Core_Model_Abstract 3 { 4 private $_userId; 5 private $_username; 6 7 ... 8 9 public function getUserId() { 10 return $this->_userId; 11 } 12 13 public function setUserId($id) { 14 $this->_userId = $id; 15 } 16 17 public function getUsername() { 18 return $this->_username; 19 } 20 21 public function setUsername($_username) { 22 $this->_username = $_username; 23 } 24 }

 

Denna modell kan nu innehålla data, men hur skall vi då sköta lagring av våra modeller? Här kommer våra Gateways in. Även här börjar vi med en basklass med det som är gemensamt för alla våra Gateways. En specialanpassad gateway för att spara till DB kan sedan se ut enligt följande.

1 <?php 2 3 /** 4 * Gateway class between Models and Database 5 * @author danlil 6 * 7 */ 8 abstract class Core_Model_Gateway_Db extends Core_Model_Gateway_Abstract 9 { 10 protected $_dbTableClass; 11 protected $_modelClass; 12 protected $_dbTable; 13 14 public function setDbTable($dbTable){ 15 if(is_string($dbTable)){ 16 $dbTable = new $dbTable(); 17 } 18 if(!$dbTable instanceof Core_Db_Table_Abstract){ 19 throw new Exception('Invalid table data gateway provided'); 20 } 21 $this->_dbTable = $dbTable; 22 return $this; 23 } 24 25 public function getDbTable(){ 26 if(null === $this->_dbTable){ 27 $this->setDbTable($this->_dbTableClass); 28 } 29 return $this->_dbTable; 30 } 31 32 public function save(Core_Model_Abstract $model) { 33 $tableInfo = $this->getDbTable()->info(); 34 $pk = $this->getDbTable()->getPrimaryKey(); 35 $setPkFunction = 'set'; 36 $getPkFunction = 'get'; 37 38 if(is_array($pk)) { 39 throw new Exception("Support for multicolumn primary keys not yet implemented. Prehaps it's time!"); 40 } 41 42 /** 43 * As a standard we take column names like foo_bar and try to get 44 * the correct data to put in it by calling getFooBar() from the model 45 * object. 46 */ 47 // echo "<pre>"; 48 // die(var_dump($tableInfo['metadata'])); 49 foreach($tableInfo['metadata'] as $columnName => $columnMetadata) { 50 $methodName = 'get'; 51 $columnNameParts = explode('_', $columnName); 52 53 foreach($columnNameParts as $columnNamePart) { 54 $methodName .= ucfirst($columnNamePart); 55 56 if($columnName == $pk) { 57 $getPkFunction .= ucfirst($columnNamePart); 58 $setPkFunction .= ucfirst($columnNamePart); 59 } 60 } 61 62 if(!method_exists($model, $methodName)) { 63 $className = get_class($model); 64 Core_Log::log("Function \"$methodName\" does not exist in \"$className\". Either implement the function or override the save() function.", Core_Log::CRIT); 65 throw new Exception("Function \"$methodName\" does not exist in \"$className\". Either implement the function or override the save() function."); 66 } 67 68 $data[$columnName] = $model->$methodName(); 69 } 70 71 $id = $model->$getPkFunction(); 72 73 if($model->$getPkFunction() != null && $model->getIsSaved() == false) { 74 $lastInsertId = $this->getDbTable()->insert($data); 75 $model->$setPkFunction($lastInsertId); 76 } 77 else if(null === $id){ 78 unset($data[$pk]); 79 $lastInsertId = $this->getDbTable()->insert($data); 80 $model->$setPkFunction($lastInsertId); 81 } 82 else{ 83 unset($data[$pk]); 84 //$where = $this->getDbTable()->getAdapter()->quoteInto('bug_id = ?', 1234); 85 $this->getDbTable()->update($data, array("$pk = ?" => $id)); 86 } 87 } 88 89 public function delete(Core_Model_Abstract $model) { 90 $tableInfo = $this->getDbTable()->info(); 91 $pk = $this->getDbTable()->getPrimaryKey(); 92 $setPkFunction = 'set'; 93 $getPkFunction = 'get'; 94 95 if(is_array($pk)) { 96 throw new Exception("Support for multicolumn primary keys not yet implemented. Prehaps it's time!"); 97 } 98 99 /** 100 * As a standard we take column names like foo_bar and try to get 101 * the correct data to put in it by calling getFooBar() from the model 102 * object. 103 */ 104 foreach($tableInfo['metadata'] as $columnName => $columnMetadata) { 105 $methodName = 'get'; 106 $columnNameParts = explode('_', $columnName); 107 108 foreach($columnNameParts as $columnNamePart) { 109 $methodName .= ucfirst($columnNamePart); 110 111 if($columnName == $pk) { 112 $getPkFunction .= ucfirst($columnNamePart); 113 $setPkFunction .= ucfirst($columnNamePart); 114 } 115 } 116 117 if(!method_exists($model, $methodName)) { 118 $className = get_class($model); 119 Core_Log::log("Function \"$methodName\" does not exist in \"$className\". Either implement the function or override the save() function.", Core_Log::ERR); 120 throw new Exception("Function \"$methodName\" does not exist in \"$className\". Either implement the function or override the save() function."); 121 } 122 123 $data[$columnName] = $model->$methodName(); 124 } 125 126 if(null === ($id = $model->$getPkFunction())){ 127 unset($data[$pk]); 128 Core_Log::log($className . " returned null as primary key and could not be deleted", Core_Log::ERR); 129 } 130 else{ 131 unset($data[$pk]); 132 $where = $this->getDbTable()->getAdapter()->quoteInto("$pk = ?", $model->$getPkFunction()); 133 $this->getDbTable()->delete($where); 134 } 135 unset($model); 136 } 137 138 public function find($id, Core_Model_Abstract $model = null){ 139 $result = $this->getDbTable()->find($id); 140 if(0 == count($result)){ 141 return; 142 } 143 $row = $result->current(); 144 if($model === null) { 145 $model = $this->_createModel($row); 146 return $model; 147 } 148 $this->_populate($model, $row); 149 } 150 151 public function fetchAll() { 152 return $this->_fetchAll(); 153 } 154 155 protected function _fetchAll($selectStatement = null){ 156 $result = $this->getDbTable()->fetchAll($selectStatement); 157 158 if(0 == count($result)){ 159 return null; 160 } 161 162 $models = array(); 163 foreach($result as $row){ 164 $model = $this->_createModel($row); 165 array_push($models, $model); 166 } 167 168 return $models; 169 } 170 171 protected function _fetchRow($selectStatement) { 172 $result = $this->getDbTable()->fetchRow($selectStatement); 173 174 if(0 == count($result)){ 175 return; 176 } 177 178 $model = $this->_createModel($result); 179 180 return $model; 181 } 182 183 /** 184 * 185 * Creates the model and if supplied sets the options 186 * @param array $options 187 * @throws Exception 188 */ 189 protected function _createModel($options = null) { 190 if(is_string($this->_modelClass) && class_exists($this->_modelClass)){ 191 if($options) { 192 $options = $options->toArray(); 193 } 194 $model = new $this->_modelClass($options); 195 $model->setIsSaved(true); 196 return $model; 197 } 198 else { 199 throw new Exception('Invalid model class provided ' . $this->_modelClass); 200 } 201 } 202 203 protected function _beginTransaction() { 204 $this->getDbTable()->getDefaultAdapter()->beginTransaction(); 205 } 206 207 protected function _commit() { 208 $this->getDbTable()->getDefaultAdapter()->commit(); 209 } 210 211 protected function _rollBack() { 212 $this->getDbTable()->getDefaultAdapter()->rollBack(); 213 } 214 }

Som synas så jobbar vår Gateway emot Core_Db_Table_Abstract objekt. Dessa är egentligen bara en specialisering av Zend_Db_Table_Abstract. Det hade även gått att ha SQL kod direkt i vår Gateway om man så önskar. Core_Db_Table_Abstract ser ut som följer.

1 <?php 2 abstract class Core_Db_Table_Abstract extends Zend_Db_Table_Abstract 3 { 4 static function getPrimaryKey() { 5 if(!empty($this)) { 6 if(count($this->_primary) > 0) { 7 $pk = $this->_primary; 8 } else { 9 $tableInfo = $this->info(); 10 $pk = $tableInfo['primary']; 11 } 12 } 13 else { 14 $calledClassName = get_called_class(); 15 $table = new $calledClassName(); 16 if(count($table->_primary) > 0) { 17 $pk = $table->_primary; 18 } else { 19 $tableInfo = $table->info(); 20 $pk = $tableInfo['primary']; 21 } 22 unset($table); 23 } 24 return $pk; 25 } 26 27 static function getName() { 28 if(!empty($this)) { 29 $name = $this->_name; 30 } 31 else { 32 $calledClassName = get_called_class(); 33 $table = new $calledClassName(); 34 $name = $table->_name; 35 unset($table); 36 } 37 return $name; 38 } 39 } 40 41

Vad allt detta resulterar i är att du nu kommer jobba med dina modeller utan att egentligen behöva tänka på den bakomliggande lagringen.

1 // Skapa en ny user och sätt data 2 $user = new Core_Model_User(); 3 $user->setName('Foo Bar'); 4 5 // Låt vår gateway, som kan jobba mot DB 6 // eller någon annan lagring spara den åt oss. 7 // Vår gateway sköter också om detta är en 8 // 'insert' eller 'update' 9 Core_Model_UsersGateway::getInstance()->save($user);

Vår Core_Model_UsersGateway använder vi för att spara och hämta Users enligt olika kriterier. Exempelvis

1 <?php 2 class Core_Model_UsersGateway extends Core_Model_Gateway_Db 3 { 4 protected $_dbTableClass = 'Core_Model_DbTable_Users'; 5 protected $_modelClass = 'Core_Model_User'; 6 7 public function fetchCurrent() { 8 if(!Zend_Auth::getInstance()->hasIdentity()) { 9 return null; 10 } 11 12 $userInfo = Zend_Auth::getInstance()->getStorage()->read(); 13 $currentUser = $this->fetchWhereId($userInfo->user_id); 14 15 return $currentUser; 16 } 17 18 public function fetchWhereId($userId) { 19 return $this->_fetchRow($this->getDbTable()->select() 20 ->where('user_id = ?', $userId)); 21 } 22 }

Jag har valt att döpa funktionerna med namn hämtade inspirerade ifrån queryn de kör. Men det är en smaksak.

Funktioner i User kan självklart innehålla all möjlig logik och inte bara sätta och returnera data. Säg att vi hade haft en funktion som hade varit getUnreadMessages() på vår User. Det hade också löst sig utan att bry sig om var/hur våra Messages var lagrade då även dessa använder sig av en Gateway. I User hade vi då tex kunna haft något i stil med.

1 class User extends Core_Model_Abstract 2 { 3 ... 4 5 public function getUnreadMessages() { 6 return Messages_Model_Gateway::getInstance()->fetchAllUnreadForUserId($this->_userId); 7 } 8 9 ... 10 } 11 12 13 // Nu kan du hämta ut den aktiva användaren, 14 // ändra dennes namn och spara ner ändringen. 15 // Allt utan att behöva bry dig alls om vad som 16 // händer i bakgrunden. 17 $currentUser = Core_Model_UsersGateway::getInstance()->fetchCurrent(); 18 $currentUser->setName('Bar Foo'); 19 Core_Model_UsersGateway::getInstance()->save($currentUser); 20 21 // Och så printar vi ut alla headers ifrån olästa 22 // meddelanden också. 23 $unreadMessages = $currentUser->getUnreadMessages(); 24 25 foreach($unreadMessages as $unreadMessage) { 26 echo $unreadMessage->getHeader(); 27 }

Det är naturligtvis lite jobb att få detta på plats och jag optimerar och finslipar detta kontinuerligt. Men fördelarna tycker jag är betydande. Jag jobbar mot Zend Framework, därav formaten på namnen på klasser etc, men det går lika bra att implementera detta i “ren” PHP eller mot något annat ramverk.

flattr this!

Överlagra PHP standardfunktioner med hjälp av namespaces

24 mars, 2011

Såg ett webinar om PHP namespaces nydligen. Jag är van med detta ifrån tex C++ så det var inte direkt några nyheter. Men om vi bortser ifrån att ‘\’ valts som separator som det har spytts galla på under lång tid så var en av sakerna de talade varmt om att man i ett namspace kan implementera funktioner med samma namn som standardfunktioner.

Exempelvis

1 <?php 2 namespace Foo; 3 /* 4 * Implementera str_split som splittar på ord istället för 5 * enskillda bokstäver. 6 */ 7 function str_split($string, $splitlen = 2) { 8 return preg_split('/\W/', $string, $splitlen); 9 } 10 11 12 // Detta kommer nu köra Foo\str_split() och INTE 13 // PHP standard str_split 14 $str = str_split("Foo, Bar");

Även om jag länge eftersök möjligheten att överlagring i stil med vad C++ har där du kan överlagra operator+ osv så tycker jag nog att ovanstående bör användas sparsamt. Säg att du har deklarerat en massa klasser i ditt namespace ‘Foo’ och sedan även har denna typ av ‘överlagring’ av ‘str_split’.

1 <?php 2 use Foo; 3 4 /* 5 * Användaren slipper skriva Foo\Bar() 6 * då han deklrarerat 'use Foo' 7 */ 8 $bar = new Bar(); 9 10 /* 11 * Sedan tänker användaren att han skall dela upp 12 * en sträng i enskillda characters. 13 */ 14 $string = 'Min helt underbara sträng'; 15 $chars = str_split($string, 1); 16 17 /* 18 * Men då 'use Foo' är deklarerat och str_split 19 * finns implementerad i 'Foo' så är ovan det 20 * samma som att skriva: 21 * 22 * Foo\str_split($string, 1); 23 * 24 * $chars kommer alltså inte innehålla varje charater 25 * utan kommer istället se ut som 26 * Array 27 * ( 28 * [0] => Min 29 * [1] => helt 30 * [2] => underbara 31 * [3] => sträng 32 * ) 33 */ 34

Om detta inte är väldigt väl dokumenterat och användaren gjorts uppmärksam på detta så kommer han att förvänta sig att om han förvänta sig att ‘str_split’ fungerar som vanligt. Det kommer den också göra om om du tex inkluderar en fiktiv klass ‘Bar’ som du har i ditt namespace ‘Foo’ med

1 <?php 2 $bar = new Foo\Bar();

Men om användaren använder dina klasser mycket kommer de förmodligen att använda ‘use’ istället. Är de lite slarviga kommer de förmodligen att köra en ‘use’ på hela ditt namespace och inte på varje enskild klass. Detta gör nu att ‘str_split’, i de filer som inkluderar ditt namespace, helt plötsligt kör din implementation av ‘str_split’. Så även om jag kan se användningsområden för detta och speciellt så länge man håller koll på vad man gör så ser jag framför mig att det kan skapa förvirring för folk som använder 3:e parts bibliotek.

För att snabbt ‘patcha’ beteenden i PHP så kan detta användas framgångsrikt, men om det är situationer då standardfunktioner i PHP behöver förändras så förefaller det mera logiskt att ‘felet’ ligger i PHP och att PHP bör patchas i källkoden. Om man annars vill ha en ‘str_split’ som splittar på ord så känns det relativt logiskt att istället bara kalla den ‘str_split_words’ eller något liknande. I dagsläget kan jag mest se ‘hack’ som man kan lösa genom att använda sig av denna typ av ‘överlagring’ annars.

Men det återstår väll att de hur detta kommer användas. Vad tycker ni själva eller har ni några bra tips på användningsområden. Lämna gärna en kommentar.

Information om i vilken ordning PHP’s nameresolution arbetar, för att undvika oplanerade problem i er egen kod, hittar ni på följande länk:

http://php.net/manual/en/language.namespaces.rules.php

flattr this!

Abstrakt basklass för Singleton pattern i PHP 5.3+

28 december, 2010

I PHP 5.3+ så tillkom funktionen get_called_class, som ger dig namnet på klassen som anropade medlemsfunktionen du befinner dig i. Tidigare har vi haft __CLASS__, men om du tex haft en klass bar som extendat klassen foo och i bar använder dig av __CLASS__ i en funktion så kommer __CLASS__att vara foo även om du har en instans av ett objekt av typen bar.

Funktionen get_galled_class kommer dock returnera foo i detta fall. Tack vare detta är det nu möjligt att göra en basklass för singletons och inte behöva implementera detta pattern i varje klass som du vill skall använda sig av det. För att lyckas med detta gör du följande

1 <?php 2 abstract class Core_Singleton 3 { 4 /** 5 * 6 * Store classes in an array since extended classes should not overwrite 7 * an existing instance 8 * @var array $_instances 9 */ 10 private static $_instances = array(); 11 12 /** 13 * Get instace of class 14 * @return Core_Singleton 15 */ 16 static public function getInstance() { 17 $calledClassName = get_called_class(); 18 19 // Check if we have instanciated this object type already 20 if(array_key_exists($calledClassName, self::$_instances)) { 21 return self::$_instances[$calledClassName]; 22 } 23 // Else we create an instance and save it for later requests 24 else { 25 $object = new $calledClassName(); 26 self::$_instances[$calledClassName] = $object; 27 return $object; 28 } 29 } 30 } 31

flattr this!

Dela "okända" objekt mellan isolerade moduler i Zend Framework

28 december, 2010

 

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.

flattr this!

Google ”Did You Mean” funktion

15 augusti, 2010

Jag gillar Googles funktion som ger dig ett förslag på vad du kanske menade om du råkade stava lite fel. Jag ville använda mig av den själv och tittade runt lite. Google har API:er till mycket av sina tjänster, men inte det. Så jag bestämde mig för att slänga ihop en egen lite snabbt.

 PHP |  copy code |? 
01
class Core_Google_DidYouMean
02
{
03
      private $locale, $search_addr, $query;
04
 
05
      public function __construct($locale = 'en-US') {
06
        $cc = strtolower(substr($locale, -2));
07
        if(Util::testUrl('http://www.google.' . $cc)) {
08
          $this->search_addr = 'http://www.google.'. $cc .'/search?hl=#LANGUAGE#&q=#QUERY#&meta=';
09
        }
10
        else {
11
          $this->search_addr = 'http://www.google.com/search?hl=#LANGUAGE#&q=#QUERY#&meta=';
12
        }
13
      } 
14
 
15
      public function search($query){ 
16
             $this->query = $query;
17
 
18
             $result = file_get_contents(str_replace(array('#LANGUAGE#','#QUERY#'),array($this->locale,urlencode($this->query)),$this->search_addr));
19
             preg_match_all("/\<div id\=res class\=med>\<h2 class\=hd\>(.*?)\<\/h2\>\<div\>\<ol\>\<li\>\<p\>\<span class\=spell style\=\"color\:\#cc0000\">(.*?)\: \<\/span\>\<a href\=\"(.*?)\"class\=spell\>(.*?)\<\/a\>/i", $result, $matches); 
20
 
21
             if(!empty($matches[4][0])) {
22
               $result = strip_tags($matches[4][0]);
23
             }
24
             else {
25
               $result = '';
26
             }
27
 
28
             return $result; 
29
      }
30
}

Util::testUrl() kollar så att url’en existerar och returnerar true eller false.

Update: Google har lagt med detta i sitt API sedan en tid tillbaka med doSpellingSuggestion()

flattr this!

Lägg till border runt bild i PHP

11 juni, 2010

Fick en fråga häromdagen om hur man kan skapa en border runt en bild i PHP. Slängde ihop en liten kodsnutt som som jag tänkte dela med mig av här.

 PHP |  copy code |? 
01
02
/*
03
* Function to create a border around an image
04
*/
05
function drawBorder($image_name, $r = 0, $g = 0, $b = 0, $thickness = 1)
06
{
07
  $image = ImageCreateFromJPEG($image_name);
08
  $color = ImageColorAllocate($img, $r, $g, $b);
09
 
10
  $x1 = 0;
11
  $y1 = 0;
12
  $x2 = ImageSX($image) - 1;
13
  $y2 = ImageSY($image) - 1;
14
 
15
  for($i = 0; $i < $thickness; $i++)
16
  {
17
    ImageRectangle($image, $x1++, $y1++, $x2--, $y2--, $color);
18
  }
19
 
20
  return $image;
21
}

Sedan kan du köra något i stil med

 PHP |  copy code |? 
1
header('Content-type: image/jpeg');
2
ImageJPEG(drawBorder("images/foo.jpg", 128, 128, 0, 3));

Bör förmodligen lägga till en switch-sats i funktionen som faktiskt kollar vilken typ av bild du använder dig av och använder sig av rätt ImageCreateFrom* funktion på bilden i fråga och lite exception handling. Men här har ni en grund.

flattr this!

Länka anrop i PHP

27 augusti, 2009

När man kodar objektorienterat i PHP så använder man sig som i så många andra språk ofta av befintliga och beprövade design patterns. Dessa är generella återanvändbara lösningar på vanligt förekommande problem i systemutveckling. Till dessa hör bland annat Factory, Singelton, Adapter, Bridge, Iterator osv. Ett design pattern som jag själv aldrig kommit i kontakt med under mina år med C++ och PHP fångade min uppmärksamhet då det används flitigt i ZendFramework som jag använder flitigt. Jag är inte säker på att namnet jag använder är rätt, men jag kallar det Command Chaining. Om någon vet det riktiga namnet så lämna det gärna i komentarerna.

Det hela är egentligen väldigt enkelt. Genom att varje medlemsfunktion i klassen returnerar en $this referens (dvs en referens till instansen av klassen) så kan man på den kalla ytterliggare funktioner. Ett exeplel kunde vara

 PHP |  copy code |? 
01
<?php
02
// Create a new instance
03
$ftp = new Ftp();
04
 
05
// Connect to the ftp, change to correct directory, 
06
// download a file and close the connection in one go
07
$ftp->connect('username', 'password', 'ftp.foobar.com')
08
    ->chdir('/folder_with_the_stuff')
09
    ->download('thefile.txt')
10
    ->closeConnection();
11
?>

Fördelen med detta design pattern är att man väldigt snabb och sammanhängande kan skriva en serie androp som hör ihop. Används t ex effektivt i ZendFrameworks Zend_Db_Table klass som fungerar som ett objektorienterat interface till databastabeller.

 PHP |  copy code |? 
01
<?php
02
 
03
// Fetching a rowset
04
 
05
$rows = $table->fetchAll(
06
  $table->select()
07
        ->where('bug_status = ?', 'NEW')
08
        ->order('bug_id ASC')
09
        ->limit(10, 0)
10
    );
11
 
12
?>

En nackdel som jag dock tycker detta pattern har är att felhanteringen blir
mycket jobbigare. Eller nja, man måste tänka på att man gör det annorlunda
iaf. Säg att personen i exemplet med ftp klassen ovan kunde logga in men
 fanns inte just den filen han ville ladda ner, då kanske han vill ladda ner
en annan fil istället. Men hur gör man då? Om download() bara returnerar
en referens till $this ändå så kommer vi aldrig att få veta att filen inte fanns
och closeConnection() kommer sedan att köras. Att returnera en felkod
funkar inte heller eftersom closeConnection() då kommer försöka köras
 det returvärdet. Vad man bör och skall göra är naturligtvis att använda
Exceptions och kasta ett sådant i de funktioner där något går fel.

 PHP |  copy code |? 
01
// Create a new instance
02
$ftp = new Ftp();
03
 
04
try {
05
// Connect to the ftp, change to correct directory, 
06
// download a file and close the connection in one go
07
$ftp-;>connect('username', 'password', 'ftp.foobar.com')
08
    ->chdir('/folder_with_the_stuff')
09
    ->download('thefile.txt')    
10
    ->closeConnection();
11
}
12
catch(Exception $e) {
13
// Handle the exception
14
...
15
?>

Om man är van vid att använda felkoder som returvärden som felhantering
så får man tänka om lite. Det ändrar också flödet med if() checkar man
kanske då traditionellt haft när man hanterat fel. Men kommar man bara
över det så kan detta design pattern snygga upp koden en hel del i ens
projekt. Om vill kan man börja lite enklare och tex implementera detta
på något så simpellt som en config klass. Om man låter alla
set−funktioner returnera en $this referens så kan man lätt känna lite
på detta design pattern utan att man behöver tänka så mycket på
felhanteringen som följer om funktionerna gör lite mera avancerade saker.

 PHP |  copy code |? 
1
// Load config from inifile
2
$config = new Config('foobar.ini');
3
 
4
// Change some settings
5
$config->setWidth('200')->setHeight('400');
6
 
7
...
8
 
9
$config->save('foobar.ini');

flattr this!

WordPress SEO fine-tune by Meta SEO Pack from Poradnik Webmastera