分类:
2009-04-01 09:01:49
This article series (starting with and continued in and ) demonstrates how to build an MVC web framework using PHP 5. This article explains the Model part of Model-View-Controller design.
The Model is where the majority of the application's logic sits. It is where you run queries against the database and perform calculations on input. A good example of what a Model would look like is a simple login script. The login script gets user input from a form, validates it against the database, and then logs in the user.
The first application using the newly created framework will be the users
module. I will be creating three Models:
Because I am introducing the idea of sessions and users into the framework, I will need to create a few more foundation classes as well as a table in a database. Before I go over the code of login.php, I'd like to walk through these classes.
FR_Session
FR_Session
is a wrapper for the built-in PHP sessions. The code isn't very involved and provides only basic support for starting, destroying, and writing sessions.
* @copyright Joe Stump
* @license
* @package Framework
* @filesource
*/
/**
* FR_Session
*
* Our base session class as a singleton. Handles creating the session,
* writing to the session variable (via overloading) and destroying the
* session.
*
* @author Joe Stump
* @package Framework
*/
class FR_Session
{
/**
* $instance
*
* Instance variable used for singleton pattern. Stores a single instance
* of FR_Session.
*
* @author Joe Stump
* @var mixed $instance
*/
private static $instance;
/**
* $sessionID
*
* The session ID assigned by PHP (usually a 32 character alpha-numeric
* string).
*
* @author Joe Stump
* @var string $sessionID
*/
public static $sessionID;
// {{{ __construct()
/**
* __construct
*
* Starts the session and sets the sessionID for the class.
*
* @author Joe Stump
*/
private function __construct()
{
session_start();
self::$sessionID = session_id();
}
// }}}
// {{{ singleton()
/**
* singleton
*
* Implementation of the singleton pattern. Returns a sincle instance
* of the session class.
*
* @author Joe Stump
* @return mixed Instance of session
*/
public static function singleton()
{
if (!isset(self::$instance)) {
$className = __CLASS__;
self::$instance = new $className;
}
return self::$instance;
}
// }}}
// {{{ destroy()
public function destroy()
{
foreach ($_SESSION as $var => $val) {
$_SESSION[$var] = null;
}
session_destroy();
}
// }}}
// {{{ __clone()
/**
* __clone
*
* Disable PHP5's cloning method for session so people can't make copies
* of the session instance.
*
* @author Joe Stump
*/
public function __clone()
{
trigger_error('Clone is not allowed for '.__CLASS__,E_USER_ERROR);
}
// }}}
// {{{ __get($var)
/**
* __get($var)
*
* Returns the requested session variable.
*
* @author Joe Stump
* @return mixed
* @see FR_Session::__get()
*/
public function __get($var)
{
return $_SESSION[$var];
}
// }}}
// {{{ __set($var,$val)
/**
* __set
*
* Using PHP5's overloading for setting and getting variables we can
* use $session->var = $val and have it stored in the $_SESSION
* variable. To set an email address, for instance you would do the
* following:
*
*
* $session->email = 'user@example.com';
*
*
* This doesn't actually store 'user@example.com' into $session->email,
* rather it is stored in $_SESSION['email'].
*
* @author Joe Stump
* @param string $var
* @param mixed $val
* @see FR_Session::__get()
* @link
*/
public function __set($var,$val)
{
return ($_SESSION[$var] = $val);
}
// }}}
// {{{ __destruct()
/**
* __destruct()
*
* Writes the current session.
*
* @author Joe Stump
*/
public function __destruct()
{
session_write_close();
}
// }}}
}
?>
FR_User
FR_User
is a basic data object in the users
table. I created a single table called fr_users
in a MySQL database called framework
:
CREATE TABLE `fr_users` (
`userID` int(11) unsigned NOT NULL auto_increment,
`email` char(45) NOT NULL default '',
`password` char(15) NOT NULL default '',
`fname` char(30) NOT NULL default '',
`lname` char(30) NOT NULL default '',
`posted` datetime NOT NULL default '0000-00-00 00:00:00',
`status` enum('active','disabled') default 'active',
PRIMARY KEY (`userID`),
UNIQUE KEY `email` (`email`),
KEY `posted` (`posted`),
KEY `status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- Special user for anonymous/not-logged-in users
INSERT INTO `fr_users` VALUES (0,'anon@example.com','','Anonymous','Coward', \
'2005-06-05 11:33:56','disabled');
* @copyright Joe Stump
* @license
* @package Framework
* @filesource
*/
/**
* FR_User
*
* Base user object.
*
* @author Joe Stump
* @package Framework
*/
class FR_User extends FR_Object_DB
{
public $userID;
public $email;
public $password;
public $fname;
public $lname;
public $posted;
public function __construct($userID=null)
{
parent::__construct();
if ($userID === null) {
$session = FR_Session::singleton();
if (!is_numeric($session->userID)) {
$userID = 0;
} else {
$userID = $session->userID;
}
}
$sql = "SELECT *
FROM fr_users
WHERE userID=".$userID." AND
status='active'";
$result = $this->db->getRow($sql);
if (!PEAR::isError($result) && is_array($result)) {
$this->setFrom($result);
}
}
public function __destruct()
{
parent::__destruct();
}
}
?>
Notice that the constructor looks around for user IDs in various forms. If it receives a user ID, the class fetches the given user ID. If it does not receive a user ID, it checks the session; if a user ID is not present, it assumes the user is not logged in and it loads the special anonymous user record.
The special anonymous user allows me to have a special user for people not logged in. It allows me to easily and quickly check whether a user is logged in:
...
if ($this->user->userID > 0) {
// User is logged in
}
...
I would have much rather made FR_User
a singleton; however, PHP5 does not allow a child class to have a private constructor when the parent class, FR_Object_DB
, has a public constructor.
FR_Auth_User
Along with a session class and a user object, the code needs a proper authentication class to applications for users who are logged in. As a result, I created a new class called FR_Auth_User
and put it into includes/Auth/User.php.
Because my login.php module class sets a session variable called userID
to the user ID of the person logged in and that value is numerical, it becomes quite easy to validate that the user is logged in. The user ID of zero indicates an anonymous surfer, which means that if the user ID is greater than zero, the person is logged in.
* @copyright Joe Stump
* @license
* @package Framework
* @filesource
*/
/**
* FR_Auth_User
*
* If your module class requires that a user be logged in in order to access
* it then extend it from this Auth class.
*
* @author Joe Stump
* @package Framework
*/
abstract class FR_Auth_User extends FR_Auth
{
function __construct()
{
parent::__construct();
}
function authenticate()
{
return ($this->session->userID > 0);
}
function __destruct()
{
parent::__destruct();
}
}
?>
As you can see, the authenticate()
method simply checks to make sure the session variable is greater than zero.
That's everything necessary to create a program: a simple application that validates a login form. The minimum necessary files for creating an application in any module are:
For this specific module class, I have created the module users
in the modules directory. Thus the module class file is modules/users/login.php and the template file is modules/users/tpl/login.tpl.
[code for modules/users/tpl/login.php]
You may be thinking, I read 8,000 words for this? Consider what the framework allows you to do. When my module loads and I finally start programming, I don't have to worry about creating sessions, connecting to databases, opening log files, or authenticating. For once, I don't have to worry that I missed an include file or forgot a snippet of common code.
This, in a nutshell, is the beauty of MVC programming. Because the controller and views have already been programmed and are in working order, you can focus on what you get paid to do: creating applications. I remember the pain of creating large sed and awk scripts to change database connections, config file locations, and so on. I remember having a header.php and a footer.php at the top and bottom of every single script.
I remember having hundreds of files that ran a function check_user()
or something like that. What if I had to change that function name or integrate a different authentication method? If I added another check for a different authentication method, then that code/check would be run in every single file on every request. Now I just create a new FR_Auth_MyAuth
and change the applications that need that specific authentication to extend that auth class.
Create a user account in fr_users
for yourself, and go to the login module and log in. If you have the mod_rewrite rules turned on, you can simply go to . If you don't, then go to . Once you have logged in, you should be redirected to the home page, which should now recognize that you are logged in.
Before you click on the Submit link, right-click on it and copy the link to your clipboard. I'll explain why in a moment. OK, now go ahead and click on the link labeled Submit, which will log you out. (The links assume you have the rewrite rules turned on.)
The logout module should destroy your session and then send you back to the home page, which should no longer show that you are logged in. Now copy and paste the logout URL, and try to go to the module again. You should see the error "You do not have access to the requested page!"
Because the class logout
extends FR_Auth_User
, it requires that the user be logged in. The function FR_Auth_User::authenticate()
checks to make sure $this->session->userID
is greater than zero. When the controller runs logout::authenticate()
(inherited from FR_Auth_User
) and a person is not logged in, the function returns false
, which tells the controller to bomb out.
As a small example of how you can combine the various pieces to the MVC puzzle, I created a simple module class called whoami.php. The script simply displays who the user is. However, it checks for a GET
argument called output
. If output
is rest
, the module switches to the FR_Presenter_rest
view, which outputs the module's $data
variable in valid XML using PEAR's XML_Serializer
class.
* @copyright Joe Stump
* @license
* @package Modules
* @filesource
*/
/**
* whoami
*
* @author Joe Stump
* @package Modules
*/
class whoami extends FR_Auth_User
{
public function __construct()
{
parent::__construct();
}
public function __default()
{
$this->set('user',$this->user);
if ($_GET['output'] == 'rest') {
$this->presenter = 'rest';
}
}
public function __destruct()
{
parent::__destruct();
}
}
?>
Some people think that MVC frameworks are heavy. That may be; however, my own MVC application run web sites that regularly receive more than 10 million page views per month. If the load gets too heavy, I simply turn on caching in my presentation layer.
The gains I have experienced in my development cycles vastly outweigh the perceived problem of heaviness. If programmed and documented properly, MVC frameworks reduce the time you spend debugging your code; they increase your productivity; and they greatly reduce the barrier to entry for new and junior programmers.
I am working on a version of this framework that I am releasing under the BSD license. I plan on using PEAR to package it up nicely, though I doubt it will be an official PEAR package. Keep your eye on for updates. You can download and install it via PEAR right now if you wish.
is the Lead Architect for Digg where he spends his time partitioning data, creating internal services, and ensuring the code frameworks are in working order.