PHP框架设计入门
原文地址:http://bbs.phpchina.com/viewthread.php?tid=59397
PHP框架设计入门之一:布局篇
(本文译自 Leon Bukhman 的 PHP Application Framework Design ,10/13/05)
引用:
This article describes the design of a complete application framework written in PHP. The reader is assumed to have a working knowledge of PHP. This part of the series describes the scope of the framework and goes over the initial class hierarchy. The following parts cover everything from session handling to creating page templates.
本文讲述了如何用PHP来做一个完整的应用框架设计。我们假定读者已有PHP的工作知识。系列的此部分讲述框架的应用和类的架构。接下来的部分将覆盖从会话处理到创造页面模板的内容。
引用:
Class Hierarchy
I like to start implementing large projects by drawing out the class diagrams and seeing how the components fit together. Luckily, as of version 4, PHP supports object oriented programming with all its bells and whistles. Although this article is written for PHP 4, you can accomplish all of the same results with PHP 5 (which is still somewhat bleeding edge at the time of writing this). Object oriented programming and design is beyond the scope of this article so I hope you are at least somewhat familiar with it. The point is that OOP is the key to developing large applications and web-development is no exception.
类的架构
在贯彻大项目时,我喜欢先画出类图,并看看怎样把各部分组件拼合在一起。幸亏,PHP从版本4开始具备了相应的功能支持面向对象编程。尽管本文是用PHP4写的,但你同样能用PHP5来实现所有的功能(在本文写作时PHP5还正待发布)。面向对象编程和设计的内容超出了本文的范围,希望读者至少在一定程度上熟识其相关内容。因为OOP是开发大型应用和web应用开发的关键,这点是毫无例外的。
引用:
We will want to keep the framework minimal for performance reasons yet scalable and flexible enough to handle any kind of project that you may encounter. To start of, lets agree that the logic for each and every page of you application will be encapsulated in a class. This class will inherit from the system base class, which will handle everything that you may want your application to do. The benefits of this approach are fairly obvious. First, you are encapsulating all of the complex code away from the project developers. Thus, you can rest assured that your new intern will not mess with your system classes (unless you want her to for some reason). All the pages in the application will be more consistent and you will not be copy-pasting the same code into every page. More importantly, if you decide to change some underlying functions of your application, you will not have to edit each page as long as you keep the interface to your system base class consistent.
为了提高效能,我们将尽可能把框架的结构最小化,但同时又有足够的扩展性和灵活性,以确保框架能满足以后各种项目的不同需求。开始之前,我们先预设一个处理逻辑,应用中的每一个页面都将被封装在一个类里面。这个类又继承了系统的基类,它将处理(此页面)应用要做的一切事情。这样做的好处是很明显的:第一,对项目开发人员封装了所有复杂的代码,这样就不用担心新同伴搞乱了系统架构 (除非你有意这样做)。第二,应用中的所有页面都能保持一致性,你再也不用去拷贝-粘贴相同的代码到每个页面。第三,更重要的,如果要改变应用中的一些功能,只有保持对系统基类的接口的一致性,就不用去编辑每一页了。
引用:
Now lets define what we want our system base class to do. Here is a basic list of features that I will go over in due time:
Establish the database connection
Define how the page will be laid out and print the HTML
Manage authentication and user session information
Define all your application’s core features
Here is the framework and all of it's glory:
现在确定一下基类要做什么。下面是准备讨论的一些基本特征:
建立数据库连接
确定页面布局和显示HTML
鉴证和用户会话信息管理
定义应用核心要素
下面是框架及其要素的实现:
复制内容到剪贴板
代码:
class_system.php - The base class which includes the following (基类包括以下部分)
|_constants.php - Application constants (应用常数)
|_functions.php - Generic functions (基本函数)
|_class_user.php - Session handling (会话处理)
|_class_template.php - Template handling (模板处理)
|_class_form.php - Form handling (表单处理)
Using this framework, all of your pages should have this structure:
此框架内,所有页面都有这样的结构:
复制内容到剪贴板
代码:
include "include/class_system.php";
class Page extends SystemBase {
//your code here
}
$p = new Page();
引用:
The idea here is that we want to minimize the time spent on the most common tasks such as creating new pages. In this case, all we have to do is include the base class (which will make sure that all the other necessary dependencies are also included), extend the base class, and simply make an instance of the class to get the ball rolling. This way you can start thinking about what the page will actually do rather than waste time with common tasks.
在这里,我们想要把像创造新页面这类最普通任务所花费的时间减到最小。在本例中,我们只需要包括基类,扩展基类,然后简单的把类实例化就可以玩下去了。这样大家就可以把精力放在研究页面具体要做什么上面,而不是把时间化在普通事务上。
Configuration Files and Useful Functions 文件配置和有用的函数
引用:
The simplest idea to explain is the need for a file that will contain the application’s configuration. We can go with XML or some other even more intricate technology that will inevitably make the framework less efficient but let’s start off with the basics. All we really need is a set of constants that define strings such as the database connection or installation paths. These are strings that we would not want to hunt down within each page of the application every time the database password is changed or the application is moved. The file constants.php is just that. The settings that I find most useful are the URL and system paths to the application.
为简单解释起见,先建立一个包含应用配置的文件。我们可以用XML或其他一些复杂的技术来做,不过这样框架的效率就会降低了,所以我们从最基本的技术开始。我们现在需要的只是一些常数用来定义像数据库连接或安装路径之类的字符串。我们不想在每次改数据库密码或者改变应用路径时都要在每页里翻来倒去的寻找修改的地方。只要把 constants.php 这个文件找出来就行了。通常我认为最有用的设定是URL和应用的系统路径。
引用:
On a similar note, there are some functions that we would like to always make available and I include them in the file functions.php. These functions are not made as methods of the base class because we would like to make them available to all the classes of our application (including those that do not extend the base class). I will go over the functions in this file when we need to use them. As a simple example, I include the me() function, which returns the file name of the current PHP file. (VB 6 programmers will remember that me is a reference to the current form).
同样的,有一些我们随时需要用到的函数,我把它们放在functions.php文件里。我们没有把这些函数作为基类的方法,是因为应用里所有的类都可能用到它们 (包括那些没有扩展基类的)。在需要用到时,我再讲这个函数。现在作为一个简单的例子,先包括一个me()函数,它将返回现在PHP文件的文件名。
复制内容到剪贴板
代码:
function me() {
return substr($_SERVER['PHP_SELF'], strrpos($_SERVER['PHP_SELF'],'/')+1,
strlen($_SERVER['PHP_SELF']));
} The Base Class
引用:
Now lets dissect the class_system.php file line by line. The first thing that we do is call session_start() which starts a new user session if none exists or does nothing otherwise (more on sessions in part 2 of this series). Then we include all the need libraries. There are two things to note about the way this is done. First, require_once is used which will only include the file once and throw an exception if the file is not available (as opposed to include which would only make a warning and proceed with execution of the page). The second thing to note is that absolute paths are used to include the files which are all in the same folder. This is done because we don’t want to have to reconfigure the server to get the application working (by changing the include path used in PHP). Relative paths would not work because the base class will be included from various other pages and the relative path would have to work for each page that includes the base class.
现在我们一行一行地来讨论system.php这个类。第一行调用了session_start() ,如果会话(session)不存在,它就打开一个新的会话(第二部分有更多内容)。然后把所有需要的库文件包括进来。这里要注意两件事情:第一,使用 require_once 只在第一次把文件包括进来,如果没有此文件,就抛出个异常 (而用include则之显示一个警告信息)。第二,把同一个文件夹里的所有文件都包括进来的绝对路径。这样做是因为我们不想为了应用而再配置服务器 (作PHP里改变包括的路径)。用相对路径行不通,因为基类会在各种不同的页面里用到,要为用到基类的每一页设定相对路径是很困难的。
复制内容到剪贴板
代码:
session_start();
$path = dirname(__FILE__);
require_once "$path/constants.php"; //defines
require_once "$path/functions.php"; //generic functions
require_once "$path/class_user.php"; //session handling
require_once "$path/class_template.php";//template handling
require_once "$path/class_form.php"; //form handling
(译注:在PHP5我们可以用 __autoload()的方法。)
The implementation of the system base class is not at all surprising if you consider the way we intended to use it. Since the code is only executed when an instance of the class is created, we put everything that we want to happen into the class’s constructor. Also, since the base class will not be able to know exactly how to render each page, we make the methods abstract and let polymorphism take care of the rest (i.e. the derived classes will override the base classes methods.) Here is a list of the methods called from within the constructor:
如果考虑到我们使用它的方式,系统基类的实施就没什么令人惊奇的了。因为只有在类的实例创造时,代码才被执行,我们可以把所有要用到的东西都放到类的构造器里。还有,因为基类不能确切的知道如何呈现出具体的每一页,我们要用到抽象的方法,并使用多态性来照料这一切。(如子类重载基类的方法。) 下面是构造器里要调用的一些方法:
复制内容到剪贴板
代码:
init() - initialize the page
authenticate() - perform authentication
handleFormEvents() - handle page submits
printPage() - output the HTML
destroy() - destroy page (close DB connection)
Unfortunately, PHP 4 does not enforce many OOP principles (note that PHP 5 has anew and much more robust object model). You can get everything to work,such as inheritance and polymorphism, but it takes some effort and some faith. For example, there is no concept of method protection so all methods (and attributes for that matter) are made public. Thisis a big no-no when it comes to OOP so a common convention is to prefix all methods that are intended to be private with anunderscore ( _ ) and then take it on faith that the users of the class will not call these methods. Another problem is that we cannot declare the class to be abstract (i.e. we do not want people to declare instances of the base class but rather force them to inherit from it). We can get around this limitation by including the following lines at the top of our constructor (you can read this article for an in-depth analysis of how this works). The code checks to see if the instance of the class is the base class and throws anexception.
复制内容到剪贴板
代码:
if (!is_subclass_of($this,'SystemBase')) {
trigger_error('Base instantiation from non subclass',E_USER_ERROR);
return NULL;}Database Access
引用:
The application framework should provide a clean interface to your data regardless of the type of SQLserver uses. It is also very desirable to have loose coupling between your application and your database backend. So, for example, if you ever decide to change the location of the database or even change the type of SQL server used, you don’t want to have to rewrite all your code. Luckily, someone has already tackled this problem for us and all we have to do is use the PEAR::DB module, which provides a consistent interface to several popular database servers. You can read all about it at the PHP Extension and Application Repository and I recommend reading the following PEAR::DB tutorial as well.
无论使用什么类型的SQL数据库,应用框架都应该对数据提供一个清晰的接口。在应用和数据库后台之间,最好能保持松散的偶合。比如说,如果用户打算要改变数据库的地理位置,或甚至于要改变SQL数据库的类型,你也用不着去改写代码。幸好,已经有人为我们处理好了这些问题了,我们要做的就是使用PEAR::DB模块,它对一些通用的数据库服务器都提供了统一的接口。你可以在”PHP扩展和应用分类“ 里阅读这部分,我建议大家也阅读下面的PEAR::DB辅导部分。
引用:
Assuming that the PEAR::DB module is installed and configured correctly, all we need to have a persistent database connection available in every page is a reference to a PEAR::DB object. We will create this object as a member variable in the System Base class and create the database connection inside the constructor. We already have the necessary constants to build a DNS string and there is also a function in functions.php that returns a pointer to a new PEAR::DB object.
假如PEAR::DB模块已经正确地安装和配置好了,在每一页里需要用到一个持续的数据库连接时,我们要做的就是引用PEAR::DB对象。我们将在系统基类里创建此对象作为一个成员变量,并在构造器里建立数据库的连接。我们已经有了建立DNS字串所需要的常量,并且在 functions.php 里也有一个函数可以返回一个指向新PEAR::DB 对象的指针。
复制内容到剪贴板
代码:
function &db_connect() {
require_once 'DB.php'; //pear db class
PEAR::setErrorHandling(PEAR_ERROR_DIE);
$db_host = DB_HOST;
$db_user = DB_USER;
$db_pass = DB_PASS;
$db_name = DB_NAME;
$dsn = "mysql://$db_user:$db_pass@$db_host/$db_name";
$db = DB::connect($dsn);
$db->setFetchMode(DB_FETCHMODE_ASSOC);
return $db;
}
The function imports the PEAR:DB library if it has not yet been imported, creates a DNS string, and connects to the database. All we have to do now is use this function in the System Base constructor as follows: $this->db = db_connect(); and we are done.
此函数在没有引进PEAR:DB库时予以引进,创建一个DNS字串,并连接数据库。我们要做的就是在系统基类的构架器里像这样:$this->db = db_connect(); 来使用此函数,而我们已经这样做了。
Optimization
You may think it is a bit too early to discuss code optimization at this point since we have yet to implement most of the necessary features. Code optimization is more of an art and I will not go into a line-by-line analyses of how we can achieve the same effect faster. The point here is that we already can tell what the major bottlenecks will be so let’s nip the problem now before its gets out of hand. The most obvious performance loss will be due to the persistent database connection that we establish for each page. In fact, if you make a few timing test to benchmark the performance of the framework you will see that establishing the connection alone take more time than everything else combined. Although it is a necessary evil and we will have to pay that price for it when using the database, not every page in our application will have to connect to the database. In fact, not every page will need toprint out HTML content (i.e. it may just process the request and redirect). So before we go on lets define a few constants for the base class to indicate that we don’t need a database connection or to print the page. You will simply define these constants in pages thatneed them.
也许你会认为现在讨论代码优化还为时太早,因为很多需要的特征还没贯彻。代码优化是一种艺术,我也不会一行一行的去分析怎么取得优化。关键的一点是,现在我们已经可以说出主要的瓶颈是什么,我们如何在其失控之前,把问题处理好。最明显导致效能丧失的是每一页都要用到的数据库的持续连接。
复制内容到剪贴板
代码:
define('NO_DB', 1) if persistent DB connection
is not needed
define('NO_PRINT', 1) if page does not get rendered
Summary
So far, we have laid the foundation for how our application will be haveand how the framework will be used. We established an OOP design for the application, defined some constants and function to expedite routine work, and added database support. Read the next part to see how to manage session data and users.
PHP框架设计入门之二:管理用户
In part 1, we covered the basic class structure of the framework andlaid out the scope of the project. This part adds session handling toour application and illustrates ways of managing users.
这是PHP应用程序框架设计系列教程的第二部分。在第一部分,我们已经介绍框架的基础类结构,并展示了项目的大体。这一部分,我们将在程序中添加会话处理功能,并演示管理用户的各种方法。
Sessions
会话
引用:
HTTP is a stateless protocol and, as such, does not maintain anyinformation about the connections made to the server. This meansthat, with HTTP alone, the web server cannot know anything about theusers connected to your web application and will treats each pagerequest as a new connection. Apache/PHP gets around this limitation byoffering support for sessions. Conceptually sessions are a fairlysimple idea. The first time a web user connects to the server, he isassigned a unique ID. The web server maintains session information ina file, which can be located using this ID. The user also has tomaintain this ID with each connection to the server. This is typicallydone by storing the ID in a cookie, which is sent back to the server aspart of the typical Request-Response1sequence. If the user does not allow cookies, the session ID can alsobe sent to the server with each page request using the query string(the part of the URL after the “?”). Because the web client isdisconnected, the web server will expire sessions after predefinedperiods of inactivity.
HTTP是一种无状态的协议,正因为如此,它没有包含任何与服务器连接的相关信息。这就意味着,HTTP是孤立的,web服务器并不知道用户与你web程序相连接的任何信息,并且服务器会将每个页面请求视为一个新的连接。Apache/PHP通过提供对会话的支持来避开这一限制。从概念上来说,会话是相当简单的。在一个用户第一次连接到服务器的时候,他被分配一个唯一的ID。web服务器在一个文件中维护会话信息(译注:即把会话信息存储到文件中),于是可以通过这个ID来定位用户信息。用户同样会在每次连接中维护这个ID。最典型的作法,就是将ID存储在cookie中,之后,这个ID会作为请求-应答序列的一部分发回给服务器。如果用户不允许使用cookie,会话ID同样可以在请求每个页面时,通过query字符串(即URL中?以后的部分)发回给服务器。因为web客户端会断开连接,所以web服务器会在一定周期后,使那些不活动的会话信息过期。
引用:
We will not go over configuring Apache/PHP in this article butwill utilize sessions to maintain user information in our application.It is assumed that session support is already enabled and configured onyour server. We will pick up where we left off in part 1 of this serieswhen we described the system base class. You may recall that the firstline in class_system.phpis session_start(), which starts a new user session if none exists ordoes nothing otherwise. Depending on how your server is configured,this will cause the session ID to be saved in the client’s cookie fileor passed as part of the URL. The session ID is always available to youby calling the build in function session_id(). With these tools athand, we can now build a web application that can authenticate a userand maintain the user’s information as he is browsing the differentpages on the site. Without sessions, we would have to prompt the userfor their login credentials every single time they request page.
我们不想在这篇文章中过多地谈论Apache/PHP的配置,除了利用会话来维护用户信息。我们假设会话支持功能已经开启,并在你的服务器上配置好了。我们将直接从本序列教程第一部分谈论系统基础类时,被我们搁在一边的地方谈起。你可能还记得class_system.php的第一行是session_start(),这一句的作用是,如果不存在会话信息,则开始一个新的用户会话,否则不做其他的事情。根据你服务器的配置,开始会话的时候,会话ID会被保存在客户端的cookie里或者作为URL的一部分进行传递。当你调用内建的session_id()函数时,总可以得到会话ID。通过这些工具,我们现在可以建立一个web应用程序,它可以对用户进行验证,并且能够在用户浏览网站不同页面的时候去维护用户的信息。如果没有会话,那么用户每一次请求页面的时候,我们就不得不提醒用户进行登录。
引用:
So what will we want to store in the session? Let’s start withthe obvious like the user’s name. If you take a look at class_user.phpyou will see the rest of the data being stored. When this file is included, the first thing that is checked is whether a user is loggedin (default session values are set if the users id is not set). Notethat the session_start() must have already been called before we start playing with the $_SESSION array which contains all our session data.The UserID will be used to identify the user in our database (whichshould already be accessible after part one of this series). The Rolewill be used to determine whether the user has sufficient privileges toaccess certain features of the application. The LoggedIn flag will beused to determine if the user has already been authenticated and thePersistent flag will be used to determine whether the user wants toautomatically be logged in based on their cookie content.
那么,我们应该在会话中存储什么信息呢?我们一下子就可以想到如用户名这类信息。如果你看一下class_user.php,你会看到其他要存储的数据。(在程序中)include这个文件的时候,首先会检查用户是否登录(如果没有用户id,那么会设置一个默认的会话值)。注意,session_start()必须在我们使用$_SESSION数组之前调用,$_SESSION数组包含所有我们的会话数据。UserID用来标识存储在我们数据库中的用户(如果您已经完成了本系列教程的第一部分,那么这个数据库中的数据应该可以访问了)。Role(角色)是用来检测用户是否有足够的权限去访问程序中的某一部分功能。LoggedIn标识用来检测用户是否通过验证,Persistent标识用来检测用户是否想依靠他们的cookie内容自动进行登录。
复制PHP内容到剪贴板
PHP代码:
//session has not been established
if (!isset($_SESSION['UserID']) ) {
set_session_defaults();
}
//reset session values
function set_session_defaults() {
$_SESSION['UserID'] = '0'; //User ID in Database
$_SESSION['Login'] = ''; //Login Name
$_SESSION['UserName'] = ''; //User Name
$_SESSION['Role'] = '0'; //Role
$_SESSION['LoggedIn'] = false; //is user logged in
$_SESSION['Persistent'] = false; //is persistent cookie set
}
User Data
用户数据
We store all the user data in our database in table tblUsers. Thistable can be created using the following SQL statement (mySQL only).
我们将所有的用户数据存储到数据库的tblUsers表,这个表可以使用下面的SQL语句来创建(仅限MySQL)
复制PHP内容到剪贴板
PHP代码:
CREATE TABLE `tblUsers` (
`UserID` int(10) unsigned NOT NULL auto_increment,
`Login` varchar(50) NOT NULL default '',
`Password` varchar(32) NOT NULL default '',
`Role` int(10) unsigned NOT NULL default '1',
`Email` varchar(100) NOT NULL default '',
`RegisterDate` date default '0000-00-00',
`LastLogon` date default '0000-00-00',
`SessionID` varchar(32) default '',
`SessionIP` varchar(15) default '',
`FirstName` varchar(50) default NULL,
`LastName` varchar(50) default NULL,
PRIMARY KEY (`UserID`),
UNIQUE KEY `Email` (`Email`),
UNIQUE KEY `Login` (`Login`)
) TYPE=MyISAM COMMENT='Registered Users';
引用:
This statement creates a bare-bones user table. Most of the fields are selfexplanatory. We need the UserID field to uniquely identify each user.The Login field, which must also be unique, stores the user's desiredlogin name. The Password field stores the MD5 hash of the user'spassword. We are not storing the actual password for security andprivacy reasons. Instead we can compare the MD5 hash of the passwordentered with the value stored in this table to authenticate the user.The user's Role will be used to assign the user to a permission group.Finaly, we will use the LastLogon, SessionID, and SessionIP fields totrack the user's usage of our system including the last time the userlogged in, the last PHP session ID the user had, and the IP address ofthe user's host. These fields are updated each time the usersuccessfully logs in using the _updateRecord() function in the usersystem class. These fields are also used for security in preventingcross-site scripting attacks.
这个语句建立了一个大概的用户表。大多数字段不言自明。我们用UserID这个字段来唯一标识每个用户。Login字段同样也必须是唯一的,存储用户使用的登录名。Password字段用来存储用户密码的MD5散列值。我们没有存储实际的密码是因为安全和隐私的原因。我们可以拿用户输入的密码的MD5散列值与数据表中的进行对比来验证用户。用户角色用来将用户分配到一个许可组。最后,我们用LastLogon, SessionID和SessionIP字段来跟踪用户对系统的使用情况,包括用户最后登录时间,用户最后使用的会话ID,用户机器的IP地址。用户每次成功登录后,会调用user系统类中的_updateRecord()函数来更新这些字段值。这些字段同时也可以用来保证安全性,保证不受XSS(跨站脚本)攻击。
复制PHP内容到剪贴板
PHP代码:
//Update session data on the server
function _updateRecord () {
$session = $this->db->quote(session_id());
$ip = $this->db->quote($_SERVER['REMOTE_ADDR']);
$sql = "UPDATE tblUsers SET
LastLogon = CURRENT_DATE,
SessionID = $session,
SessionIP = $ip
WHERE UserID = $this->id";
$this->db->query($sql);
}
Security Issues
Authentication
验证
Now that we understand the various security issues involved, lets lookat the code for authenticating a user. The login() function accepts alogin name and password and returns a Boolean reply to indicatesuccess. As stated above, we must assume that the values passed intothe function came from an untrusted source and use the quote() functionto avoid problems. The complete code is provided below.
现在我们已经了解了各种相关的安全问题,下面我们来看一看验证用户的代码。login()函数接收一个登录名和密码,返回一个Boolean(布尔值)来标明是否正确。正如上面所说的,我们必须假定传入函数中的值是来自于不可靠的来源,用quote()函数来避免问题。完整的代码如下:
复制PHP内容到剪贴板
PHP代码:
//Login a user with name and pw.
//Returns Boolean
function login($username, $password) {
$md5pw = md5($password);
$username = $this->db->quote($username);
$password = $this->db->quote($password);
$sql = "SELECT * FROM tblUsers WHERE
Login = $username AND
Password = md5($password)";
$result = $this->db->getRow($sql);
//check if pw is correct again (prevent sql injection)
if ($result and $result['Password'] == $md5pw) {
$this->_setSession($result);
$this->_updateRecord(); //update session info in db
return true;
} else {
set_session_defaults();
return false;
}
}
To logout, we have to clear the session variables on the server as well asthe session cookies on the client. We also have to close the session.The code below does just that.
用户注销的时候,我们要清理在服务器上的会话变量,还有在客户端的会话cookie。我们还要关闭会话。代码如下:
复制PHP内容到剪贴板
PHP代码:
//Logout the current user (reset session)
function logout() {
$_SESSION = array(); //clear session
unset($_COOKIE[session_name()]); //clear cookie
session_destroy(); //kill the session
}
Inevery page that requires authentication, we can simply check thesession to see if they user is logged in or we can check the user'srole to see if the user has sufficient privileges. The role is definedas a number with the larger numbers indicating more rights. The codebelow checks to see if the users has enough rights using the role.
在每一个页面都要求验证,我们可以简单地检查一下会话,看用户是否已经登录,或者我们可以检查用户角色,看用户是否有足够的权利。角色被定义为一个数字(译者注:即用数字来表明角色),更大的数字意味着更多的权利,下面的代码使用角色来检查用户是否有足够的权利。
复制PHP内容到剪贴板
PHP代码:
//check if user has enough permissions
//$role is the minimum level required for entry
//Returns Boolean
function checkPerm($role) {
if ($_SESSION['LoggedIn']) {
if ($_SESSION['Role']>=$role) {
return true;
} else {
return false;
}
} else {
return false;
}
}
Login/Logout Interface
登录/注销的接口
引用:
Now that we have a framework for handling sessions and user accounts,we need an interface to allow the user to login and out. Using ourframework, creating this interface should be fairly easy. Let us startwith the simpler logout.php page which will be used to log a user out.This page has no content to display to the user and simply redirectsthe user to the index page after having logged him out.
现在我们已经有一个处理会话和用户账号的框架了,我们需要一个接口,这个接口允许用户登录和注销。使用我们的框架,建立这样的一个接口应该是十分简单的。下面我们就从比较简单的logout.php页面开始,这个页面用来注销用户。这个页面没有任何内容展现给用户,只是在注销用户以后,简单将用户重定向到index页面。
复制PHP内容到剪贴板
PHP代码:
define('NO_DB', 1);
define('NO_PRINT', 1);
include "include/class_system.php";
class Page extends SystemBase {
function init() {
$this->user->logout();
$this->redirect("index.php");
}
}
$p = new Page();
引用:
First we define the NO_DB and NO_PRINT constants to optimize the loading time of this page (asdescribed in Part 1 of this series). Now all we have to do is use theuser class to log the user out and redirect to another page in thepage's initialization event.
首先,我们定义NO_DB和NO_PRINT常量来优化加载这个页面的时间(正如我们在本系列教程中第一部分描述的那样)。现在,我们要做的所有事情,就是使用user类来注销用户,并在页面初始化事件中重定向到另外的页面。
引用:
The login.php page will need an interface and we will use the system'sform handling abilities to simplify the implementation process. Details of how this works will be described in Parts 3 and 4 of thisseries. For now, all we need to know is that we need an HTML form thatis linked the application logic. The form is provided below.
这个login.php页面需要一个接口,我们使用系统的表单处理能力简化处理的实现过程。至于这个过程是如何运作的,我们将会在本系列教程的第三和第四部分详细介绍。现在呢,我们所需要知道的全部事情,就是我们需要一个HTML表单,这个表单与应用程序的逻辑相连接。表单代码如下:
复制PHP内容到剪贴板
PHP代码:
<form action="<?=$_SERVER['PHP_SELF']?>" method="POST" name="<?=$formname?>">
<input type="hidden" name="__FORMSTATE" value="<?=$_POST['__FORMSTATE']?>">
<table>
<tr>
<td>Username:
<td><input type="text" name="txtUser" value="<?=$_POST['txtUser']?>"></td>
</tr>
<tr>
<td>Password:
<td><input type="password" name="txtPW" value="<?=$_POST['txtPW']?>"></td>
</tr>
<tr>
<td colspan="2">
<input type="checkbox" name="chkPersistant" <?=$persistant?>>
Remember me on this computer
</td>
</tr>
<tr style="text-align: center; color: red; font-weight: bold">
<td colspan="2">
<?=$error?>
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" name="Login" value="Login">
<input type="reset" name="Reset" value="Clear">
</td>
</tr>
</table>
</form>
引用:
Now we need the code to log a user in. This code sample demonstrates how touse the system framework to load the above form into a page template,handle the form events, and use the user class to authenticate the user.
现在我们来编写用户登录的代码。这个代码演示了如何使用系统框架将上面的表单加载到一个页面模板中,以及处理表单事件、使用user类来验证用户。
复制PHP内容到剪贴板
PHP代码:
class Page extends SystemBase {
function init() {
$this->form = new FormTemplate("login.frm.php", "frmLogin");
$this->page->set("title","Login page");
if (!isset($_POST['txtUser']) && $name=getCookie("persistantName")) {
$this->form->set("persistant","checked");
$_POST['txtUser']=$name;
}
}
function handleFormEvents() {
if (isset($_POST["Login"])) {
if($this->user->login($_POST['txtUser'],$_POST['txtPW'])) {
if (isset($_POST['chkPersistant'])) {
sendCookie("persistantName", $_POST['txtUser']);
} else {
deleteCookie("persistantName");
}
$this->redirect($_SESSION['LastPage']);
} else
$this->form->set("error","Invalid Account");
}
}
}
$p = new Page();
引用:
Onpage initialization, the form is loaded into the page template, thepage's title is set and the user's login name is pre-entered into theinput field if the persistent cookie is set. The real work happens whenwe handle the form events (i.e. when the user presses a button tosubmit the page). First we check if the login button was clicked. Thenwe use the login name and password submitted to authenticate the user.If authentication is successful, we also set a cookie to remember theusers name for the next time. If the authentication fails, an error isdisplayed on the page.
在页面初始化时,表单被加载到页面模板中,页面的标题已经被设置好,如果有永久的cookie,那么用户的登录名还会被预先输入到输入框中。当我们处理表单的时候,我们就真正有事情要做了(比如:当用户按下一个按钮提交页面)。首先,我们检查登录按钮是否被点击了。然后,我们使用提交过来的登录名和密码来验证用户。如果验证成功,我们同时设置一个cookie来记住用户名以便下次使用。如果验证失败,则会在页面上显示一个错误。
Summary
总结
引用:
So far, we have laid the foundation for how our application will behaveand how the framework will be used. We added user managementcapabilities to our application and covered several security issues.Read the next part to see how to implement page templates and separateapplication logic from the presentation layer.
目前为止,我们已经谈论了我们的应用程序如何运转及框架如何使用的基本信息。我们为我们的应用程序添加了用户管理能力,并且谈论了几个安全问题。下一部分,将可以看到如何实现页面模板,从而将应用程序逻辑从表现层中分离出来。
安全问题
This seems like a logical place to address several security issues thatcome up when developing web applications. Since security is a majoraspect of user management, we need to be very careful not to leave anycareless bugs in this part of our code.
这一部分看起来应该来考虑几个在开发web应用程序会遇到的安全问题。因为安全性是用户管理的一个主要方面,我们得非常细心,不在我们这一部分的代码中留下任何因为粗心导致的bug。
引用:
The first issue that needs to be addressed is the potential for SQLinjection in any web application that uses posted web data to query adatabase. In our case, we use the login name and password supplied bythe user to query the database and authenticate the user. A malicioususer can submit SQL code as part of input field text and maypotentially achieve any of the following: 1) login without having avalid account, 2) determine the internal structure of our database or3) modify our database. The simplest example of this is the SQL codeused to test if the user is valid.
第一个要考虑的问题是,不管在任何web应用程序中都会遇到的——SQL注入攻击(SQL注入会发送web数据来进行数据库查询)。在我们的情况中,我们使用用户提供的登录名和密码来查询数据库进而验证用户。一个怀有恶意的用户可以提交SQL代码作为输入文本的一部分,从而可能达到下面的几个目的:1 不需要拥有有效的账号即可登录 2 探测我们数据库的内部结构 3 修改我们的数据库。下面是一个非常简单的例子,用来测试用户是否有效。
复制PHP内容到剪贴板
PHP代码:
$sql = "SELECT * FROM tblUsers
WHERE Login = '$username' AND Password = md5('$password')";
引用:
Suppose the user enters admin'-- and leaves the password blank. The SQL codeexecuted by the server is: SELECT * FROM tblUsers WHERE Login ='admin'--' AND Password = md5(''). Do you see the problem? Instead ofchecking the login name and password, the code only the checks thelogin name and the rest is commented out. As long as there is a useradmin in the table, the query will return a positive response. You canread about other SQL injection exploits in David Litchfield's publication.
设想一下,用户输入 admin'-- ,然后将密码框留空。服务器执行的SQL代码则为:SELECT * FROM tblUsers WHERE Login = 'admin'--' AND Password = md5('')。你是否发现问题了?代码不同时检查登录名和密码了,只是检查登录名,(因为)余下的部分被注释掉了。只要在表里面有一个admin用户,这个查询就会返回一个肯定的回答。你可以通过阅读David Litchfield的文章(http://blackhat.com/presentation ... u-05-litchfield.pdf)来了解其他的SQL注入攻击。
引用:
How do you protect yourself from this kind of threat. The firststep is to validate any data sent to the SQL server that comes from anuntrusted source (i.e. the user). PEAR DB provides us with thisprotection using the quote() function which should be used on anystring sent to the SQL server. Our login() function shows otherprecautions that we can take. In the code, we actually check thepassword in both the SQL server and in PHP based on the recordreturned. This way, the exploit would have to work for both the SQLserver and PHP for an unauthorized user to get in. Overkill you say?Well, maybe.
你该怎么样保护你自己的代码免受这种类型的威胁呢。第一步是检验任何从不可靠的来源(比如:用户)发送到SQL服务器的数据。PEAR DB中的quote()函数为我们提供了这样的保护,这个函数可用于发送到SQL服务器的任何字符串。我们的login()函数(译注:该函数请见下文)显示了我们可以采取的其他预防措施。在我们的代码中,我们在SQL服务器和PHP中(根据SQL服务器返回的记录)都检查了密码。这样的话,攻击必须同时对SQL服务器和PHP都有效,才能使一个未验证的用户登录进去。你想说这杀伤力太大了吧?是的,也许吧。
引用:
Another issue that we have to be aware of is the potential for sessionstealing and cross site scripting (XSS). I won't get into the variousways that a hacker can assume the session of another authenticated userbut rest assured that it is possible. In fact, many methods are basedon social engineering rather than bugs in the actual code so this canbe a fairly difficult problem to solve. In order to protect our usersfrom this threat, we store the Session IP and Session ID of the usereach time he logs in. Then, when any page is loaded, the users currentSession ID and IP address are compared to the values in the database.If the values don't match then the session is destroyed. This way, if ahacker gets a victim to log in from one machine and then tries to usethat active session from his own machine, the session will be closedbefore any harm can be done. The code to implement this is bellow.
另一个问题是,我们必须警惕会话窃取和跨站脚本攻击(XSS)的可能性。我不想过多地谈论一个黑客冒充其他已验证用户会话信息的各种方法,但确定的是那确实有可能。事实上,比起利用代码中的bug,许多基于社会工程学的方法更可以称得上是十分难解决的问题。为了保护我们的用户不受这样的威胁,我们在用户每次登录的时候存储他的会话IP和会话ID。然后,当页面加载完成,我们就拿用户当前的会话ID和IP地址和数据库中的值进行比对。如果不匹配,那么就破坏会话信息。这样子,如果一个黑客让一个受害者从一台机器上登录,然后试着从他自己的机器使用受害者的活动会话,那么在他做出任何破坏之前会话就会被关闭。具体的实现代码如下:
复制PHP内容到剪贴板
PHP代码:
//check if the current session is valid (otherwise logout)
function _checkSession() {
$login = $this->db->quote($_SESSION['Login']);
$role = $this->db->quote($_SESSION['Role']);
$session = $this->db->quote(session_id());
$ip = $this->db->quote($_SERVER['REMOTE_ADDR']);
$sql = "SELECT * FROM tblUsers WHERE
Login = $login AND
Role = $role AND
SessionID = $session AND
SessionIP = $ip";
$result = $this->db->getRow($sql);
if ($result) {
$this->_setSession($result);
} else {
$this->logout();
}
}
PHP框架设计入门之三:页面模板
引用:
This is part 3 of a multi-part series on the design of a complete application framework written in PHP. In part 1, we covered the basic class structure of the framework and laid out the scope of the project. The second part described methods for managing users and session data. This part describes a practical implementation of page templates and the separation of application logic from the presentation layer.
这是PHP框架设计入门系列教程的第三部分。在第一部分,我们已经介绍框架的基础类结构,并展示了项目的大体。第二部分叙述了管理用户和会话数据的一些方法。这一部分,我们谈论页面模板的具体实现及应用逻辑与表现层的分离。
Templates
模板
引用:
Wouldn't it be nice if all the pages on your site had a similar style? Yes, it would… but that’s not the only reason for using page templates. In fact, if thats all that you require, style sheets should solve the problem with much less overhead. If you are looking to add a simple header of footer to every page, Apache provides that functionality via server side includes (or you can use PHP to simply read in an file and output it to the top of each page). In the context of this framework, however, what we are trying to accomplish is a bit more sophisticated. In effect, templates allow us to add a separate presentation layer to our web application. This approach is similar (though much simpler) to the one employed in ASP.NET.
如果你网站上的所有页面都有一个相似的外观,这难道不是一件美妙的事情吗?是,确实是很美妙,但是那并不是使用页面模板的唯一原因。实际上,如果你的要求此限于此(译注:即页面有一个相似的外观),那么用样式表(CSS)就足够解决问题,并且可以使你少花费许多精力。如果你在寻找一种给每个页面加入一个简单的页头、页脚的方法,可以使用Apache提供的功能,那些功能通过服务端包含实现(或者你可以使用PHP简单地读入一个文件,然后输出文件的内容到每一个页面的顶部)。然而,在这个框架的中,我们所要尝试完成的事情要更复杂一些。从效果上来看,模板可以为我们的web应用程序加入一个分离了的表现层。这种方法与ASP.NET中实现的类似,但相比起来简单许多。
引用:
There are many template engines available for PHP but the approach we will use here is based on Brian Lozier’s article Beyond The Template Engine. The idea is that most of the existing template engines provide much more overhead that we want for what we need to accomplish. In fact, PHP can do what we need in just a few lines of code which open up a text file and replace all the place-holders with dynamic content. So if we encapsulate that functionality in a class and put a cherry on top we end up with class_template.php and a realization of a presentation layer in our application framework.
虽然有许多的PHP模板引擎可供我们选择,但我们这里还是以Brian Lozier的文章Beyond The Template Engine中谈论的模板引擎为基础。这样做的理由是,就我们需要完成的事情来说,许多现有的模板引擎都过于笨重,要有不少额外的开销。实际上,像打开文本文件并将所有的占位符替换成动态内容这些我们需要做的事情,只需要几行PHP代码就可以完成。所以,如果我们将那些功能都封装到一个类中,将一切都写进class_template.php,那真可以称得上是画龙点睛,这样子,我们的应用程序框架中就有了一个表现层的实现。
引用:
In short, we will achieve the following benefits from implementing templates as described below:
Limited overhead (compared to not using templates at all)
Separation of the business logic layer from the presentation
Each page has a consistent feel and functionality
Simplified site maintenance
简而言之,我们要实现的模板引擎要有以下的几个优点:
有限的额外开销(相对于完全不使用模板引擎而言)
业务逻辑层与表现层的分离
每个页面都有一致的体验和功能
简化站点维护
Inside the Belly of the Beast
深入核心
引用:
If you've been following this series from Part 1 you will notice that so far we have described a page as an object of a class which performs some operations but does not output anything to the screen (unless you decided to use your own poetic license to interprete the action of outputting to the client's browser as just another operation). The reason for this is that all aspects regarding the way the results is displayed are handled by the template engine and stored in the page template files. Each page will use the same template file (preferably) and embed the the dynamic content using the interface provided by the template class. Actually, in the final version of the application framework, each page reads in a generic template for the page layout and then nests another page template as a form. The form templates are unique to each page and allow for greater flexibility. For now, lets just keep things simple. Read Part 4 of the series to see how the forms are implemented.
如果你从第一部分一直看到这里,你会注意到,到现在为止,我们已经将页面描述成一个类中的一个对象,这个对象实现了一些操作,但却没有输出任何东西到屏幕上(除非你别出心裁地将输出结果(到客户端浏览器)的行为理解为另外的操作)。其中的原因是,结果显示方式的方方面面都由模板引擎处理,并且存储在页面模板文件中。每个页面(最好)都使用相同的模板文件,并通过模板类提供的接口插入动态的内容。其实,在我们最终版本的程序框架中,会有一个通用的模板来呈现我们每个页面的外观,然后往其中嵌入另外一个页面模板作为一个表单。表单模板对每个页面来说是唯一的,并且有比较强的灵活性。现在,我们一切从简,你可以阅读本系列教程的第四部分,了解表单是如何实现的。
引用:
Page templates, as implemented in this framework, are nothing more that PHP files. In fact, you can place any valid PHP command into these files but we will employ the honor principle to ensure that you don’t. After all, the goal was to separate the presentation layer from the application logic and placing code into these template files defeats that purpose. To embed the dynamic content into the templates simply place a PHP variable into the template file like so:
在这个框架中,已经实现了页面模板,说起来,页面模板也不过是PHP文件。实际上,你可以在这些文件中放入任何有效的PHP代码,我们只能与你做"君子约定"(honor principle)来确保你不会这么做。毕竟,我们的目标是将表现层从应用逻辑中分离出来,而将代码放到这些模板文件中则违背了我们的初衷。要将动态内容插入到模板中,只需要简单地像下面这样把一个PHP变量放进去:
复制PHP内容到剪贴板
PHP代码:
<?=$Title?>
引用:
Then, inside the class for this page, we can set the variable as follows:
然后,在这个页面的类当中,我们可以使用下面的语句来设置变量的值:
复制PHP内容到剪贴板
PHP代码:
$this->page->set("Title","Page Title");
.
引用:
Everything else is taken care of by the framework. All you have to do is specify the right template file to use and do write the line
echo $this->page->fetch($this->template);
when you are ready to output the result to the screen. (And even that part is taken care of for you in the base class.)
其余的事情框架都会处理好。你所要做的事情,仅仅是指定要使用的模板文件,然后当你准备把结果输出到屏幕上的时候,再写上
echo $this->page->fetch($this->template);
这样的一行就可以了(甚至那一部分,基类都为你处理好了)。
引用:
If using the template engine doesn't seem too difficult, look how easy it is to implement the engine itself. Since we are using PHP files as templates, the PHP parser will take care of all the hard work. All we need to do is maintain an array of values that we want to assign to the place-holders (i.e. when you use the set() method). Then we need to implement a fetch() method which will extract the values used for the place-holders into the local namespace, read the template file into an output buffer, and then return the results. Here is the code in all its glory:
如果使用模板引擎看起来并不太难,你可以看一看实现引擎本身有多容易。由于我们使用了PHP文件作为模板,PHP解析器会处理所有费事的工作。我们所要做的,只是维护一个要赋给占位符(比如:当你使用set()的时候)的数组的值。我们还必须实现一个fetch()方法,这个方法会提取占位符所要用到的变量,把它们放入到局部的命名空间中(译注:就是使那些变量在局部的作用域中有效),然后读取模板文件的内容到输出缓冲区中,再返回相应的结果。下面是实现的代码:
复制PHP内容到剪贴板
PHP代码:
function fetch($file) {
extract($this->vars); // Extract the vars to local namespace
ob_start(); // Start output buffering
include($this->path . $file); // Include the file
$contents = ob_get_contents(); // Get the contents of the buffer
ob_end_clean(); // End buffering and discard
return $contents; // Return the contents
}
引用:
This approach has several advantages compared to other template engines which implement their own parser to parse the page templates. First of all, we have all the power and speed of PHP at our disposal. If occasionally we have to sneak a little logic into the template files then we have the option to do so. Furthermore, the templates execute as fast as PHP itself so we are not adding much overhead to the generation of each page. Template engines that have their own parsers implemented in PHP are slower and those that are implemented in C require installing extra modules on the web server. Finally, the users of the template engine (i.e. the page designers) do not need to learn a new syntax to create the files (and if they know PHP then even better). All in all, this gives us a pretty good design, if I do say so myself.
相比于其他实现了他们自己的解析器来解析页面的模板引擎,这种方法有几个优点。首先,我们充分利用了PHP的功能强和速度快。如果有时候,我们不得不偷偷地将一些逻辑放到模板文件中,我们也可以做到。另外,模板的执行速度就跟PHP自身一样快,所以,生成每一个页面并没有给我们带来过多的额外开销。用PHP实现了自己的解析机制的模板引擎,运行起来要慢得多,而那些用C实现了的,都要求在Web服务器上安装扩展模块。最后,模板引擎的使用者(比如:页面设计师)不需要为了创建页面而学习一种新的语法(当然,他们如果懂PHP更好)。总而言之,如果要我说的话,这会给我们带来优美的设计。
Summary
总结
引用:
At this point we have a fairly usable framework. However, we have not addressed several key goals: managing data in each page every time the page is submitted and displaying the data uniquely for each page using page templates. Both of these issues are resolved in Part 4 of this series using Forms.
现在,我们已经有了一个相当好用的框架。然而,我们还没有考虑几个关键问题:管理每个页面提交的数据,使用页面模板为每个页面独特地显示数据。这两个方面的问题都将在本系列教程的第四部分(使用表单)解决
PHP框架设计入门之四:表单和事件
PHP, framework
This is part 4 of a multi-part series on the design of a complete application framework written in PHP. In part 1, we covered the basic class structure of the framework and laid out the scope of the project. The second part described methods for managing users and session data. The third part described a practical implementation of page templates. In this fourth and final section, we will expand on our implementation of page templates to provide the web application persistent form data capabilities. We will also apply the framework to built an error page and a page dispatcher.
这是PHP框架设计入门系列教程的第四部分。在第一部分,我们已经介绍框架的基础类结构,并展示了项目的大体。第二部分叙述了管理用户和会话数据的一些方法。第三部分谈论了页面模板的具体实现。在第四部分这个最后的章节中,我们将详细地讨论如何通过页面模板来为我们的Web应用程序提供持久型的数据。同时,我们会用我们的框架来创建一个出错页面和一个页面分发器。
Forms
表单
If you've read Part 3 and felt a little gypped, you are right. The fact of the matter is that our use of templates as described in that article is not quite practical. One template for the entire application. Really? Does that mean each page has to have exactly the same layout? Of course not. The part that was not entirely stated was that the page templates described the general principle for separating the presentation layer out from the application logic. Now that this is established, we can look into applying these templates more practically. For starters, it is reasonable to have a global template as long as there is some provision for customizing each page. Ergo forms.
如果你已经读完第三部分,并且有点上当受骗的感觉,那就对了。(你有这样的感觉)主要的原因是,我们使用的那篇文章中所描述的模板,并没有多少可以动手实践的地方。整个应用程序中就只用一个模板。真的吗?那是不是意味着每个页面都有一个完全相同的外观?当然不是。这不能一概而论,页面模板只不过是描述了将表现层从应用逻辑层分离的一般方法。现在一切就绪,我们可以动手将模板付诸实践了。只要我们为定制页面的可能做好了准备,那么在开始的时候,有一个全局模板是合乎情理的。Ergo forms
Forms are actually nothing more than a template within a template. In fact the class_form.php class actually inherits from the class_template.php class. In the global template we put a place-holder for the form, <?=$form;?>, and then in the code set the $form variable of the page template to the result of fetching another template for the form. The form template itself does not necessarily have to have an HTML form element; but, since web application frequently deal with dynamic content, you will usually want to place the form tag into the the
template anyway. Now that we have solved that problem lets discuss the real power of forms: data.
表单其实只不过是模板中的模板。实际上,class_form.php类继承自class_template.php类。在全局模板中,我们放置一个表单的占位符:<?=$form?>,然后,在代码中将$form这个页面模板变量的值设置为指定的表单模板的内容。表单模板自身并非一定要有一个HTML的表单元素。然而,因为web应用程序总是要处理动态内容,所以,无论如何,你都会经常将表单标签放入到模板中。现在我们已经解决了那个问题,接下来我们来讨论一下表单真正的灵魂所在——数据。
Persistent Data
持久型数据
One of the difficulties in writing web applications is maintaining persistent data across multiple submits of the same page. This is not a problem for thick client applications because all of the data is stored in the client's memory. Since HTTP is a stateless protocol, however, we have to collect all the variables that were submitted using the POST or GET methods and send them back to the client with every single post back. Traditional web sites don't have to do this because every page is designed to do only one task so it gets submitted only once. The difference is that we are writing a web applications where each page is a modular entity. If we were to write a separate page for every button that's contained on the page then we would have an order of magnitude more pages to maintain. Furthermore, the very concept of a button (in a traditional thick client sense) doesn't exist in HTML. Yes you have something that looks like a button control but all it does is submit a page. Its up to us to connect the server side logic and have it execute when that button is clicked.
编写web应用程序一个困难的地方,就是在同个页面的多次提交中维护持久数据。这对富客户端应用程序(译者注:如桌面应用程序即可视为富客户端应用程序)来说并不是什么问题,因为所有的数据都存储在客户端的内存中。因为HTTP是一个无状态的协议,所以,我们不得不使用POST或者GET方法来收集所有提交过来的变量,然后在每一次回发(post back)中,再将其发回给客户端。传统的web站点并不需要做这件事,因为每个页面都被设计为仅处理单一的任务,所以,只需要提交一次就够了。而我们现在编写的这个web应用程序与之不同,每个页面都是一个模块实体。如果我们为每个按钮编写一个单独的页面,那么我们可能就会有一大堆的页面要维护了。此外,传统意义上的(富客户端中的)按钮概念,在HTML中并不存在。是的,你要说确实有一些看似按钮的控件,但它所能做的,仅仅是提交页面。按钮被点击的时候,连接服务端逻辑,并执行相应的操作,这些事情都取决于我们。
Another point about data is that not all of it gets submitted during a HTTP connection. What about the data that is generated at the server and needs to be maintained inside each page. For example, lets say you are displaying a table of data and want to remember which column was used to sort the data. You can store this in a hidden input control inside the form; but, if your page has many of these kinds of properties, then you may not want the overhead and burden of maintaining all of them this way. What other choice do you have?
另外,在一个HTTP连接中,并非所有的数据都是来自于页面提交。比如服务器产生的需要在每个页面中维护的数据。举个例子,你正在展示填满数据的表格,并且想要记住哪一列是被用来排序数据的。你可以将这个信息存储在表单里头一个隐藏的输入控件中,然而,如果你的页面有许多的属性,用这样的方法去维护它们,可能会使你耗费精力,负担过重。有没有其他的方法呢?
If you look at the template class carefully, you will notice that everything that we are considering data is just a value that needs to be embedded somewhere within our page template. If we store this data across post backs, then we don't have to worry about maintaining the other data that gets submitted with each page. This is precisely the what the form class does. We save all the variables just before we output the form to the client and load them back as soon as the form class gets instantiated. All the form variables are stored in a hidden input control named __FORMTATE. If you are familiar with ASP.NET way of doing things then you will realize that this a very similar approach. Note that when using the framework you do not have to worry about how this happens because the form class updates the information accordingly and even updates the values that were submitted during the last post-back. The one cumbersome issue with this approach is that any form property that corresponds to an actual control on the form (i.e. an item that sends a name/value pair with the page request) needs to be indicated to the form class so that the form properties can be automatically updated with the latest value send back. This is why we need to pass a $form_controls array when instantiating the form class. The code needed to maintain persistent form data is reproduced below.
如果你细心地看一看template class(译注:在class_template.php中定义了这个类),你会注意到,我们只是将数据当成一个值,这个值会被插入到我们页面模板的某个地方。如果我们在回发的过程中,一直保存这个数据,我们就不必为维护其他提交过来的数据操心。这正是form class所要做的(译注:原文链接有误,应该是form class)。在我们将表单输出给客户端之前,我们保存了所有的变量,在表单类实例化之后,我们立即将这些变量加载回来了。如果你熟悉ASP.NET的话,我们的这种处理方法与它是非常相似的。需要注意的是,当你使用这个框架的时候,你不必操心这一切是如何发生的,因为form class(译注:原文链接有误,应该是form class)会相应地更新信息,甚至会在最后一次回发时,更新那些提交过来的值。但是,这种方法有一个令人讨厌的缺点,和表单中实际的控件相关联的表单属性(比如:页面请求时发送一个键/值对的数据项),都必须在表单类中指明,以便表单的属性能够自动被更新为最新回发过来的值。这就是为什么在实例化form类的时候,我们需要传递一个$form_controls数组。维护持久型数据的代码如下:
function _saveState() {
$this->set("__IsPostback",true);
$state = chunk_split(encrypt(serialize($this->state_vars),session_id()));
$_POST["__FORMSTATE"] = $state;
}
function _restoreState() {
if(isset($_POST['__FORMSTATE'])) {
$state = decrypt($_POST['__FORMSTATE'],session_id());
$this->vars = unserialize($state);
$this->state_vars = $this->vars;
}
}
function _updateState() {
if (is_array($this->form_controls)) {
foreach ($this->form_controls as $state_var => $control_name) {
$this->set($state_var, $_POST[$control_name]);
}
}
}
Encryption
加密
HTTP is inherently a very insecure protocol. It was designed, as were many Internet protocols in the 1980's, with a security model based on user trust. Decades later, this model fell apart as the popularity of the Internet grew (and the users became less trustworthy). Although, we can still send data in clear text when implementing public forums, banking and e-commerce applications require a great deal more security. Why do I bring this up now? For starters, because SSL does not solve all of our problems. Yes we can encrypt the communication channel between the server and client with confidence but what happens to the data once it reaches the client. Once a client opens a web site, the HTML code used to generate the page is always available in clear text even if the communication channel was encrypted. That means that any data that we send to the client can remain in their browser cache or in their cookies indefinitely (and its all in clear text)[url=#notes]1[/url]. All of this makes it easier for mal-ware and spy-ware programs to gather data about its victims. Even worse is the situation where you don't want the user to see what properties are stored in the form.
HTTP天生就是一种很不安全的协议。和20世纪80年代被设计出来的众多internet协议一样,它将安全模型架构在对用户的信任之上。数十年后,随着Internet的增长扩大(同时,用户变得更加不可信),这样的模型便面临着崩溃的危险。虽然,对于公共论坛(这样的web程序),我们仍然可以发送明文数据。但是,对于银行、电子商务这样的应用程序来说,就需要更高的安全性。为什么现在我要谈论这些呢?首要的原因是,SSL并不能解决我们所有的问题。确实,我们完全可以对服务端和客户端之间的通信进行加密,但那些已经发送到客户端的数据呢?尽管我们加密了服务端和客户端之间的通信,但当一位用户打开一个web站点,HTML代码生成的页面总是以明文方式呈现。那就意味着,我们发送到客户端的任何数据都有可能被保存在客户端浏览器的缓存中,或者可能保存在cookie中(都是以明文的方式保存)。这会给那些恶意软件或间谍软件收集用户(受害者)的数据提供便利。当你不想让用户看到表单中存储了哪些属性值的时候,情况会更糟。
Now that I have hopefully convinced you that encryption of our form data is necessary, lets look in to how we can implement it. You will notice that the _saveState() and _restoreState() methods reproduced above call the encrypt() and decrypt() functions respectively. These functions are implemented in the functions.php file and require the mcrypt library for PHP to be installed on the server. Lets look at how these functions work.
现在,我希望你深信,加密表单数据是十分必要的,下面就让我们来看一下,如何来实现加密。你会注意到复制在上面的_saveState()和_restoreState()方法,都分别调用了encrypt()和decrypt()函数。这些函数在functions.php中编写好了,函数使用了安装在服务器上的PHP的mcrypt库。下面,让我们来看一看这些函数是如何运作的:
//encrypt plane text using a key and an mcrypt algorithm
function encrypt($pt, $key, $cipher='MCRYPT_BLOWFISH') {
$td = mcrypt_module_open(constant($cipher), "", MCRYPT_MODE_OFB, "");
$key = substr($key, 0, mcrypt_enc_get_key_size($td)); //truncate key to length
$iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND); //create iv
mcrypt_generic_init($td, $key, $iv); //init encryption
$blob = mcrypt_generic($td, md5($pt).$pt); //encrypt
mcrypt_generic_end($td); //end encryption
return base64_encode($iv.$blob); //return as text
}
//decrypt ciphered text using a key and an mcrypt algorithm
function decrypt($blob, $key, $cipher='MCRYPT_BLOWFISH') {
$blob= base64_decode($blob); //convert to binary
$td = mcrypt_module_open(constant($cipher), "", MCRYPT_MODE_OFB, "");
$key = substr($key, 0, mcrypt_enc_get_key_size($td)); //truncate key to size
$iv = substr($blob, 0, mcrypt_enc_get_iv_size($td)); //extract the IV
$ct = substr($blob, mcrypt_enc_get_iv_size($td)); //extract cipher text
if (strlen($iv) < mcrypt_enc_get_iv_size($td)) //test for error
return FALSE;
mcrypt_generic_init($td, $key, $iv); //init decryption
$pt = mdecrypt_generic($td, $ct); //decrypt
mcrypt_generic_end($td); //end decryption
$check=substr($pt,0,32); //extract md5 hash
$pt =substr($pt,32); //extract text
if ($check != md5($pt)) //verify against hash
return FALSE;
else
return $pt;
}
复制代码
By default, we will encrypt the form data using the Blowfish algorithm with the client's unique (pseudorandom/dynamically generated) session ID as the key. The Blowfish algorithm was used because benchmarking showed that it ran in a reasonable time for encrypting both small and large amounts of data. Also, the cypher text produced is proportional in size to the plane text. These are all good qualities to have when we are planing to encrypt arbitrary amounts of data.
默认情况下,我们使用Blowfish算法,并用客户端唯一的(伪随机/动态产生的)会话ID作为加密密钥,来加密表单数据。之所以使用Blowfish算法,是因为基准测试(benchmarking)显示,在加密少量和大量数据时,这个算法所耗费的时间都比较合理。对于加密不确定数量的数据,这个算法给我们提供了良好的保证。
I will not get into the details of using the mcrypt library in this article (see the PHP manual for details) but will describe the general algorithm used for encryption. After the library is initialized using our key, we take the plane text and append the MD5 hash of the text prior to encryption. When we are decrypting the cypher text, we will take the decrypted text, separate out the MD5 hash that was appended (this is always 32 bytes in length), and compare that MD5 hash to the MD5 hash of the text just decrypted. This way even if someone manages to modify the the cypher text in a meaningful way, we will be able to see that it was changed. Also, since the MD5 hash contains 32 bytes of random data, adding it to the plain text will make cracking the code without the key very difficult. This approach will work well as long as the the hacker does not have access to the session ID of the client (which is not always a good assumption… expecially when the Session ID is stored in a session cookie on the client).
我不会在这篇文章中过多地谈论使用mcrypt库的细节(详细的使用方法请参考PHP手册),但我会谈论用来加密的常规算法。在mcrypt库用我们的密钥初始化后,我们将原始文本的MD5散列值添加到原始文本的前面,来进行加密。在我们解密的时候,我们从加密了的文本中分离出添加了的MD5散列值(MD5散列值总是32位的)和原始文本,然后将这个分离出来的MD5散列值与原始文本的MD5散列值进行比较。这样的话,即使有人能够修改加密后的文本值,我们也可以察觉出改动。同时,因为MD5散列值是32位无规律的数据,将它添加到明文中,如果没有解密密钥,就很难进行破译。只要黑客没有获取客户端的会话ID(我们不应该总做这样的假设,特别是当会话ID保存在客户端的会话cookie中的时候),那么我们的加密机制就会正常运作。
While we are on this subject, why not compress the data before/after it is encrypted and reduce the amount of data that is transmitted between the server and client. Well, to be honest, I tried. It turned out that the time it took to compress/decompress this data was longer that any amount of time that we would have saved by reducing the transition delay. In fact, the format of the serialized or encrypted data that we would be compressing is such that compression is not very effective in reducing the size (i.e. there are not many repeatable elements in the text).
既然我们谈到这里,那就顺便来谈谈为什么不在加密前(后)压缩一下数据,以减少服务端和客户端的数据传送量。老实说,我试过了。结果表明,压缩解压缩数据所耗费的时间比我们减少数据量所节约的传送时间还要长。实际上,串行化(serialize)或加密了的数据,压缩并不能有效地减小数据的大小(比如:文本中没有太多重复东西的)。
Form Events
表单事件
So we have our new page, it looks pretty, and is somewhat secure. What do we do when the user actually clicks on a button. If you look back to Part 1 of the series you will remember that the system base class has an abstract function handle FormEvents() which gets called as the page is processed. You can overwrite this function in each page and handle the events accordingly. For example, lets say you have a button on your page called cmdSearch and you want to see if it was clicked. All you have to do is test for isset($_POST["cmdSearch"]) inside the handleFormEvents() function.
现在我们的新页面看起来很漂亮,并且具有一定的安全性。那么,当用户真的点击一个按钮的时候,我们应该做什么呢。如果你回头看一看本系列教程的第一部分,你会记得系统的基础类中有一个抽象函数handleFormEvents(),在处理页面的时候会被调用。你可以在每个页面中覆盖(重写)这个函数,以更好地处理事件。举个例子来说,在你的页面上有一个按钮叫做cmdSearch,你想知道它是否被点击了。你所要做的事情,就是通过handleFormEvents()函数中的isset($_POST["cmdSearch"])语句来进行检测。
This approach looks simple enough but it only solves half of our problem. Any HTML element can trigger an onClick JavaScript event on the client. What if we want that event to be handled on the server side. Since the object clicked does not necessarily have to be a button, you wouldn't always be able to find out if it was clicked using the approach described above. To solve this problem, our form template will need two more hidden input objects: __FORMACTION and __FORMPARAM. We can test for these values when the page is submitted and simply set the values in JavaScript whenever any event is raised that needs to be handled by the server. To set these objects in JavaScript and then submit the form we would do something like:
这个方法看起来很简单,但它只是解决了我们问题的一半。在客户端的任何的HTML元素都可以触发一个JavaScript的onClick事件。如果我们想在服务端处理那样的onClick事件,那又该怎么办呢。因为被点击的对象并不一定是一个按钮,所以,如果你使用上面所描述的方法,并不总能知道那个对象是否被点击了。要解决这个问题,我们的表单模板需要两个额外的隐藏输入控件:__FORMACTION和__FORMPARAM。我们可以在页面提交的时候检测这些值,一旦(客户端)有任何需要服务端处理的事件产生,我们可以用JavaScript简单地 设置这些值。要在JavaScript设置这些对象,并提交表单,我们可以用像下面这样的代码:
function mySubmit(action, param) {
document.forms[0].__FORMACTION.value = action;
document.forms[0].__FORMPARAM.value = param;
document.forms[0].submit();
}
复制代码
Note that since our model supports only one form element per page, we can simply refer to the first form in the forms array when accessing the objects. In practice, you wouldn't want to have multiple forms on a single page anyway.
注意,因为我们的模型仅支持一个页面有一个表单,所以,在访问对象的时候,我们可以简单地通过forms数组来引用第一个表单(译注:即document.forms[0])。总的来说,在实际的应用中,我们并不会想在一个单一的页面上放置多个表单。
Error Handling
错误处理
Customized error pages are necessary for any web application in order to improve the users experience whenever the inevitable errors occur. So, since we have to build the pages anyway, lets demonstrate how to apply this framework to writing a custom error page which lets the user save the error that occurred into our database (whether you actually address the problem is up to you). Lets start with a file for the form template which will contain our user interface.
对任何一个web应用程序来说,总是不可避免地会产生错误,因此我们需要有自定义错误页面这样的功能,以便(发生错误的时候)提升用户体验。虽然我们已经建立过若干个页面了,但还是演示一下,如何使用这个框架来编写一个自定义的错误页面,这个错误页面可以让用户在遇到错误的时候,将所产生的错误保存到我们的数据库中(当然,是否考虑这个问题完全取决于你)。我们先创建一个表单模板文件,在文件中编写我们的用户界面,代码如下:
<form action="<?=$_SERVER['PHP_SELF']?>" method="POST" name="<?=$formname?>">
<input type="hidden" name="__FORMSTATE" value="<?=$_POST['__FORMSTATE']?>">
<div align="center" style="width: 100%">
<div style="height: 280px;">
<img src='<?=IMAGE_PATH."error/error".$err.".gif"?>' style="padding-top: 20px">
</div>
<div style="height: 120px">
<?=$message?><br><br>
<? if (isset($_POST["log"])) : ?>
Click <a href="#" οnclick="history.go(-2)">here</a> to go back.
<? else : ?>
If you think there is something wrong with the site, <br>
please submit the error for review so that it can be fixed. <br><br>
<input type="submit" name="log" value="Submit">
<? endif; ?>
</div>
</div>
</form>
复制代码
As you can see there is a little logic embedded in the template file but so be it. I didn't want to over complicate things by doing everything by the book (this is after all an example of how to mold the application framework to various needs). Now lets look at the main PHP file. The first thing that you will notice is that the NO_DB constant is defined because, in the usual case, we will not need a database connection. If the user wants to log the error, then we can create the database connection ourselves [url=#notes]2[/url]. Also notice that this page does not use the same generic template as the rest of the web site is using (the “error.inc.php” template is used to give the error page a special look). The rest of the page is fairly straight forward. We have to include the system base class in every page and then we derive our current page from the base class. In the init() method we indicate that we want to use a form and specify where the form template is located. Then, we check if this is the fist time that this page loaded (i.e. the user has not yet clicked any buttons on this page which would cause it to post back to itself). If so, we set the appropriate form properties. When the user clicks on the submit button, the handleFormEvents() function records the error in our database. Note that you will probably want to log a lot more information in your error pages.[url=#notes]3[/url].
正如你所看到的,我们在模板文件中放入了一些逻辑处理代码。我不想墨守成规,一切都按书本行事,那样会把问题复杂化(毕竟这只是一个例子,只是用来说明如何来架构一个框架,以满足各种各样的需求)。现在我们来看一看主要的PHP文件。首先,你会注意到,我们定义了NO_DB常量,通常情况下,我们不需要一个数据库连接。如果用户想记录下错误,我们可以自己创建一个数据库连接。同时,这个页面没有使用与web站点其余部分相同的通用模板(error.inc.php模板用来为错误页面展现一个特别的外观)。页面的其余部分十分明了。我们在每个页面中include系统基础类,然后让当前的页面类继承系统基础类。在init()方法中,我们说明了我们要使用一个表单,并且指定了表单模板的位置。然后,我们检测这个页面是不是首次加载(比如:用户还没有点击这个页面上的任何按钮,从而导致页面内容的回发)。如果是,我们就设置好适当的表单属性。当用户点击了提交按钮,handleFormEvents()函数就会将错误记录到我们的数据库中。当然,你可能想在错误页面类中记录更多的信息。
define("NO_DB", 2);
include "include/class_system.php";
class Page extends SystemBase {
function init() {
$this->form = new FormTemplate("error.frm.php", "frmError", true);
if ( !$this->form->IsPostback() ) {
switch($_GET['err']) {
//specify all the error which you want to handle here
case 404:
$title = _("File not Found");
$message = _("The URL that you requested, '") . $_SERVER[REDIRECT_URL] .
_("', could not be found.");
$this->form->set("err","404");
break;
case 500:
$title = _("Server Error");
$message = _("The requested for ") . $_SERVER[REDIRECT_URL] .
_(" caused a server configuration error.");
$this->form->set("err","500");
break;
default:
$title = _("Unknown error");
$message = _("An unknown error occured at ") . $_SERVER[REDIRECT_URL];
break;
}
$this->form->set("notes",$_SERVER[REDIRECT_ERROR_NOTES]);
$this->form->set("redirectURL",$_SERVER[REDIRECT_URL]);
$this->form->set("redirectQueryString",$_SERVER[REDIRECT_QUERY_STRING]);
$this->form->set("remoteIP",$_SERVER[REMOTE_ADDR]);
$this->form->set("userAgent",$_SERVER[HTTP_USER_AGENT]);
$this->form->set("referer",$_SERVER[HTTP_REFERER]);
$this->form->set("lang","en");
} else {
$title = _("Error Page");
$message = _("Thank you for reporting the error.");
}
$this->page->set("title",$title);
$this->form->set("message",$message);
}
function handleFormEvents() {
if (isset($_POST["log"])) {
$db = db_connect();
$Number = $db->quote($this->form->get("err"));
$Notes = $db->quote($this->form->get("notes"));
$RequestedURL = $db->quote($this->form->get("redirectURL"));
$Referer = $db->quote($this->form->get("referer"));
$sql = "INSERT INTO tblErrorLog(Number, Notes, RequestedURL, Referer)
values($Number, $Notes, $RequestedURL, $Referer)";
$result = $db->query($sql);
if (DB::isError($result))
die($result->getMessage());
$db->disconnect();
}
}
}
$p = new Page("error.inc.php");
The Page Dispatcher
页面分发器
Here is another example of how to apply this framework to a page that doesn't quite fit the mold. Look at the URL of the current page. Notice anything interesting. The entire site is written in PHP and yet you are looking at a .html file. I'll give you a hint, the HTML page is not actually there. In fact, the page that you looking at is called articles. The web server was configured to interpret the file as a PHP file even though it does not have a .php extention. Also, using the AcceptPathInfo On directive, you can configure Apache to backtrack along the requested URL until it find the desired file. So in this case, the HTML file is not found and the file articles is found so everything after the valid file name (i.e. /article_name.html) is passed into the articles PHP file as a query string. Why would we want such a convoluted system. Well, for starters, this hides the server's internal folder structure from the users. More importantly, however, it makes the URL more human readable than something like "articles.php?ID=12345") and increases the chances that search engines will cache the page. Many search engines don't cache dynamic pages and having a query string like "?ID=12345" is a dead give away.
这儿是另一个例子,它演示了如何将这个框架应用于一个 看一看当前页面的URL。注意任何有趣的东西。整个网站都是用PHP编写的,然而你却看到一个.html文件(译注:即整个URL看起来是指向一个html文件)。我给你一个提示:其实那里并没有一个真正的HTML页面。实际上,你看到的页面是articles。web服务器被配置成将文件解析为一个PHP文件,尽管(被解析的)文件并没有一个.php的扩展名。同时,你可以将AcceptPathInfo设成On,这样apache就会沿着URL(所指示的路径)一直往回查找,直到找到所需要的文件为止。这样一来,没有找到HTML文件,但找到了articles文件,在有效文件名之后的字符串(比如:/article_name.html)就会作为查询字符串(query string)传递给articles这个PHP文件。为什么我们要用这样一个拐弯抹角,令人费解的机制呢?首先,它对用户隐藏了服务器内部真正的文件夹结构。更重要的是,比起"articles.php?ID=12345"这样的形式,它使URL更具可读性,并增加了搜索引擎缓存页面的机率。许多搜索引擎并不缓存动态页面,像通过传递"?ID=12345"这样的查询字符串才能看到的页面,搜索引擎会完全对它置之不理。
How does one implement articles using this application framework? Because of the template engine, its actually quite simple. Take a look.
如何使用本应用程序框架来实现一个articles呢?通过我们已有的模板引擎,实现起来相当简单。下面是实现代码:
class Page extends SystemBase {
function init() {
//determine page from query string
$page = substr($_SERVER['PATH_INFO'],1,strpos($_SERVER['PATH_INFO'], '.')-1);
if (file_exists("templates/_$page.frm.php")) {
$this->form = new FormTemplate("_$page.frm.php", "frmArtile");
} else { //article not found
$this->redirect(BASE_PATH."error.php?err=404");
}
}
}
$p = new Page("view.inc.php");
Wrapping up
装配
By now, we have covered all the necessary aspects of designing web applications in PHP. Although the framework described here may not be exactly what you need, hopefully this series of articles provided a basic idea of the issues that need to be considered when writing your own web applications in PHP. Feel free to contact me if you have any questions or comments.
到现在为止,我们已经谈论了设计PHP Web应用程序所需要的方方面面。虽然这里描述的这个框架可能并不是你确切想要的,但我希望这个系列的教程,能够在你编写自己的PHP Web应用程序时,给你一个基本的参考,知道应该考虑哪些问题。如果您有任何问题或看法,欢迎随时跟我联系。