Page 15 of 20

Posted: Fri Jun 02, 2006 3:44 pm
by Ambush Commander
Gateway is the simplest, but breaks down when there's no clean correspondence between domain objects and the relational database. Furthermore, you can't "build" a mapper off a gateway, while you can do the vice versa. However, mappers are complex beasts and VERY difficult for newbies to understand. However, my understanding is that if we create a miniature domain model to handle the authentication (which is probably a GOOD idea, considering the possible complexity of the whole thing: we should go OOP), mappers are best suited for the task.

So... how are the unit tests going?

Edit - I've added proof-of-concept interfaces for all three possible solutions (there's a fourth, Column Data Gateway, but we're not going to use that). A mapper would also need an Identity Map, a Key Generator, and, perhaps a Unit of Work. A lot of extra components, but I've already got implementations of them lying around in other projects. But, still, if we add that stuff, we go down the "framework" route.

ALSO - I had a revelation. If the column names can be anything at all, depending on what the user names them, we CANNOT use associative returns, because they wouldn't mean anything.

Posted: Fri Jun 02, 2006 4:51 pm
by santosj
I was thinking that we could get the key by using the XMLHTTPRequest or AJAX if you will. That is once we get to that point or will you do it differently?

Also, I have no idea what you are talking about, so good luck on that. I did look at the abstract class and it looks good.

Posted: Fri Jun 02, 2006 4:53 pm
by Ambush Commander
I was thinking that we could get the key by using the XMLHTTPRequest or AJAX if you will. That is once we get to that point or will you do it differently?
My turn to be clueless. :?

Posted: Sat Jun 03, 2006 8:48 am
by santosj
I think we are totally over thinking this DB setup. I understand DBResult and can extend that easily. However I don't think we have to update anything. Are we going to also implement the profile also, as well as the administrative pages and functions? We can provide support for it, but not do it ourselves.

To be honest, I was hoping for something like:

Code: Select all

//$authtype= new DBMap($column, $table, $type);

$username = new DBMap('username', 'user', AuthTools::USERNAME);
$password = new DBMap('pass', 'user', AuthTools::PASSWORD);

$authtools->addColumn($username);
$authtools->addColumn($password);
Or this possible with the gateway and Mapper thingy? Because if it is then I'll be quiet.

Posted: Sat Jun 03, 2006 8:56 am
by Maugrim_The_Reaper
Let's leave out AJAX for the moment - it's an extra dependency to worry over...

I agree we're overdoing the DB setup - get something that works first, then worry about whether it needs refactoring...

Posted: Sat Jun 03, 2006 10:23 am
by Ambush Commander
I agree we're overdoing the DB setup - get something that works first, then worry about whether it needs refactoring...
I disagree. DB access is one of those central issues that, once a decision is made, is VERY difficult to refactor. It's worth getting it right.
To be honest, I was hoping for something like:
Not exactly sure what the code does, so can't tell you.

Posted: Sat Jun 03, 2006 10:34 am
by santosj
I disagree, I have done DB refactoring before and it isn't all that difficult.

Code: Select all

$query = mysql_query("SELECT ". $username->column.", ". $password->column ." FROM ". $username->table ." WHERE ".$username->column ."='$_POST[username]'");
Okay, it is terrible, but just an example.

I've done some AJAX, just the other day and it isn't all that difficult, once you over the bashing your head on to the table part. Seriously, the reason why I like PHP is because if it crashes and burns you have some idea where you went wrong.

"xmlTransport is Not a Constructor" really doesn't tell you anything, since like, looking at it, yes it is an constructor, should I kill a sheep for you oh hail great JavaScript Satan?

Anyway, I fixed it so it is all good. I'm not talking about right now, I'm talking about once Authtools is nearing completion.

Posted: Sat Jun 03, 2006 10:46 am
by Ambush Commander
I disagree, I have done DB refactoring before and it isn't all that difficult.
How big was the codebase you were working with? And are we talking about the same type of refactoring? I'm referring to the changes throughout all files that interact with the database if the API changes.
Okay, it is terrible, but just an example.
Still doesn't clarify. Why don't you tell me what DBMap and AuthTools are?

Posted: Sat Jun 03, 2006 11:59 am
by santosj
To be honest the DB abstraction was only a total of 5 classes in the factory model and it was optimization. Still, anything can be refactored given enough time. It isn't much of an issue. However, I don't think telling you that we are going to scrap the DB model. What I meant to say is that I would like to develop a simpler model for those who are of novice level. Even for me, until I see a finished example, I'll have no idea where to begin on how to use it. So really, it is more, "oh man, this is why I build my own," and I'm not dissing the system. I'm still waiting and I'm most likely wrong about it. I just hope I am and that I eventually understand how to use it. Probably just needs one of those Auto functions.

DBMap

Maps to the table and username column and the type assigns the value so that the other classes now how to deal with it. I got the idea from another Auth system that allowed you to choose the table and column for which the username column was retrieved. The same for the password.

I would say that the AuthTools would probably be the gateway perhaps where it translates the maps to the rest of the system for use.

Posted: Sat Jun 03, 2006 2:23 pm
by Ambush Commander
Well, the abstraction should make it easy to use. Here's how each of the different patterns would work...

Mapper:

Code: Select all

$user = $mapper->findByUsername($input['username']);
$user->password->validatePassword($input['password']);
Active Record:

Code: Select all

$user = User::findByUsername($input['username']);
$user->password->validatePassword($input['password']);
Gateway:

Code: Select all

$user_data = $gateway->findUserByUsername($input['username']);
$password = new Password($user_data['password']);
$password->validatePassword($input['password']);

Posted: Sat Jun 03, 2006 2:47 pm
by santosj
Ambush Commander wrote:Well, the abstraction should make it easy to use. Here's how each of the different patterns would work...
I usually don't love it when I'm wrong. Keep doing what you're doing. I can really give you any review since you have left me back at DBResult and Iterator.
Mapper:

Code: Select all

$user = $mapper->findByUsername($input['username']);
$user->password->validatePassword($input['password']);
This is sweet, the names could probably be changed, but even n00bs and people like me can get this.
Active Record:

Code: Select all

$user = User::findByUsername($input['username']);
$user->password->validatePassword($input['password']);
Don't like it, The use of static methods may give it a speed up from the Mapper, but it is 'harsh' to look at.
Gateway:

Code: Select all

$user_data = $gateway->findUserByUsername($input['username']);
$password = new Password($user_data['password']);
$password->validatePassword($input['password']);
Between, the Mapper and the Gateway, I'll say I may just use your DB Abstraction on my other projects if you so allow it.

Posted: Sat Jun 03, 2006 3:06 pm
by Ambush Commander
If you've got some time, I strongly suggest you read Patterns of Enterprise Application Architecture. None of this stuff is mine. ;-) It's all from that book.

The trouble with mappers are they're most complicated. Most users won't be able to overload them effectively without understanding the pattern in the first place. On the other hand, I love the cleanness of the interface: it goes straight to OOP.

The gateway is is the closest jump from manually executing the SQL throughout the domain code. All it does is wrap the SQL. This means that, in it's simplest form, you'd have things like:

Code: Select all

$gateway->updatePassword($user['id'], $user['password'], $user['salt'], $user['algorithm']);
While the mapper takes it a step further, and accepts objects (which, understandably, would be a lot easier to do). Also, the mapper tends to do a lot of SQL generation for basic commands, which makes it somewhat a beast to understand internally.

I implemented a mapper a while ago, it looked like this:

Code: Select all

<?php

class Mapper
{
    
    var $_db;
    var $_KeyGenerator;
    var $_IdentityMap;
    
    var $_table; //abstract
    var $_primaryKey; //abstract
    var $_name; //abstract
    
    //timer related
    var $_interval; //optional abstract
    var $_random; //optional abstract
    
    function Mapper(&$db, &$map, &$key, &$plugin) {
        $this->_db =& $db;
        $this->_KeyGenerator =& $key;
        $this->_IdentityMap =& $map;
        
        //rename tablename based on prefix
        $this->_table = $plugin->getDatabasePrefix() . $this->_table;
    }
    
    function _findStatement($field = false) {
        if (!$field) {
            $field = $this->_primaryKey;
        }
        return " SELECT " . $this->_columns .
          " FROM `" . $this->_table . "` " .
          " WHERE $field = ? ";
    }
    
    function &_abstractFind($id) {
        $result =& $this->_getObject($id);
        if ($result != null) return $result;
        $rs =& $this->_db->Execute($this->_findStatement(), array($id));
        if (!$rs) return null;
        $result =& $this->_load(&$rs);
        return $result;
    }
    function &_abstractFindUnique($field, $value) {
        $result =& $this->_getObjectByUnique($field,$value);
        if ($result != null) return $result;
        $rs =& $this->_db->Execute($this->_findStatement($field), array($value));
        if (!$rs) return null;
        $result =& $this->_load(&$rs);
        return $result;
    }
    function &_abstractFindAll() {
        $rs =& $this->_db->Execute("SELECT " . $this->_columns .
          " FROM `" . $this->_table . "`");
        if (!$rs) return array();
        $result =& $this->_loadAll(&$rs);
        return $result;
    }
    function _abstractDelete(&$obj) {
        $this->_db->execute(
          "DELETE FROM `{$this->_table}` WHERE id = ? LIMIT 1;",
          array($obj->getID()));
    }
    function _abstractInsert(&$subject) {
        $id = $this->_KeyGenerator->nextKey();
        $subject->setID($id);
        $insertArray = $this->_doInsert(&$subject);
        $this->_db->execute($this->_insertStatement(), $insertArray);
        $this->_setObject(&$subject);
        $this->_setObjectByUnique(&$subject);
    }
    
    function _abstractDeleteExpired() {
        if (mt_rand(0,$this->_random)) return;
        $this->_db->execute(
          "DELETE FROM `{$this->_table}` WHERE timer_expiration < ?",
          array(time())
          );
    }
    
    function &_loadFromRow($row) {
        if (empty($row)) return null;
        $id = (int) $row[$this->_primaryKey];
        $result =& $this->_getObject($id);
        if ($result != null) return $result;
        $result =& $this->_doLoad($row);
        $this->_setObject(&$result);
        $this->_setObjectByUnique(&$result);
        return $result;
    }
    
    function &_load($rs) {
        $array = $rs->GetArray();
        if (empty($array)) return null;
        return $this->_loadFromRow($array[0]);
    }
    
    function &_loadAll($rs) {
        $resultarray = array();
        $array = $rs->GetArray();
        foreach($array as $row) {
            $resultarray[] =& $this->_loadFromRow($row);
        }
        return $resultarray;
    }
    
    function _updateTimer(&$obj) {
        $timer =& $obj->getTimer();
        if ($timer->getExpiration() === null) {
            $timer->setInterval($this->_interval);
            $timer->reset();
        }
    }
    
    function _insertStatement() {} //abstract
    function &_doLoad(&$rs) {} //abstract
    function _doInsert($subject) {} //abstract
    
    
    function &_getObject($id) {return null;} //abstract
    function &_getObjectByUnique($field, $value) {return null;} //abstract
    function _setObject(&$obj) {} //abstract
    function _setObjectByUnique(&$obj) {} //abstract
    
    
}

?>
and a concrete implementation:

Code: Select all

<?php

class Mapper_Token extends Mapper
{
    
    var $_name = 'Token';
    var $_table = 'tokens';
    var $_primaryKey = 'id';
    
    var $_columns = ' `id`, `value`, `creation`, `timer_expiration`, `user_id` ';
    
    var $_interval = 1814400; //3 weeks
    var $_random = 1;
    
    function find($id) {
        return $this->_abstractFind($id);
        $this->deleteExpired();
    }
    
    function insert(&$obj) {
        $this->_updateTimer(&$obj);
        $this->_abstractInsert(&$obj);
        $this->deleteExpired();
    }
    
    function delete(&$obj) {
        $this->_abstractDelete(&$obj);
        $this->deleteExpired();
    }
    
    function deleteExpired() {
        $this->_abstractDeleteExpired();
    }
    
    function &_getObject($id) {
        $token =& $this->_IdentityMap->getToken($id);
        return $token;
    }
    function &_getObjectByUnique($field, $value) {return null;}
    function _setObject(&$obj) {
        $this->_IdentityMap->addToken(&$obj);
    }
    function _setObjectByUnique(&$obj) {}
    
    function &_doLoad($fields) {
        if (!$fields) return false;
        if ($fields['timer_expiration'] <= time()) return null;
        $Registry =& Registry::instance();
        $MapperUser =& $Registry->getMapperUser();
        $obj =& new Token((int) $fields['id'],$fields['value'],
          (int) $fields['creation'],new Timer($fields['timer_expiration']),
          $MapperUser->find((int) $fields['user_id']));
        return $obj;
    }
    
    function _insertStatement() {
        return "INSERT INTO `{$this->_table}` ({$this->_columns}) VALUES (?,?,?,?,?)";
    }
    
    function _doInsert(&$obj) {
        $timer =& $obj->getTimer();
        $user  =& $obj->getUser();
        return array($obj->getID(), $obj->getValue(), $obj->getCreation(),
          $timer->getExpiration(), $user->getID());
    }
    
}

?>
This code is doing a lot of stuff, including automatic, randomly executed expiration queries and identity map registration. Imagine if a user decided they wanted to use flat files...

Posted: Sat Jun 03, 2006 3:38 pm
by santosj
I see, so the Mapper would be easy only if there doesn't need to be any changes. I had it the other way around then. Both are still nice and I wouldn't mind playing around with it in my mind. Probably drive me even more crazy, but that is okay.

If you want people to use it, then make it easy to use.

Posted: Tue Jun 06, 2006 9:08 pm
by Christopher
Uh ... you guys went off and built yet another database abstraction layer? I think I must have totally misunderstood the goal of this thread.

Posted: Tue Jun 06, 2006 9:23 pm
by alex.barylski
Seriously, the reason why I like PHP is because if it crashes and burns you have some idea where you went wrong.
Huh...

I disagree...from other programming languages perspective...

Being loosely typed and very flexible has it's advantages...but knowing what went wrong isn't one of them :)

On the contrary...PHP like other laymen languages will let you blindly enter characters and likely output something...perhaps not useful...but still it's something...

IMHO, thats not very helpful in the sense you have suggested...

Cheers :)