Finally, here’s a look at the code of header.php and footer.php: header.php
Note that the above code uses the wwwroot and dirroot parameters in such a way that header.php and footer.php can also be included by pages put in a subdirectory. The only “trick” is that all entries in menu_structure.txt are written as “absolute” paths, e.g.: menu_structure.txt .|TELUG|/index.php|Inizia da qui :-)|gnome-starthere-mini.png ^^^ [snip] ..|Marco Pratesi|/marco/|||linuxmarco ^^^ [snip]
If all pages to be linked by the menu are on the same site, an alternative (but more bandwidth consuming) solution may consist of using the setPrependedUrl() method (used in PgMarket). In Figure 6, you can see how the site looks on Netscape 4 (“vanilla” version, corresponding to “OLD” in lib/browser_detection.php) and on Links (“PLAIN” version) in Figure 7. As a final consideration for this section, you can note that a very deep customization of the menu look and behavior can be achieved through the “new generation” patch, which is very small. This illustrates the versatility of the PHPLM system and its approach. As the “new generation” patch is rather “quick and dirty”, in future versions I will change the code to integrate it in a clean way, without hardcoding HTML stuff into the class.
Figure 7 footer.php
Figure 6
December 2003
●
PHP Architect
●
www.phparch.com
40
FEATURE
The PHP Layers Menu Project Revealed
PHPLM in Ampoliros and the “OnClick” patch (by Alex Pagnoni) The Ampoliros PHP web application platform is one of the packages using PHP Layers Menu. It uses it extensively and the user friendliness of the interface benefits greatly from it. A little introduction to Ampoliros: Ampoliros was born to provide developers the opportunity to reuse their code at the maximum level and to let system administrators do real administration stuff with minimum installation and management efforts for the applications. Another Ampoliros goal is to administer not only a site, but an unlimited number of sites with a single Ampoliros installation; this is very important for service providers. I also wanted to offer end users a pleasant and uniform interface. PHP Layers Menu lends itself nicely to these goals. Adding menus to an application greatly improves its functionality and, thanks to the PHP Layers Menu template feature, the menus can also be aesthetically pleasing. You can see PHPLM in action on the screenshots page at the Ampoliros homepage (located at www.ampoliros.com/en/screenshots.php). A little note on how the Ampoliros user interface is designed: the HUI (Html User Interface) is its name and it is completely object oriented, organized in containers (like groups, tabs, tables, grids) and widgets (buttons, form elements, and obviously menus). Adding a new widget (or container) to the system is only a matter of creating a new file with a class inside it (this class must extend the HuiWidget or HuiContainer ones), composed by its constructor for initializing and passing the widget arguments and the Build() method that does the real stuff. Thanks to the HUI design, implementing PHP Layers Menu inside Ampoliros and adapting it was very easy. In fact I only had to copy the PHP Layers Menu original code into a new widget file (the “menu” widget), define a new HuiMenu class and then let the HuiMenu>Build() method create a new instance of the PHP Layers Menu main class passing the menu structure and taking its output. Then I adapted the menu templates to the Hui interface standards and to the Hui Figure 8 theme feature. Being based on and introducing open technologies and standards, Ampoliros uses the original PHP Layer Menu specifications for implementing a menu structure in the Hui interface. So, once you already know Ampoliros architecture and Hui API, the PHP Layers Menu documentation is the only
December 2003
●
PHP Architect
●
www.phparch.com
guide you need to implement menus in an Ampoliros module. Iacopo Spalletti’s patch Iacopo Spalletti has developed a patch that brings 4 new features to PHP Layers Menu: 1. two custom JavaScript functions are called on opening and closing layers, respectively; 2. a fixed ordinate can be set for all levels in a menu; 3. a unique DOM ID is associated to each arrow image to allow to manage arrows via JavaScript; 4. if URL is empty for an item, no “href” attribute is used in the corresponding anchor tag (this feature removes the Netscape 4 support). With regard to feature 1 in the Iacopo Spalletti’s patch, LayersMenu::setCustomJsFunction($name) and LayersMenu::setCustomJsFunction($name) allow us to define two JavaScript functions that will be called on the opening and on the closing of each level in a menu, respectively. The mentioned custom JavaScript functions must accept the layer name as their only parameter. Feature 2 allows us to force the same fixed (absolute) ordinate for the upper border of all the sub menu layers of a given menu, using LayersMenu::setFixedPos($menuName, $ordinateFixed) . Feature 3 allows you to do things like keep the arrow images hidden, showing them only when the mouse pointer passes over the corresponding link, as can be seen in Figure 8 from www.toscanateatro.it, where the patch has been used. Something very similar will probably be applied, together with the “new generation” patch, to the PHPLM 3.1.x development branch to add the possibility of “switching” between two arrow images when the mouse passes over an item.
41
FEATURE
The PHP Layers Menu Project Revealed
Trick: how to use the same database table for multiple menus As mentioned during our PgMarket discussion, PHPLM can retrieve menu data from a database, along with data for i18n support. However, the “vanilla” version implicitly foresees that a given table can store data only for one menu. Therefore, if you need to store data for two or more menus, you have to use two or more tables. For example, you can choose to use tables whose names are the same as the corresponding menus: “foo” and “foo_i18n” for a menu named “foo”, “bar” and “bar_i18n” for a menu named “bar”, and so on. However, you will see how some simple changes to the code allows us to put data for all menus in a single table. I prefer a PostgreSQL database, but for a MySQL database (or for another DBMS) there are only minor differences. We just add another field, i.e. the menu name, to the table(s), seen here in the start dump: —- DUMPS/pgsql.start.dump.orig +++ DUMPS/pgsql.start.dump @@ -8,6 +8,7 @@ CREATE SEQUENCE phplayersmenu_id_seq start 100 increment 1 maxvalue 2147483647 minvalue 1 cache 1 ; CREATE TABLE phplayersmenu ( + menuname text, id int2 DEFAULT nextval(‘phplayersmenu_id_seq’) NOT NULL, parent_id int2 DEFAULT 1 NOT NULL, text text, @@ -17,7 +18,7 @@ target text, orderfield int2 DEFAULT 0, expanded int2 DEFAULT 0, PRIMARY KEY (id) + PRIMARY KEY (menuname, id) ); CREATE TABLE phplayersmenu_i18n (
I have added the new “menuname” field to the primary key so we can use the same “id” for items belonging to different menus. Let us create the database and the table: bash$ createdb -U postgres phplayersmenu CREATE DATABASE bash$ psql -U postgres phplayersmenu < DUMPS/pgsql.start.dump
Now we have to add a simple condition to the WHERE clauses, and we’re done! —- lib/layersmenu.inc.php.orig +++ lib/layersmenu.inc.php @@ -889,6 +889,7 @@ $this->tableFields[“expanded”] . “ AS expanded FROM “ . $this->tableName . “ WHERE “ . $this->tableFields[“id”] . “ <> 1 + AND menuname = ‘$menu_name’ ORDER BY “ . $this->tableFields[“orderfield”] . “, “ . $this->tableFields[“text”] . “ ASC“); $this->_tmpArray = array(); @@ -909,6 +910,7 @@ $this->tableFields_i18n[“title”] . “ AS title FROM “ . $this->tableName_i18n . “ WHERE “ . $this->tableFields_i18n[“id”] . “ <> 1 + AND menuname = ‘$menu_name’ AND “. $this->tableFields_i18n[“language”] . “ = ‘$language’“); while ($dbresult->fetchInto($row, DB_FETCHMODE_ASSOC)) {
December 2003
●
PHP Architect
●
www.phparch.com
Future perspectives Some important issues have to be considered after the 3.0 release, starting from the 3.1 development branch. • Supported browsers
Until now, the set of supported browsers has been kept as broad as possible, with only one exception: the support of Netscape < 4.07 has been dropped starting from version 2.0, due to an evident bug for which there is no workaround. But, unfortunately, at this time, the support of some old browsers implies some constraints that prevent the implementation of a significantly better product. Moreover, to support such browsers, I have spent a large amount of testing and development time that could be dedicated to the development of new features. The following browsers are being considered for removal from the ‘supported’ list: • Netscape 4.x, whose DOM, unfortunately, is rather poor; • Internet Explorer 4, which is no longer supported even by Microsoft, and is very buggy and cranky, and whose DOM implementation is not standard compliant; • Opera 5 and 6 have many problems and bugs, also their DOM is still poor; • Konqueror 2.x that was still a “young” browser. In future releases, I will very probably drop the support of all of these browsers, in part because it would not reduce the number of supported platforms. For example, Mozilla supports an even larger number of platforms than Netscape 4, and all the named browsers can be upgraded without paying any fee. Furthermore, PHPLM provides “accessibility solutions” that allow support for any browser capable of handling simple HTML code. Of course, someone will be averse to this support removal, but, at the time of this writing, I am speaking about realtively few people. If you are among the ‘change averse’, please let me hear your voice and let me know the reasons why you disagree, either participating in the discussion on the sourceforge forums dedicated to the project, or writing me an e-mail. By the way, a reasonable middle-ground solution could consist of leaving the current layers-based menus unchanged, renaming its methods to show that it is an “old” solution, and starting the development of a “new generation”, more modern, layers-based menu type, supporting only “modern” browsers. In any case, the final decision will be made by me. I won’t feel too horrible about it either way, since PHPLM is free software and its license allows everyone to reuse the code and/or to fork the development (and to continue it if one day I have to give up for whatever reason). I am not forcing anyone to accept my decisions. With the “call for flames” out of the way, I want to
42
FEATURE
The PHP Layers Menu Project Revealed
speak about some interesting features that can be added in the future and some possible changes. • Forms and windowed elements - the “see through” problem
I often receive bogus bug reports about a “z-index bug”, corresponding to the pass-through effect: layers are “perforated” by some form elements (known as “windowed elements”), as if they had a z-index value that places them below such elements. Well, this is not a z-index related problem, it is not even a bug of PHPLM. It is a known bug, or, if you prefer this term, a known limitation of some browsers. This happens each time a browser creates an element using a system’s built-in widget: the browser does not draw the region covered by the widget anymore, and the widget “perforates” the layers. This problem is markedly browser dependent: each browser is associated to its own set of “problematic” form elements. The most problematic browsers are Netscape 4, Konqueror, and Opera < 7, whereas Mozilla, Opera 7, and Safari seem to be the best guys from this point of view. I do not find a lot of problems in Mozilla 1.4 and Opera 7. As I have said, we are not talking about a PHPLM bug, we are dealing with browser limitations. However, a workaround could be implemented: each time a layer is popped up, the menu system could hide the page’s windowed elements that overlap with that layer and are problematic for the browser being used. Some interesting references about this topic: http://www.geocrawler.com/mail/msg.php3?msg_id=442660 6&list=119 http://www.webreference.com/dhtml/diner/seethru/ http://www.intranetjournal.com/ix/msg/36271.html
use tools like VIM, MC, and BASH even to prepare coffee). However, a GUI that allows the final user to prepare data without touching any menu structure file and/or any data dump could be useful and cool and could increase the user base. • Code reorganization
Probably I will reorganize the code a bit and the classes tree to use less code in building a single menu; currently, the code is better suited to use the package as in the demo, i.e. to put more than one type of menu on the same page. Conclusions These are the most relevant changes and improvements I am considering for future development of PHPLM. If you have other interesting ideas, feel free to submit them either on the sourceforge discussion forums or sending me an e-mail. A loud “ciao” to everyone! :-) Acknowledgments I would like to thank Alex Pagnoni and Iacopo Spalletti for their contributions to this paper.
References
• More comfortable layers menus
This is an important improvement that requires sufficiently rich DOM and CSS support and hence it would conflict with the option of supporting browsers like Netscape 4. To provide more comfortable menus and improve the imitation of traditional ‘system-like’ menus, I will have to do something like what I have done in the “new generation” patch.
DIJKSTRA - PHP Tree Menu, authored by Bjorge Dijkstra, whose home site was on www.zinc.f2s.com/scriptorium/index.php/treemenu; nowadays, this is a broken link, and, alas, I am unable to find that menu system elsewhere. SMITHMENU - The JavaScript Menu Component, authored by Gary Smith, whose home site was, very likely, on developer.netscape.com/viewsource/smith_menu/menu.html (sorry, I cannot find it anymore among my old bookmarks), that is now a broken link; currently, related URLs are: developer.netscape.com/viewsource/smith_menu2/menu.html and developer.netscape.com/viewsource/smith_menu/smith_menu.html
• A Graphical User Interface to build the menus
Some people have asked for a tool to input the menu data through a GUI (Graphical User Interface). Currently, the logical way to prepare data for the menus to be built is to write - by hand - the menu structure file, using a text editor, or, eventually, something like a spreadsheet. If you want to use a database rather than a menu structure file, the right conversion method allows you to obtain a data dump corresponding to the text file prepared by hand, as it is shown in an example script bundled with the package. In my opinion, preparing the menu structure is neither difficult nor annoying (maybe this is my opinion because I like to
December 2003
●
PHP Architect
●
www.phparch.com
DOM - Document Object Model (DOM) - www.w3.org/DOM/ HIERMENUS - HierMenus by DHTML Lab www.webreference.com/dhtml
About the Author
?>
Marco Pratesi lives near Teramo (Italia), where he was born in 1971. He is an Electronics Engineer and teaches at the universities of L'Aquila and Roma. In his spare time, he likes to develop and promote free software and documentation. He is a founding member of the Teramo LUG. He is also involved in the "Progetto Linguistico Italiano OpenOffice.org".
Click HERE To Discuss This Article http://forums.phparch.com/113 43
DBDesigner4
P R O D U C T
R E V I E W
http://www.fabforce.net
by Eddie Peloke
I
am, by nature, not a planner. I am the one in the development design meeting always asking..”ok...so when can we code?”. It is not that I don’t see the need for planning and the benefit of a well thought out design before coding begins, I just would rather be coding than writing design specs. That is why I am not a regular user of database modeling programs. Sure, I have used them in the past, but typically only to ‘reverse engineer’ an existing database into a model so the customer can see a ‘pretty picture’. One major excuse I have often used as to why I don’t use them is the price. Typical commercial products are just out of reach for the average programmer to use on their own. Another excuse is time. Typically your database will change tremendously once you begin actually writing code and unless you continually keep your database diagram up to date, you can find yourself with a backlog of catch up work. Fabforce.net has strived to solve both of these issues with their database design tool DBDesigner4. Fabforce.net calls DBDesigner4 “ a visual database design system that integrates database design, modeling, creation and management into a single, seamless environment”. DBDesigner4 was created, developed and optimized for MySQL although it does contain connection options for Oracle, MSSQL, and SqlLite. DBDesigner4 has some nice features (see end of review for full feature list) which will allow you to quickly and efficiently design your database as well as keep it updated. Best of all, DBDesigner4 is a free Open Source product licensed under the GPL. So, if database
December 2003
●
PHP Architect
●
www.phparch.com
design and maintenance is a part of your coding routine, pull up a chair and let’s get started. Get the Software DBDesigner4 is available via download from Fabforce.net. The software is available for Windows
QUICK FACTS Description: DBDesigner 4 is a visual database design system that integrates database design, modeling, creation and maintenance into a single, seamless environment. See the feature list at the end of this review. Supported platforms for DBDesigner4: • Windows 2000, and XP • Redhat Linux (7.0 and higher on i686) Price: FREE! (Donations are welcome!) http://www.fabforce.net/dbdesigner4/donations.php
Download Page: http://www.fabforce.net/dbdesigner4/downloads.php
Product Homepage http://www.fabforce.net/dbdesigner4/
45
PRODUCT REVIEW and Linux. Windows users are treated to a nice executable walking you through the installation process or you can opt for the zipped binaries. Linux users can find installations instructions on the Fabforce website. I chose the Windows executable and within a few minutes from download, DBDesigner4 was up and running. Let’s Start Modeling! When DBDesinger4 is started up, you will be taken into ‘Design Mode’. In Design Mode, you will see a nice uncluttered workspace with an empty canvas surrounded by the basic tools for the task at hand. The tools available to you are not as extensive as some of the commercial, and consequently more expensive products, but are enough for the average programmer. Design Mode allows you to build database models from scratch or reverse engineer an existing database (see Figure 1). Once placed on the canvas or model as the help describes it, DBDesigner4 allows you move the objects around and change their structure. You can add/delete columns, change column names and datatypes and
DBDesigner4
create indexes and foreign keys. Some of the objects, such as foreign keys, are automatically created when you use the Relation tool from the tools palette. Making a relationship between two tables creates a visual relation in the model and also automatically creates the foreign key in the table, depending on the relationship tool used. Other graphical tools include notes, images and regions all of which can be placed in your model to better visualize and annotate your database. Besides the graphical tools, there are also some nice edit windows where you can create and assign table indexes, datatypes, etc. much like you would find in any good db administrator. In listing 1 you will notice some interesting and helpful features to the right of your model. The first feature is a Navigator window which lets you quickly move around your model. With large models, this can be extremely helpful. Below the navigator you will find a Datatypes pane where you can get information on specific datatypes as well as edit their descriptions, add parameters, etc. Lastly, under the Datatypes pane you will find a tree view of your database model. So, your model is complete, now what? DBDesigner4 includes two nice features to help you
Figure 1
December 2003
●
PHP Architect
●
www.phparch.com
46
PRODUCT REVIEW transform your model into the actual meta information of the database, Database Synchronization and Create Table Scripts. As their name implies, one allows you to automatically make the changes to your database, the latter creates the necessary sql scripts so you can do it later (Figure 2). The other DBDesigner4 mode is ‘Query Mode’. One interesting feature of the Query Mode is the ability to let the application help you build your sql statements. Simply clicking within your model, you can build select, insert, update and delete statements. One down side to this, however, is that the statement may not be in the format you need. For example, when you choose to join two tables, it creates a statement with the older style join instead of joining using the join keyword. This may be fine if that is what you want but may not work for others. If you like to get your hands dirty and write your own queries, don’t worry, DBDesigner4 allows you to do that as well. No matter if you write them yourself or have the application help, any query can be executed against your database. DBDesigner4 has a nice results grid where you can scroll your data results, apply and discard changes (See Figure 3).
DBDesigner4
Figure 2
Figure 3
December 2003
●
PHP Architect
●
www.phparch.com
47
PRODUCT REVIEW
DBDesigner4
Figure 4
Figure 5
December 2003
●
PHP Architect
●
www.phparch.com
48
PRODUCT REVIEW Expanding DBDesigner4 supports the use of plugins which help to expand the product’s capabilities (Figure 4). DBDesigner4 currently ships with three plugins which are a nice compliment to the product, SimpleWebFront, HTMLReport, and DataImporter. FabForce.net notes SimpleWebFront “was designed to be an easy-to-use tool that allows the automatic creation of php-webpages. The created php-webpages allow you to insert/edit/delete and search for datatuples in your mysql-databases.” Note the ‘mysql-databases’ as SimpleWebFront only works with MySQL. HTMLReport as the name implies generates a simple HTML report showing the structure of your database. Lastly, DataImporter allows you to import data from other sources such as text files and other databases. Help! A general downfall of many open source products is their lack of good documentation. That is not the case with DBDesigner4. DBDesigner4 has a very nice html manual which covers all aspects of DBDesigner4 and should give you a great step in the right direction. The only downside of the documentation is that is isn’t searchable (Listing 5). What I liked Overall, I am very happy with the product. It did everything I asked and will suit my needs well. Design mode synchronization will help me keep my database and model easily in tune while the Query mode’s query execution capabilities will allow me to easily manage my data and possibly replacing my other MySQL administration tool. If you need help with your queries, the query generator can be a huge benefit. From the built in ‘optimize’ and ‘repair’ script generators to the ‘export model as MDB xml file’ option, DBDesigner4 has many ways to help you take your model into actual code. What I didn’t Like There aren’t may things about the product that I didn’t like but I do have a few gripes. For one, the product is tailored around MySQL, which in my case is fine but will not necessarily serve others. Second, the product has no UML support. Many of the commercial products, although much more expensive, not only allow for database modeling but also UML diagrams, flow charts, etc.
Conclusions Overall, if you use MySQL as your backend database than this is a great tool. It is light enough so that you can have a database model quickly and easily up and running, yet has enough features to keep the average
December 2003
●
PHP Architect
●
www.phparch.com
DBDesigner4
DBDesigner4 Feature List • Available on Linux / MS Windows • User Interfaced based on industry standard layouting software - Canvas navigation similar to Adobe Illustrator® and Photoshop® - Palettes (docked / floating) including “Bird Eyes view” - Available objects include tables, realtions, labels, regions, images - Extensive Drag’n’drop support - Extensive Popup-Menu support - Advanced Editors - UNLIMITED Undo-/Redo- functions - Copy-/Cut-/Paste clipboard functions (XML, DDL) - Align functions • Design Mode / Query Mode • Reverse engineering MySQL, Oracle, MSSQL and any ODBC databases • Userdefined schema-generation • Model-To-Database syncronisation • Index support • Automatic foreign key placement • Weak entity support • Standard Inserts storing and sync • Full documentation capabilities • Advanced model printing • Output as image • All MySQL datatypes with all options • User defined datatypes • Database storage, ability to save model within database • Network-/Multiuser access through database storage • Version control* • SQL Query Builder • SQL Command History • SQL Command storage within model • Plugin interface * Version control only available when using database storage
programmer happy. If you currently use one of the larger commercial modeling tools, DBDesigner4 may not have all the features you want but I find it well suited for the average programmer looking for a fast and easy way to design and maintain their MySQL database.
php|a
49
Customized Mailing List Management with PHPMailer
F E A T U R E
by Brent R. Matzelle While the mail() function may be sufficient for the simplest email needs of your application, more complex and demanding applications may benefit from a more flexible and robust solution, capable of grabbing user data from a pre-existing system, dealing with large amounts of user data and emails, and working in a consistent manner on any platform. This article discusses how the idea for PHPMailer came about, how it was created, how it is used, and why it is useful. Introduction Electronic mail, or email, is one of the primary technologies that helped the Internet become the popular global network that it is today. Even though many of the original standards have evolved since their creation in the 1980s, email is still fundamentally the same now as it was then. However, with time and maturity, implementing email has become a much more complex task. In order to send a simple email several events must occur. First, a mail client such as Outlook or Evolution creates an email message. The message is composed of a header and a body. The body is the portion of the email that is most visible to users. This contains the message content of the email. The header is appended to the top of the message and contains various bits of information used by mail servers and other mail clients to determine the destination of the email message and tells other mail clients to display the email. With a mail client you have some control over these headers with the “To”, “Cc”, or “Bcc” fields. After composing a message the client then sends the message to a mail server that is versed in the Simple Mail Transport Protocol (SMTP), the TCP based protocol that mail servers use to transport email messages between clients and each other. Once the message is successfully received, the SMTP server relays the message to the mail server of the destination domain using the information contained in the headers. The mail clients that most people are familiar with are the “fat client” types like Evolution. These clients work very well for composing and sending out individual December 2003
●
PHP Architect
●
www.phparch.com
emails. But what if you want to send out emails to a database of hundreds or thousands of users? If you used a fat mail client it would take a very long time to send out each personalized message individually. A very popular solution to this problem is using a mailing list program. This software allows one to manage the sending and customizing of large numbers of email messages all at once with a minimal amount of effort. There are several free mailing list managers available for download around the Internet. However, many of us do not have the luxury of using these managers because our user data is stored in our own custom system. If you are in this situation, then it is often easier to write your own mailing list management program. This article explains how to write your own mailing list manager from your own custom web system. The Solution About two years ago I started to look for ways to integrate PHP programming into an enterprise web environment. Electronic mail was and still is a very important part of each system that I worked with so I looked for the support that PHP had to offer. Some of you might be familiar with the mail function in PHP. It allows you to send a list of email addresses a subject,
REQUIREMENTS PHP: 4.2+ OS: Any Applications: N/A Code: http://code.phparch.com/18/6 Code Directory: mailing
50
FEATURE
Customized Mailing List Management with PHPMailer
message, and additional headers in a single function. This function allows you to send simple mail messages quickly. However, it becomes very cumbersome if you have many addresses, have to add your own headers, or if you want to add attachments. Furthermore, it is implemented differently on Windows and Unix platforms. The Windows version connects to a given SMTP server, while the Unix version uses the command-line sendmail executable to send messages. This means that your PHP-based mail application might react differently on a separate platform, something that the PHP language is usually known to avoid. After working for a while with the PHP mail function I realized that while it worked for the simplest of web site setups, its limitations for large complex projects would cause me problems in the future. As I normally do when I need something from PHP, I checked out the major free software sites and found several examples of email library software. None that I found truly met my particular needs. Since I had a lot of experience with email library software I thought that perhaps I could write one myself. On prior projects I had used a good object-oriented programming interface written by a proprietary software company. The properties and methods of this class suited me very well, so I modeled my own project with the same interface. The next step was to define the base requirements for the class. My primary concern was that it be scalable to a large number of users. Additionally, I work with PHP in both win32 and Linux environments, so I needed the class to work the same way in each environment. So, unlike the PHP mail function, it had to behave the same way in separate environments. The best solution was a class to send email via SMTP directly from PHP code. These primary goals make up the essence of the project that I later named PHPMailer. This project works to create a full-featured programmer’s library for sending emails. It abstracts all the processing necessary to write complex emails easily and flexibly. It is composed of two classes. The first is an SMTP class originally authored by Chris Ryan that I later adopted and enhanced. It provides a complete interface to sending mail messages via SMTP. Its guidelines closely follow RFC 821, the well known standards document that describes the SMTP protocol. This class gives PHPMailer very fine-grained control over sending messages over SMTP. The second layer is the PHPMailer class itself, which creates the structure of an email for the user. These classes combined offer the unique features of the PHPMailer project. The Full Featured Email Transfer Class PHPMailer was designed to give you complete programming control over sending email. I’ve tried to include lots of handy features. One nice feature is its ability to send to multiple address types. This includes December 2003
●
PHP Architect
●
www.phparch.com
the normal recipients (“To”), carbon copy (“CC”), and blind carbon copy (“BCC”). You can also send mail with either plain text or full HTML. The HTML can either point directly to images located on remote web sites or it can use embedded images that are attached directly to the email. This is useful if the user has email but has no direct Internet access (or is reading mail offline). PHPMailer has support for attachments of several types. The first is through ordinary files. It loads an attachment from a file located on a remote web server’s filesystem. It can also send attachments that are based on string or binary data. This is advantageous to use if you want to load an email from a data source other than the filesystem. For example, you could load a binary file from a remote source like a database. Another neat use would be to load a file from a remote FTP or HTTP server via the PHP fopen() function and then attach it to an email. That could be very handy! What if you have more than one SMTP server in your network and you would really like to use both of them? PHPMailer has a feature to meet that need as well. You can add multiple SMTP servers with multiple ports. While sending, if one mail server times out because of server overload or something, PHPMailer will switch to the alternate server. It works as a kind of simple SMTP failover system. Some mail systems use special headers to identify properties of an email. If this is the case in your system then you can use the PHPMailer method for adding your own custom headers. Unless simplicity is a must I would suggest sending via PHPmailer’s native SMTP capability. If that solution does not fit best with your configuration then there is another option in which PHPMailer can help you. By calling a single class function you can switch between several sending methods. The default method is to use the native PHP mail method. You can also choose to send via the sendmail executable (on Unix-like systems). Both of these methods tend to send emails faster if there are not many emails to send. Not all email clients support HTML email. There are several popular clients, like Mutt, which are designed to only read text-based email. In an attempt to cater to both mail clients that will only read text and those that can read HTML multipart-alternative emails were developed. This email message format allows you to send both a text and HTML version of an email in a single message. When the message is received by the client it can decide what type of message format it would like to display. This is a good option for mailing lists where you are not sure if the user can support (or wants to see) one or more of the formats. Structuring the Database So you have your own mailing system and you are interested in developing a mailing list program to send
51
FEATURE
Customized Mailing List Management with PHPMailer
out emails to a large number of your users. I will assume that you already have your own custom database that you use to store all your user information. Since each database is unique to you or your organization I will demonstrate the proper structure of a database through what I would consider to be a common setup. The database creation and access examples in this article are based on the MySQL database management server (http://www.mysql.com/). Support for this server is built into PHP by default and there are a huge number of people familiar with it so it was an easy choice for this article. If you have a different database then you will find that the examples can be quickly adapted for whichever one you use. An even better solution would be to alter my examples to use your favorite PHP database abstraction layer. For my example only two tables are needed to adequately handle the mailing list application. The first table I call subscriber (see Listing 1), as it represents the generic user data for your web system. The only other table is subscriber_email, which contains all of the information useful to PHPMailer. It contains columns to store the email address of the user, whether the user is subscribed to the mailing list and if they want HTML email. You will notice that two of the fields are SMALLINT types, the is_subscribed and is_html columns. I chose SMALLINT because these fields are to represent boolean values. When I want to determine if a user option is turned on I indicate this with the value of 1 (one) and off if the value is set to 0 (zero). This technique is probably familiar as it is used in many systems. Choosing an SMTP Server You simply cannot send mail efficiently without the help of a well designed SMTP server. You will find that there are many to choose from and each one usually fits into its own niche. For the purposes of this article I suggest a general purpose, speedy SMTP server. You will probably find dozens of free and commercial types out there. Postfix on Linux has always been my SMTP server of choice. It does an excellent job of queuing messages while keeping system resources low even under a high load. Nowadays most mail servers are configured by default to prevent untrusted sources from relaying mail Listing 1 CREATE TABLE subscriber ( subscriber_id INT PRIMARY KEY, first_name VARCHAR(70), last_name VARCHAR(70) );
●
PHP Architect
mynetworks = 192.168.0.0/24
This line means that Postfix should allow all IP addresses from the 192.168.0.x subnet to relay mail. Your mail server will no doubt have a different method to set this configuration. A quick browse through some documentation should get you the right setting. Writing the Mailing List Manager Now that we are finished with all of the external components required for the mailing list system we can get to the source code. The first step is to install the PHPMailer library. Download the latest version of PHPMailer from the project web site, phpmailer.sourceforge.net/, and extract all files from the archive. Copy the PHPMailer class files into the php.ini include_path as explained in the README. If you do not have control of the include directory and you are running PHP 4.0 or higher you can use this function to set it to the correct directory: ini_set(“include_path”, “.:/path/to/phpmailer/dir”);
As of the latest version of PHPMailer there is now multilanguage support. This allows developers to choose a default language for error messages at runtime. So if you have a system that supports more than one language you can switch to the users preference with a single call method called SetLanguage. All of the PHP source code for this mailing list manager is stored in the progress_meter.php file. To make configuration of the list manager easier I placed all variables that are specific to your environment at the top of this file (see Listing 2). All of the variables that start with “db_” are database related variables. For instance, I named my database “mailing_list” so I set the $db_database variable to this value. The $smtp_host variable is the name or IP address of your SMTP server. If you want to add more than one SMTP server then Listing 2
CREATE TABLE subscriber_email ( subscriber_email_id INT PRIMARY KEY, subscriber_id INT /* REFERENCES subscriber */, address VARCHAR(100), is_subscribed SMALLINT, is_html SMALLINT );
December 2003
through them. In the past SMTP servers were not configured in this way and email spammers would use them to send out loads of email without having to use their own resources and bandwidth. You will have to configure your SMTP server to relay email sent from your web server. I set this up on Postfix by allowing the machine on my local intranet to relay by adding one line to Postfix’s main.cf file like this:
●
www.phparch.com
// Configuration variables $db_host = “localhost”; // $db_username = “root”; // $db_password = “”; // $db_database = “mailing_list”; // $maximum_send = 2; // // each page $smtp_host = “192.168.0.1”; // // $log_path = “c:/temp/”; //
database host database username database password database name max number of emails to send load SMTP host name/IP address(es) path to the log file
52
FEATURE
Customized Mailing List Management with PHPMailer
you can if you separate each one with a semi-colon like this: $smtp_host
= “smtp.mydomain.com;192.168.0.50”;
The $maxiumum_send variable is the maximum number of messages that are sent each time the page is loaded. I will explain what this variable is for later. I will explain the $log_path variable in a later section as well. The List Manager Interface The list manager interface is very simple. The interface is HTML, with a form containing three fields (see mailing_list.html). Subscribers can choose to receive either HTML or text versions of the list email. These options are represented by a field for each choice. Each field points to a version of the list email of the appropriate type located on the web server. This enables you to write list emails in your favorite text or HTML editor. Then when you are done with each email you can just place it on the web server and point to the path with the list manager interface. Remember that the web server has to have access to the directory in order to read the file. The only other field is the Subject field. This field is used by PHPMailer to set the subject line for the outgoing emails. This subject is exactly the same subject line that you are used to seeing in your personal mail clients. There is a Send Messages button at the bottom of the page. When you click this the page will post to the progress meter page, progress_meter.php. This is where the mail sending actually takes place. The page sends out emails while displaying a progress meter showing the percentage of the total number of emails sent. Managing Large Lists Web servers and browsers are designed to quickly load individual pages. If a web server takes too long to send data to the browser two things can happen. Either the web server will send a timeout message to the browser or the browser itself will display a timeout message. Inevitably, the number of users in a system will grow to a much higher number over time. This can put a heavy load on a mailing list system. The more messages you have to send, the longer the web server will take to return a message to the web browser. As a result, you will eventually get to a point when timeout problems become a common occurrence. To combat this problem you could increase the timeout on both your web server and browser. This solution is not adequate for several reasons. First, it makes your software more difficult to port, since each web server has a different method to set the timeout. Also, you would not want to turn the timeout off because the timeout is good for preventing high server load if there is a processing
December 2003
●
PHP Architect
●
www.phparch.com
problem. You would need to configure all web browsers that used the interface to the higher timeout. Thankfully, there is a solution to this problem. Rather than sending all email messages at once, just send them out in smaller batches. If you only send out several messages at a time then the server will be finished with the processing much earlier so that neither the web browser or server timeout is ever reached. To accomplish this task all you need is a small amount of web programming trickery. If you take a look at the mailing_list.html page you will notice a hidden field called start_with is set to zero. This value tells the mailing list manager what subscriber record to start with so that it can send out all emails, but in much smaller numbers each time. The start_with variable gets incremented as emails are sent and then re-passed to the page as the new value. The subscriberRecords() method returns all subscribed user information in a set of records. The code then checks if the total number of subscribers is less than the value of $start_with. If the start range is higher, then all the emails have been sent. If not, it will continue to send messages. In the mail sending part of the routine (see Listing 3 on the next page) a new instance of PHPMailer is created. At this point several of the email properties are set. These include setting the subject of the email, the SMTP host, and the From address of the message. A call to mysql_data_seek() sets the subscriber list at the correct position so that the sending can commence. It is here that you can see what the purpose of $maximum_send variable is. The $sent variable that was initialized to zero earlier gets incremented whenever a message is sent. If this value is higher than $maximum_send then the current batch of emails is complete and the page can continue to show the current percentage in the progress meter. What value you should set as $maximum_send should reflect the capabilities of your mail and web server. If these servers are very fast then you can increase this number. Test it with different values to determine the optimal number for your system. When the current batch is finished sending, it will display the progress meter with the current completion statistic. There is a JavaScript method called pause that waits for one second then determines if the page should be re-posted if it is not totally complete. This is how the page knows to continue sending messages. A new feature of PHPMailer is the SMTP keep-alive functionality. It allows a programmer to maintain the connection to an SMTP server between each sending to eliminate the overhead of disconnecting and then reconnecting between each sending. This reduces the time that it takes to send mail while reducing the load. I am using this functionality to make list sending more efficient.
53
FEATURE
Customized Mailing List Management with PHPMailer
Customizing Emails A bit of customization in a web system can really improve the experience for users. This holds true in mailing lists as well. I added just a touch of customization to this example to demonstrate how you might do it yourself. There are many more ways to customize emails outside the examples that I give here. Just use your imagination and develop some of your own. If you take a look at the database structure you can see that we know a few things about each subscriber. We will use all of this information to customize the list email. In this example we know whether or not they requested HTML email. If you notice the main code loop (see Listing 3) the code checks if the is_html field is set to one (“1”). If it is, then I turn on HTML email with PHPMailer with the IsHTML() method. As an added touch I want to have the name of the person who I am sending the email to in the actual email. I do this by borrowing an idea from the template engines out there. You can see that in the message.txt and message.html files I have added {NAME} where a normal greeting might be. This identifier allows me to use the str_replace() function to replace this text with the name of the current mailing list subscriber. If you want to get really fancy with this functionality you can use a template engine like Smarty to accomplish this.
Catching Errors When you are dealing with sending a large number of emails, problems are bound to eventually occur. Issues like incorrect email addresses and mail server issues can be a cause of problems. If a problem occurs while you are sending messages you want to know about it but at the same time you do not want to stop sending out your mailing list of possibly thousands just for an issue with one user! The answer to this problem is a simple log file. Logging allows the code to continue sending messages while saving the error messages of any problems that occurred along the way. To handle this issue I wrote a function called logError() (see Listing 4) that saves all error messages to a mail_errors.txt file along with the time the error occurred. When an error occurs in PHPMailer the message is stored in the $ErrorInfo variable. All errors in PHPMailer bubble up to the Send() Article continued on page 66 Listing 4 /** * Log an error to a file. */ function logError($message) { global $log_path; $file = fopen($log_path . “mail-errors.txt”, “a”); $date = date(“Y-m-d H:i:s”); fwrite($file, $date . “ - “ . $message . “\r\n”); fclose($file); }
Listing 3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
= $start_with) { $mail = new PHPMailer(); $mail->Subject = $subject; $mail->SMTPKeepAlive = true; $mail->Host = $smtp_host; $mail->From = “
[email protected]”; $mail->FromName = “Mailing List Manager”; $mail->IsSMTP(); // turn on SMTP mailing // Load the text and HTML version from disk $text_version = loadFile($text_path); $html_version = loadFile($html_path); mysql_data_seek($res, $start_with); // start at next subscriber in list while($row = mysql_fetch_array($res)) { if($sent < $maximum_send) { $full_name = $row[“first_name”] . “ “ . $row[“last_name”]; $mail->AddAddress($row[“address”], $full_name);
December 2003
if($row[“is_html”] == “1”) // send HTML version { $mail->IsHTML(true); $mail->Body = str_replace(“{NAME}”, $full_name, $html_version); } else // send text version { $mail->IsHTML(false); $mail->Body = str_replace(“{NAME}”, $full_name, $text_version); } if(!$mail->Send()) { logError(“Sending to: “ . $row[“address”] . “, Error: “ .
●
PHP Architect
●
www.phparch.com
54
Friday January 22nd & Saturday the 23rd Vancouver, British Columbia, Canada http://vancouver.php.net
The conference will be held at Simon Fraser University's Harbour Centre Campus located in the heart of Vancouver's spectacular downtown. The conference will feature two tracks of sessions, one focused on technical discussions, the other focused on related technologies, and usage within the business and educational community. Tickets for this event will be $75 for students, and $150 for all other registrants. You can register online for the first Western Candadian PHP Conference at http://vancouver.php.net.
Gold Sponsors
Silver Sponsors
The Software Sleuth Tracking application defects through a system logging framework
F E A T U R E
by Graeme Foster
Your system has crashed. Your manager is yelling in your ears. Your job is hanging by a thread. To top it off, you have no clue as to what went wrong. What you need is a good, solid maintenance framework.
T
he performance of software systems needs to be monitored. If your clients are complaining that your application is too slow, or if your application has crashed, it’s already too late and you have no choice but to switch to “firefighting mode”. I’ve had my share of fighting fires—and expressions like “20 million policy holders aren’t going to get their annual bonuses unless you fix the code, Graeme” only elicit from my laconic answers like “So I guess that I won’t be going down to the pub just yet, then?” Having to deal with system crashes appears to be a common thread linking all the jobs I have done. Even though I have, for the moment, left the challenge of maintaining large systems behind, I still find that even my word processor will remind me that error-free systems don’t exist. Therefore, the proper maintenance of systems is crucial to the viability of any company—and maintenance should be an ongoing process, rather than something done when problems arise. A good maintenance plan requires a suitable set of tools to be put into place so that sufficient information will be available when problems arise, thus shifting the firefighting scenario to a proactive one. Inevitably, the firefighting will still be necessary, but if the tools are there then it is considerably easier to resolve whatever problems occur than having to pour over pages and pages of raw data (or, worse, trying to guess what went wrong). Building a maintenance framework tends to be an evolutionary process rather than a revolutionary one. New functionality is added to the framework when some spare time can be squeezed out of the normally
December 2003
●
PHP Architect
●
www.phparch.com
impossible deadlines we all have to deal with. Ideally, the whole framework should be designed at the very start of a project, but for many developers even that is an elusive luxury. A maintenance framework will essentially monitor and log information originating from three basic categories: • Error Messages • System Events • Session Records The gradual development of a framework requires consideration of many different design alternatives and eventually the choice of one that fits into the existing system without providing a crippled logging framework. Occasionally, this will require hard decisions that will require whole sections of the code to be rewritten—but, while this is definitely not good news, you’ll appreciate your work when the first problem arises and you will be able to quickly pinpoint its cause and address it (or even better, prevent the problem from arising in the first place through careful monitoring of your system’s performance). I already discussed the creation of a basic logging
REQUIREMENTS PHP: 4.2+ OS: N/A Applications: N/A Code: http://code.phparch.com/18/4 Code Directory: apptrack
56
FEATURE
Implementing Web Server Load Management
framework in my article entitled Maintenance from the Outset, which appeared in the August 2003 issue of php|a (available at http://phparch.com/issue.php?mid=12). Thus, I will use that framework as the starting point for this article, and build upon it so that the error logging functionality can be enhanced and the logging of system events can be added to the framework. Planning The existing error logging functionality works, but it is too restrictive. For example, it stores all error messages into the same log file, without making any distinction as to their origin or severity. When errors are displayed Figure 1
on the screen, their colour is the same for all types of messages. Turning error logging on or off requires poking around in the code. These and other customization options are prime candidates for a preference file! We’ll start by writing a preference class that the error logging functionality can reference to obtain preference settings. The preference class will read a preference file and then hold that information in a number of variables that the error log class can access. This begs one observation: the error log classes require file access, and so does the preference class. Thus, we can benefit by creating a class hierarchy with a class to perform file access as the parent and having the error logging and the preferences classes as its children. I have already written the file logging functionality— it was incorporated as part of the ErrorLog class. Therefore, our first step will be to shuffle some of the code around so that the functionality can properly be encapsulated in a different set of parent classes (come up with the idea of calling this refactoring and you’ll get a doctorate). Figure 1 shows a class diagram for the existing version of ErrorLog. As you can see, it features just two public interface methods—it’s the private methods that take care of performing most of the work. For example, there are methods responsible for handling file access, managing the file counter, displaying the error messages on the screen, and writing them to the file. Before diving into the code let’s look at what will be needed, which I have laid out in the good-looking diagram shown in Figure 2. We already have an error log class that writes errors to the screen and to a log file. As part of this article, we will be developing a system log that will record system access activity to an external file. In the not-so-distant future, we may well want to add a session class capable of recording the session informa-
Figure 2
December 2003
●
PHP Architect
●
www.phparch.com
57
FEATURE
Implementing Web Server Load Management
tion for each user—again, to a log file. Since all three classes need to write information to a log file, we will extract the common logic out of ErrorLog and add it to a parent class that we’ll call Logger, which, in turn relies on FileAccess to read and write data to a file. The logging facility itself consists of two files: one contains the logging information, while the other acts as a counter to keep track of how many entries have currently been written to this particular log. The Preference Class Hierarchy Let’s move on to the preferences class. It seems to me that, at a fundamental level, there are two types of preference settings in this project: those that are common to all the logging classes and those that are specific to each class. Therefore, as you can see back in Figure 2, we will add a Preferences class to hold the generic settings, and then write specialized classes, like ErrorPref and SystemPref, to handle the specialized preferences. For the sake of clarity, I will break down the creation of the preference functionality in two stages. The first stage will involve creation of the preference class itself, as well as the functionality necessary for storing and implementing the preference settings. The second stage will concentrate on the storage of specific preference settings in a file and the reading and writing of these preferences from and to the file. Figure 3
December 2003
●
PHP Architect
●
www.phparch.com
Initially, we will hard code the value of each attribute in the constructor of each preference class. While this is not very flexible, it provides a way of determining the various settings and moving them from the logging classes to their new home without making things too complicated right away. Since both the preference and logging classes will eventually need to perform file I/O, we’ll also remove the methods that control this functionality from the logging class and place them in the FileAccess class. Finally, since we are refactoring the ErrorLog class, we will remove the methods and attributes that will be common to all the logging classes and put them in a separate parent class. Therefore, to recap up to this point, we have three tasks to build our preference class hierarchy: • Create the preference hierarchy • Create a file access class • Create a logging class Almost all of the code that we need has already been written and currently resides in the ErrorLog. Thus, our main concern will be that of reorganizing the classes and making sure that we assign the proper responsibilities to each class. Ideally, each class should be given a single task (please note my use of the word ideally!), which should be clearly identified so that the class` areas of responsibility can be clearly outlined, as this makes coding much easier. When a class is designed to perform too many tasks, the boundary between them can become blurred, resulting in much more difficult maintenance, since a chance in one area could adversely affect the behavior of another. Obviously, this is not always possible or desirable, but from a design perspective it is usually a good rule of thumb. However, having too many classes will adversely impact the system’s performance, particularly in an interpreted language like PHP. If speed is your primary concern, then a compromise between ease of maintenance and performance will be necessary. Figure 3 shows our new class hierarchy in a bit more detail. As I mentioned earlier, FileHandler sits at the very top, providing the file I/O functionality needed by the other classes, like opening, reading from and writing to files, handling locking mechanisms, and so on.. The Logger class expands on its methods to add the generic management of log files, while Preferences provides the basic
58
FEATURE
Implementing Web Server Load Management
framework for handling the preference settings. Finally, ErrorLog is a specialized child class of Logger, while ErrorPref inherits Preference’s methods to provide preference settings to ErrorLog. On to the Code We’ll start off by creating the preference classes. Since initially these classes will not be using file access, we can, for the moment, make the Preferences class the root of our hierarchy, which will later be modified to include the File Handler class as we saw in Figure 2 above. The cPreferences class will just consist of a constructor whose job is to initialize all the relevant attributes. The code for this class is shown in Listing 1. As you can see, a few settings are defined here. The $Display and $File attributes determine whether logging should be performed to the screen and/or to a file; $PreError and $PostError provides starting and ending tags for the error display (thus making it possible to change colour and style of the output), and $Path contains the name of the folder where the log files will actually be created. The ErrorPreference class (an abridged version of which is shown in Listing 2) extends cPreference. Here, I have included all the code specific to the preferences used for error logging. At the moment, this class only has a constructor, like its parent; later on, we’ll also add get and set methods, so that the preferences can be retrieved and modified. Most of the code here is selfexplanatory; however, the usage of the $Level variable is a little complex and, therefore, I will briefly explain how this attribute works. Listing 1 1 ” 14 ,$PostErrorTag = “” 15 ,$Path = “log/” 16 ) 17 { 18 $this->Display = $display; 19 $this->File = $file; 20 $this->PreError = $PreErrorTag; 21 $this->PostError = $PostErrorTag; 22 $this->Path = $Path; 23 if (!file_exists($this->Path)) 24 if (@mkdir($this->Path) === false) 25 { 26 echo “Permission to create the directory $PreErrorTag”; 27 system (“pwd”); 28 echo “/{$this->Path} $PostErrorTag has been denied to the user “; 29 system (“whoami”); 30 } 31 } 32 } 33 34 ?>
December 2003
●
PHP Architect
●
www.phparch.com
The $Level variable is used to hold specific information dependent on the type of error that has occurred. It is an array that contains one element for each possible type of error. The types of errors that are currently supported are shown in the constants.inc file, which you’ll find in the Classes directory of the code associated with this article: // Error Levels define (“AUTH”,”Authorisation”); define (“VALID”,”Validation”); define (“USER”,”User”); define (“SQL”,”SQL”); define (“SEVERE”,”Severe”);
The way the $Level attribute is handled is undoubtedly quite ugly in that it is a nested array. The first level of the array relates to the error level, while the second level contains two elements: the first, called string, contains information on how to format an error mesListing 2 1 ” 11 ,$PostHTMLTag = “” 12 ,$WhichFiles = BOTH_FILES 13 ,$display = true 14 ,$file = true 15 ,$PreErrorTag = “
” 16 ,$PostErrorTag = “” 17 ,$Path = “log/” 18 ) 19 { 20 parent::cPreferences($display, $file, $PreErrorTag, $PostErrorTag, $Path); 21 $this->PreHTMLTag = $PreHTMLTag; 22 $this->PostHTMLTag = $PostHTMLTag; 23 $this->WhichFiles = $WhichFiles; 24 $this->Level= array 25 ( 26 “Validation” => array 27 ( 28 “string”=> array 29 ( 30 “file”=>true 31 ,”desc”=>”Validation: “ 32 ) 33 ,”HTML”=> array 34 ( 35 “display”=> true 36 ,”desc”=>$PreHTMLTag.”Validation: “.$PostHTMLTag 37 ) 38 ) 39 ,”Authorisation” => array 40 ( 41 “string”=> array 42 ( 43 “file”=>true 44 ,”desc”=>”Authorisation: “ 45 ) 46 ,”HTML”=> array 47 ( 48 “display”=> true 49 ,”desc”=>$PreHTMLTag.”Authorisation Error: “.$PostHTMLTag 50 ) 51 ) 52 ); 53 } 54 } 55 56 ?>
59
FEATURE
Implementing Web Server Load Management
Listing 3 1 ouch = new cSevereError; 14 $this->locking = $locking; 15 $this->errorStatus = NO_ERROR; 16 $this->retry = $retry; 17 $this->pause = $pause; 18 } 19 function AppendLine($file, $msg) 20 { 21 $handle = $this->OpenFile($file); 22 if ($handle == false) 23 return false; 24 else 25 return $this->WriteToFile($handle, $msg); 26 } 27 28 function OpenFile ($file, $mode = ‘a’, $force = true) 29 { 30 if (file_exists($file)) 31 { 32 $handle = @fopen($file, $mode); 33 if (!$handle) 34 { 35 $this->errorStatus = COULD_NOT_OPEN_FILE; 36 return false; 37 } 38 else 39 return $handle; 40 } 41 else if ($force) 42 { 43 $handle = @fopen($file, ‘a’); 44 if (!$handle) 45 { 46 $this->errorStatus = COULD_NOT_CREATE_FILE; 47 return false; 48 } 49 else 50 return $handle; 51 } 52 else 53 { 54 $this->errorStatus = FILE_DOES_NOT_EXIST; 55 return false; 56 } 57 } 58 59 function ReadFormat ($handle, $format= “%s\n”) 60 { 61 if ($this->locking) 62 { 63 $locked = flock($handle,LOCK_SH+LOCK_NB); 64 $attempts = 0; 65 while (!$locked and $attempts <= $this->retry) 66 { 67 usleep($this->pause); 68 $locked = flock($handle,LOCK_SH+LOCK_NB); 69 $attempts++; 70 } 71 if (!$locked) 72 return false; 73 } 74 $line = fscanf($handle, $format); 75 if ($this->locking) 76 { 77 $locked = flock($handle,LOCK_UN+LOCK_NB); 78 $attempts = 0; 79 while (!$locked and $attempts <= $this->retry) 80 { 81 usleep($this->pause); 82 $locked = flock($handle,LOCK_UN+LOCK_NB); 83 $attempts++; 84 } 85 if (!$locked) 86 $this->ouch->alert(“Failed to release a file lock”); 87 }
Continued on page 65 December 2003
●
PHP Architect
●
www.phparch.com
sage if it is to be represented as a plain string (as it is the case when it is written to the logfile). The second one, called HTML, holds formatting information to be used when an error message is shown through the browser. Both the {string} and HTML variables are themselves arrays—I told you it was ugly! It is these arrays that will hold the information used to control how the logging and displaying of errors is handled. The Preferences and ErrorPref classes hold the basic preference information. Obviously, we can add more preferences later, but for now it is important to see how the logging class must be modified so that it can work within our new hierarchy. The first step in this direction consists of splitting the functionality of the error logging class so that the file handling routines can be moved outside of it. The file handling class is about providing the functionality for file access. As you can see from Listing 3, the class that I have written is capable of all the basics, but could probably use some improvements in the future. At the moment, it is only capable of performing those functions that we need—for example, it can read from a file using a format string, we may need a new method for reading the entire contents of the file in one sweep. Now, on to the logging classes themselves. While the two preference classes are relatively straightforward, the Logging and ErrorLogging classes are a bit more complicated, since the latter overloads errors from the former. The constructors for the two classes, which are shown in Listing 4, illustrate how the responsibility of initializing attributes is handled by the class that defines them. Both constructors ensure that their respective parent is called; however, for example, the cErrorLog constructor needs to perform some work before it calls the cLogging constructor. The parent class is essentially an abstract class, and, since we will never create an instance of it, it is perfectly acceptable for the $pref attribute to be set to null. However, we will create instances of cErrorLog and, therefore, its constructor must be called with a valid reference to a preference Listing 4 1 Preferences = $pref; 8 $this->LogFileName = $fileName; 9 } 10 11 // The cErrorLog constructor 12 function cErrorLog ($fileName = “ErrorLog”, $pref = null) 13 { 14 if ($pref == null) 15 $pref = new cErrorPref(); 16 parent::cLogging($fileName, $pref); 17 } 18 19 ?>
60
FEATURE
Implementing Web Server Load Management
class. Because of this, cErrorLog::cErrorLog checks to ensure that the preference parameter is not null. This way, the cErrorLog object will always have a reference to a valid preference object. The logMsg method of cErrorLog is part of the class’ public interface. It overloads the method of the same name that it inherits from its parent. As illustrated in Listing 5, the error log class has its own preferences and, therefore, logMsg first checks with the $Preferences attribute to determine whether it needs to display or log the error message, and only then call the appropriate display and file access methods. Both of these private methods will, in turn, perform some initial work based on the preference settings and then call the parent method to complete the task. For example, the pDisplay method will display the error level using the colour designated for this purpose in the preference object, and will then proceed to get the parent class to actually display the message on the screen. The display and write methods of each class are shown in Listings 6 and 7. Setting the Preference Settings Now that we have established the preference hierarchy and incorporated the logging classes in it, we can look at adding functionality to enable the modification of the preference settings. First of all, we will ensure that the preference settings will be read from a file, rather than being hard-coded. This means that the preferences class will need to access files and, therefore, it should inherit the FileHandler class. We also need to design a format for the preferences file, and, following a well-established PHP tradition, I will use a plain text file that will contain
each preference setting and its corresponding value on the same line. Since there will be levels of nesting in the preference file, we can allow the use of whitespace indentation (although this is not essential, it will make reading the file much easier). What we do need is a start and end marker for the preference data; as you can see in Listing 8 (next page), we use a combination of [TagName]-[[TagName-End], for example [Preferences] and [Preferences-End]. Notice how the indentation helps when reading the file. As I have mentioned, indentation is not strictly necessary, since our code will look for the variable name, followed by an equal sign to signify the start of the item value and, finally a line feed to indicate the end of the value. Listing 7 1 Preferences->PreError; 6 echo $msg; 7 if ($break) 8 echo “
”; 9 echo $this->Preferences->PostError; 10 } 11 12 function pWrite($msg) 13 { 14 $IPAddr = (string)$_ENV[‘REMOTE_ADDR’]; 15 $timeStamp = getdate(); 16 $file = $this->Preferences->Path . $this->LogFileName; 17 $line = $msg . “\t” 18 . $IPAddr . “\t” 19 . $timeStamp[“mday”] . “ “ 20 . $timeStamp[“month”] . “ “ 21 . $timeStamp[“year”] . “ “ 22 . $timeStamp[“hours”] . “:” 23 . $timeStamp[“minutes”] . “:” 24 . $timeStamp[“seconds”] . “\n”; 25 $theresult = $this->pWriteFile($file, $line); 26 } 27 28 ?>
Listing 5 1 errLevel = $lvl; 6 if ($this->Preferences->Level[$this->errLevel][“HTML”][“display”]) 7 $this->pDisplay ($msg, $break); 8 if ($this->Preferences->Level[$this->errLevel][“string”][“file”]) 9 $this->pWrite($msg); 10 } 11 12 ?>
Listing 6 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Preferences->Level[$this->errLevel][“HTML”][“desc”] . “ “; parent::pDisplay($msg, $break); } function pWrite($msg) { $msg = $this->Preferences->Level[$this->errLevel][“string”][“desc”] . “\t” . $msg; parent::pWrite($msg); } ?>
December 2003
●
PHP Architect
●
www.phparch.com
61
FEATURE Let’s take a look at a couple of entries. The HTML description taken from the Validation section reads as follows: desc =
Validation:
Whereas the HTML description taken from the Authorization section looks like this: desc = $this->PreHTMLTag Authorization Error: $this->PostHTMLTag
The first value is a simple string, but the second one incorporates attributes of the preference class—thus making the framework’s logging capabilities a bit more flexible. To extract the preference settings, we need to read in the whole file and then extract from that the section of interest. Once we have the section, we can then extract the items from within its contents. This means that, as we mentioned earlier, we need to add a method to the FileHandling class that will read the entire contents of a file. We will also need a few methods in the preferences class: ReadPrefFile() takes care of reading a whole preference file, while GetPrefSection() and GetPrefDetail() extract a section from a preference file and an individual value from a section respectively. These methods are called by the constructor to read the information from the preference file (if it exists) rather than using the default values. If the need arises to create a new preference setting, it must be added to the preference file, and an extra call to the GetPrefDetails() method is required. The need for several nesting levels makes the preferListing 8 1 [Preferences] 2 Display = true 3 File = true 4 PreError =
5 PostError = 6 Path = log/ 7 [Preferences-End] 8 [ErrorPref] 9 PreHTMLTag =
10 PostHTMLTag = 11 WhichFiles = BOTH_FILES 12 [Level] 13 [Validation] 14 [string] 15 file = true 16 desc = Validation: 17 [string-End] 18 [HTML] 19 display = true 20 desc =
Validation: 21 [HTML-End] 22 [Validation-End] 23 [Authorisation] 24 [string] 25 file = true 26 desc = Authorisation: 27 [string-End] 28 [HTML] 29 display = true 30 desc = $this->PreHTMLTag Authorisation Error: $this->PostHTMLTag 31 [HTML-End] 32 [Authorisation-End]
December 2003
●
PHP Architect
●
www.phparch.com
ence file for the ErrorLog class a little more challenging, but not too difficult. I wrote a method, which you can see in Listing 9, to extract the values as needed. Most of this code is self explanatory—in fact; all it does is to break the task down by calling other methods. However, things get a little hairy when it comes to reading the desc setting for the HTML section, because the values contain references to variables and data members that are declared inside the code. Thankfully, PHP provides the eval() function, which makes it possible to evaluate PHP code stored in a string. Thus, we just need to ensure that we pass our setting value as part of a valid PHP statement in a string. To do so, we will be incorporating the setting value in a statement that assigns it to a PHP variable (in the format $v = “statement”;). Keep in mind that any variables used in the preference file must be properly referenced; therefore, if the variable whose value you want to have as part of your setting is an attribute of the current preference class, it must be inserted as $this>myVar and not just $myVar. System Event Logging Error logging is about capturing the error messages that a user sees during an application’s execution. This makes it possible both to assist in finding a solution to immediate problems (such as system crashes) and in identifying trends and perform an analysis of how the application is used. System logging, on the other hand, is about capturing information on how an application interacts with the rest of the system. Please note that I am using the word “application” here, since a system can be composed of multiple different pieces of software—includListing 9 1 GetPrefSection($Level,$start); 7 $pref = $this->$PrefFileSection; 8 // Now extract the string section 9 $this->GetPrefSection(“string”,$start); 10 $pref = $this->$PrefFileSection; 11 // Extract the file and desc details; 12 if ($this->GetPrefDetail($pref,’file’)==’true’) 13 $this->Level[$Level][“string”][“file”]=true; 14 else 15 $this->Level[$Level][“string”][“file”]=false; 16 $this->Level[$Level][“string”][“desc”]=$this>GetPrefDetail($pref,’desc’); 17 // Now extract the HTML section 18 $start = $this->GetPrefSection(“HTML”,$start); 19 $pref = $this->$PrefFileSection; 20 // Extract the display and desc details; 21 if ($this->GetPrefDetail($pref,’display’)==’true’) 22 $this->Level[$Level][“HTML”][“display”]=true; 23 else 24 $this->Level[$Level][“HTML”][“display”]=false; 25 $text = $this->GetPrefDetail($pref,’desc’); 26 eval(“\$detail=\”$text\”;”); 27 $this->Level[$Level][“HTML”][“desc”] = $detail; 28 } 29 30 ?>
62
FEATURE ing the client browser—that talk to each other in some way or other (e.g.: through reading and writing cookies, or by accessing a database). While intervening on a system that has already been written without a logging framework is always risky and difficult, implementing database logging can, in particular, pose some serious problems if the database functionality has already been written, since the database methods will already be scattered all over your code. Therefore, we will concentrate on this particular aspect of system logging. Logging can be performed in one of three ways: you can have the developers manually call the logging functionality as needed (e.g.: before or after each database call); you can create a set of logging functions that transparently perform database operations; or, you can write a set of database access functions that transparently log data as appropriate. The first approach relies on the developers’ ability to always remember to make logging calls after every database operation. This means that they would only have to forget to do so once to cause the logging record to become incomplete—and hence, to completely defeat the purpose of having one in the first place. The other two approaches are much better: the developer only needs to make one call and all necessary actions are performed in a completely transparent way. The question now is whether to embed logging in a set of database classes or the other way around. Technically speaking, the two methods are equivalent, but the idea of embedding the logging functionality in the database classes is, in my opinion, much better. With this approach, the developer is just concerned with constructing database calls, and the logging is performed automatically for them. Also, they can’t easily circumvent the mechanism, since a direct call to the logging functionality will not really produce any useful results. Again, my driving principle here is to make it as easy as possible for the logging to take place with minimum inconvenience for the developer—and with as little leeway as possible. Where and How Now that we have established where the logging will be performed, it’s time to determine how. Probably, the best way to do this is to use stored procedures that are executed as triggers and write to a log file as needed. Unfortunately, while this is an excellent solution, not all database systems support stored procedures; also, we’d need to remember to call the stored procedure from each new table—and this is already an approach that we have rejected earlier because it is difficult to maintain. Because Pear::DB provides an abstraction layer between the program and the database, it provides an December 2003
●
PHP Architect
●
www.phparch.com
excellent candidate where the logging calls can be placed. Assuming that all database calls are performed through Pear::DB, the logging operations will also be performed properly with minimal effort. Since Pear is written and maintained by someone else (for which I am most grateful), we can’t go and modify its classes directly, or any change in their code will cause our modifications to be lost (or, worse yet, force us never to upgrade Pear in the first place). Another problem with this approach arises when the system is being hosted by a third party and they don’t want Pear to be tampered with. Pear::DB provides an abstraction layer between the application and the database. By extension, we can add our own abstraction layer between the application and Pear’s database functions. Our class will be called by the application, perform the necessary logging and then pass the bucket on to Pear so that it will be able to do its database magic. Wait a moment, though... why not use inheritance? We could easily subclass the Pear code and simply come up with our own transparent logging mechanism, resulting in something that is a bit cleaner—and more “scientific”—than the approach I have chosen. Unfortunately, using inheritance probably isn’t the right call here. To understand why this approach would fail, it is necessary to look at how Pear::DB works. Pear::DB allows the developer to write a single code statement knowing that the code will then determine how to perform the actual database calls based on the database system that is being used. It does this by using what is known as a “factory design” pattern. In short, the abstraction layer consists of a few control classes and one class for each database that is supported. When a connection to the database is established, the control class creates an instance of the proper database class and returns it to the caller. Therefore, it would be necessary for us to write child classes for each specific database system supported by Pear—and, if the people developing Pear add support for another database, we would need to follow suit. Moreover, we’d still have to change the method used to connect to a database, and we have already decided that direct code changes to Pear are something we want to avoid. Before we reject inheritance altogether, we should probably look at one more possible solution, which is to insert the new classes in a different place in the Pear class hierarchy. The approach we just examined consisted adding the classes at the bottom of the hierarchy (that is, where the databases are actually accessed); a quick look at the hierarchy tree shows that making changes anywhere else will still require us to modify the Pear code—therefore, inheritance is definitely out of the picture. So, I guess it’s “humbug” to the scientific approach... and “thumbs-up” to my gut feeling that stepping out
63
FEATURE of the box is the right way to go. After all, I’ve been doing this for twenty years and by now if I weren’t prepared to step out of the box I’d still be using flowcharts while everyone else is down the pub. Therefore, we’ll build an abstraction layer between our code and Pear::DB. This abstraction layer will ensure that all the necessary logging is performed and that the appropriate database calls are made. The final design decision that we have to make concerns the question of how the abstraction layer knows that logging is to be performed. Since now through the preference management mechanism it is possible to turn logging on or off, the abstraction layer needs to be able to find out whether logging is currently enabled. I think a good solution to this problem would be to use sessions: if logging is enabled, we’ll store a logging object in a session variable, and the abstraction layer will only have to check whether that variable exists to determine whether logging is to be performed. By using session variables, we only need to instantiate the logging object once, rather than having to create a new object every time logging is required. This makes our interface to the logging tools a lot “cleaner”. However, the first goal is to make logging work— thanks to our evolutionary approach, we can worry about this whole session thing later on. The final class structure that we’ll use is shown in Figure 4. The SystemLog class will be responsible for the logging of system calls. It will be called by the “dummy” Pear::DB abstraction layer that we’re about to write. This layer, in tun, will interact with the session data to determine if a system log variable exists. If one does, it will send the required logging data to the SystemLog->msg() method, and then go on to call the appropriate {Pear::DB} methods. Finally, if logging is enabled, the result of the database interaction will also be logged via a second SystemLog->msg() message. In the diagram shown in Figure 4, our abstraction class has been given the same name as the Pear interface class. Naturally, this will not work once we get down to writing the actual code, since PHP will not allow us to declare two classes with the same name in
the same script. Therefore, I will call this intermediate class DBTemp—and, of course, this will require us to change all the source code if we are introducing the framework in an existing application so that our new class is used instead of Pear::DB. These changes are not complex, however: all we need to do is replace any reference to Pear::DB with DBTemp, and remove any include statement that deal with Pear from the source. On to the Code! The first step in creating our logging facility is to create some constants that will be used to indicate the type of system access that is being performed. I have created a number of different database access types, as shown in Listing 10. Next, it is necessary to create a preference settings class for SystemLog to use and add the preference for each type of system access that we have defined to the preference definition file. Doing so will be similar to what we have done for the ErrorLog class earlier, so I will limit myself to noting that you’ll be able to find the resulting preference class in the code that comes with this article. For the abstraction layer to work properly without making further changes to our code, it must closely mimic the way Pear::DB works. Therefore, it must be possible to call its connect method statically, and the latter must return an instance of the connect class. It will also need to transparently call the Pear::DB connect class and store the database connection in an instance variable, as shown Listing 10 1
Types (“DB_READ”,”ReadingDB”); (“DB_INSERT”,”InsertDB”); (“DB_UPDATE”,”UpdateDB”); (“DB_DELETE”,”DeleteDB”); (“DB_DROP”,”DropDB”); (“DB_WRITE”,”WritingDB”); (“FILE_READ”,”ReadingF”); (“FILE_WRITE”,”WritingF”);
Figure 4
December 2003
●
PHP Architect
●
www.phparch.com
64
FEATURE in Listing 11. The fact that a connect call is made is also logged as a database “read” action. This can be turned off by setting the display and file preferences to false for the ReadDB system access type. Given the limited scope of this article, I have only written two more methods in the abstraction layer to mirror the functionality provided by Pear::DB: query and isError. If any other methods are needed by you or your developers, you’ll have to add them in to the abstraction layer. The code for the query method is given in Listing 12. Here we perform a bit of magic by looking at the contents of the SQL statement and guessing the type of action being performed. Obviously, something this simple won’t work a hundred percent of the time, but it’s probably close enough for everyday use, particularly since the entire SQL statement is logged anyway. Incidentally, the query method can easily be improved by checking the result returned by Pear::DB, and creating a log entry for it; the additional code needed for this purpose is shown in Listing 13 (next page). Where to Go From Here Developing a framework is, literally, a never-ending job. While what we have developed here is a good starting point, there are still many more tasks ahead. For example, the preference classes need to provide a means of accessing the settings in a more organic way through a Listing 11 1 logMsg(“Connecting to $dsn”,DB_READ); 7 include_once “DB.php”; 8 $dbLog = new DBTemp; 9 $dbLog->db = DB::connect($dsn); 10 return $dbLog; 11 } 12 13 ?>
Listing 12 1 logDrop($SQL); 7 elseif (stristr($SQL,”DELETE”)) 8 $this->logDelete($SQL); 9 elseif (stristr($SQL,”UPDATE”)) 10 $this->logUpdate($SQL); 11 elseif (stristr($SQL,”INSERT”)) 12 $this->logInsert($SQL); 13 elseif (stristr($SQL,”SELECT”)) 14 $this->logRead($SQL); 15 else 16 $this->logWrite($SQL); 17 include_once “DB.php”; 18 return $this->db->query($SQL); 19 } 20 21 ?>
December 2003
●
PHP Architect
●
www.phparch.com
Listing 3: Continued from page 60 88 return $line; 89 } 90 91 function FileRead($filename) 92 { 93 $handle = $this->OpenFile($filename, ‘rb’, false); 94 if ($handle == true) 95 { 96 if ($this->locking) 97 { 98 $locked = flock($handle,LOCK_SH+LOCK_NB); 99 $attempts = 0; 100 while (!$locked and $attempts <= $this->retry) 101 { 102 usleep($this->pause); 103 $locked = flock($handle,LOCK_SH+LOCK_NB); 104 $attempts++; 105 } 106 if (!$locked) 107 return false; 108 } 109 $file = fread($handle, filesize ($filename)); 110 if ($this->locking) 111 { 112 $locked = flock($handle,LOCK_UN+LOCK_NB); 113 $attempts = 0; 114 while (!$locked and $attempts <= $this->retry) 115 { 116 usleep($this->pause); 117 $locked = flock($handle,LOCK_UN+LOCK_NB); 118 $attempts++; 119 } 120 if (!$locked) 121 $this->ouch->alert(“Failed to release a file lock”); 122 } 123 return $file; 124 } 125 else 126 { 127 return false; 128 } 129 } 130 131 function WriteToFile ($handle, $details) 132 { 133 if ($this->locking) 134 { 135 $locked = flock($handle,LOCK_EX+LOCK_NB); 136 $attempts = 0; 137 while (!$locked and $attempts <= $this->retry) 138 { 139 usleep($this->pause); 140 $locked = flock($handle,LOCK_EX+LOCK_NB); 141 $attempts++; 142 } 143 if (!$locked) 144 return false; 145 } 146 $result = fwrite($handle, $details); 147 if ($this->locking) 148 { 149 $locked = flock($handle,LOCK_UN+LOCK_NB); 150 $attempts = 0; 151 while (!$locked and $attempts <= $this->retry) 152 { 153 usleep($this->pause); 154 $locked = flock($handle,LOCK_UN+LOCK_NB); 155 $attempts++; 156 } 157 if (!$locked) 158 $this->ouch->alert(“Failed to release a file lock”); 159 } 160 if (!$result) 161 fclose($handle); 162 return $result; 163 } 164 165 } 166 167 ?>
65
FEATURE set of get and set methods. This will make is easier to manage and validate the settings, while at the same time also allowing the creation of a user interface for doing so interactively. In addition, to have a proper framework you’ll probably need to add session logging for file access and cookies and, for completeness, the database logging abstraction layer should be extended to cover all of Pear::DB functionality. The preference settings that I used for the system logging classes were similar to those used for error logging, but, probably a different set of values are required instead. For example, you may want to include extra information in the log messages, such as who the person issuing a database call is, whether they were logged on, and so forth. The one important lesson that I have learned from my years building logging frameworks is that you should never be afraid of changing existing code. If you’re using OOP, in particular, changing classes is often relatively painless, unless you start changing their public interfaces. A good design process requires looking at all the possible approaches, and always remembering that not all design decisions are clear cut—often, it all boils down to choosing the lesser of two (or more) evils.
Listing 13 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
$this->db->query($SQL);
if (DB::isError($result)) { $msg = $result->getMessage(); if (stristr($SQL,”DROP”)) $this->logDrop($msg); elseif (stristr($SQL,”DELETE”)) $this->logDelete($msg); elseif (stristr($SQL,”UPDATE”)) $this->logUpdate($msg); elseif (stristr($SQL,”INSERT”)) $this->logInsert($msg); elseif (stristr($SQL,”SELECT”)) $this->logRead($msg); else $this->logWrite($msgs); } return $result; ?>
About the Author
Click HERE To Discuss This Article http://forums.phparch.com/115
Continued from page 54: Customized Mailing List Management with PHPMailer
method, so when this occurs while sending mail I make a call to logError to save the error. To make the web interface even more user friendly, I added an error count statistic to the web interface to display these errors as the sending progresses. Conclusion Writing your own mailing list manager can be accomplished without a lot of effort. The examples in this tutorial only lightly touch on the total capabilities of a list manager system. With the help of PHPMailer you can do a lot more to make yours as flexible and powerful as possible. If you are looking for some new ideas then always feel free to visit the PHPMailer project page and peruse the public forums and mailing lists. There are many users there that have gone through many of the issues that you have and can help you develop your own solution.
?>
Graeme is currently working as a lecturer (Computer Science) at Sherubtse College, Bhutan, but has also worked in the UK, Papua New Guinea, and New Zealand. He much prefers the Himalayan air to London back streets, even if the electricity here is much less reliant.
php|a
References Email History – http://livinginternet.com/?e/ei.htm MySQL – http://www.mysql.com/ PEAR – http://pear.php.net/ Smarty – http://smarty.php.net/ PHPMailer – http://phpmailer.sourceforge.net/
About the Author
?>
I am an application developer for a software company in eastern Pennsylvania. I became interested in PHP in 1999 and have been an avid user ever since. I recently bought a house in Philadelphia and am thoroughly enjoying the nightlife there. If someone would like to get in contact with me my email is
[email protected].
Click HERE To Discuss This Article http://forums.phparch.com/114 December 2003
●
PHP Architect
●
www.phparch.com
66
T I P S
&
T R I C K S
Tips & Tricks By John W. Holmes
Isset() Alternative Tom Rogers, from the PHP-General mailing list, offers up the code in Listing 1 as a means to get rid of all the isset() checks that usually litter your code. If you have set an error_reporting level that displays warnings, you’ve probably noticed that doing a simple if($_GET[‘foo’] == ‘bar’)
will result in a warning about an undefined variable if “foo” isn’t passed in the URL. So, a usual solution is to do something such as if(isset($_GET[‘foo’]) && $_GET[‘foo’] == ‘bar’)
but who wants to do all of that typing? This is where Tom’s handy class comes into play (see Listing 1 on the next page). It’s an easy to understand class that doesn’t require a constructor; just include the code in your page. Since its small, it could also be included as an auto_prepend file to easily make it available to your entire site. After including the class into your page, checking for the existence of a variable is as simple as if(req::get(‘foo’)) { echo “Hi {$_GET[‘name’]}”; }
The method will return FALSE if the variable is not set, otherwise it will return its value. The class offers a method for the POST, GET, REQUEST, SESSION, SERVER, and COOKIE variables and each one can be called with the simple req::post, req::session, etc, syntax. Each method can also be passed an optional default value as the second parameter that will be returned instead of December 2003
●
PHP Architect
●
www.phparch.com
FALSE in the event the variable is not set. The class also offers a second set of functions that make it easy to check for equality by adding “EQ” to the method name. So the above checks for $_GET[‘foo’] using isset() and an equality check can be replaced with if(req::getEQ(‘foo’,’bar’)) { echo “Hi {$_GET[‘foo’]}”; }
A final tip Tom offers is to set up your syntax highlighting to recognize the text “req::” and color it so you’ll easily notice it within your code. For those of you who haven’t built up their code library, this would be a useful addition. Thank you for your suggestion, Tom. Determining Where You Are If you haven’t taken a good look at the result of print_r($_SERVER), you’re missing out on a lot of useful data. The $_SERVER array contains a wealth of information that you can put to use in your scripts. By combining a variety of the variables present, you can dynamically determine the web address to the current script. We can start with the $_SERVER[‘HTTPS’] variable. If you’re operating under HTTPS, then this variable will be set to “ON”. $_SERVER[‘SERVER_NAME’] will give you the domain name of the server you’re on and
REQUIREMENTS Code: http://code.phparch.com/18/5 Code Directory: tipstricks
67
TIPS & TRICKS Listing 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
December 2003
●
PHP Architect
●
www.phparch.com
$_SERVER[‘SCRIPT_NAME’] will give you the name of the current script. If you’re just looking for the path to the script, you can use dirname($_SERVER[‘SCRIPT_NAME’]). You can use the code in Listing 2 to put all of this together and create some useful variables to use within your scripts. You can use $_CONF[‘html’] as a starting point for building any links within your site and $_CONF[‘current_page’] comes in handy for those forms that always link back to the same script. The best part is that as your program is installed on different servers, this is one less thing the user has to supply to get your scripts working. Boolean Short Circuit This is just a simple tip that you may or may not have noticed already that deals with IF statements. Say you have a statement such as if(condition1 && condition2)
in your script. If, as PHP evaluates whatever condition1 is, it determines the result to be FALSE, PHP will not move on to condition2 at all. This is good because no matter what the result of condition2 is, the entire IF statement will evaluate to FALSE, so why should PHP waste time evaluating whatever is in condition2? The same doesn’t hold true if you’re doing an OR conditional, though. if(condition1 || condition2)
Even if condition1 results in a FALSE, the overall statement can still come out to TRUE if condition2 results in TRUE. Loading Your Database If you’re trying to automate the installation of a program you’ve written, you’ve probably had to deal with finding a method to load the initial database data so your scripts will work. Well, the easiest way to accomplish this is just an exec() call (or one of the other system calls) sending the file to your database. //MySQL for example exec(“mysql –u$user –p$password $database < $filename”);
That works great and is probably the fastest method, providing you actually know where the database executable is
68
TIPS & TRICKS located (you may need to give a complete path), or the PHP user has permission to run the executable in this manner. If this doesn’t work for you, there is always the option to load the file yourself and execute each query. Of course this is going to be a lot slower, but it’s more portable and only done once, anyhow, when the program is installed. The code in Listing 3 gives you a sample method you can use to read the SQL file and build Listing 2 1
Listing 3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
0) { //Add current line to query $query .= trim($line); //If last character is semi-colon, //then that’ the end of the query if(substr($query,-1) == “;”) { //Add (optional) prefix to table names by //matching beginning of query and inserting //the prefix before the existing table name //(and after the back tick, if it’s there). $query = preg_replace(‘/^CREATE TABLE (`?)/’, “CREATE TABLE \\1$prefix”, $query); $query = preg_replace(‘/^INSERT INTO (`?)/’, “INSERT INTO \\1$prefix”, $query);
the queries to run. Since the queries can be spread across multiple lines (usually the CREATE TABLE queries), this script loops through the lines of the file and builds a query until it comes to a string ending in a semi-colon. Then it takes the query it’s been building, adds in an optional table prefix* and runs the query. This example uses ADODB as a database abstraction layer. *Table Prefix Here’s just a Good Idea that I’m sure others will agree with. Take the extra time to design your installation program so it has the option to create tables with a prefix. Many users of your script will only have one database and if you’re script expects a table called “users” it’ll cause conflict with other scripts requiring a table by that name. Giving the users the option to add a “survey_” or “board_” prefix will prevent these conflicts. It may also allow them to install multiple instances of your script, maybe a live one and one for production to help you with programming (as long as it’s okay by your license). Now the hard part to agreeing with this Good Idea is that you must remember to include the prefix variable in every query you run. Now the easy way (or so it seems) to do this is to just have a constant or variable defined with the prefix and include that everywhere. $result = $db->Execute(“SELECT column FROM {$prefix}Table1 WHERE … “);
So long as you don’t forget to include that anywhere, you’ll be fine. Another option is to add a wrapper layer to your Execute() method that’ll add in the prefix for you by analyzing the query. With the number of different queries and syntaxes, though, this would be difficult to write and may affect performance. The option to cache the already prefixed queries would certainly help. Now you’re probably hoping there’s a Listing 4 right after this that gives you this “magic” method, but, unfortunately, there isn’t. Consider this a “Tips ‘n Tricks Challenge” to you high-speed and motivated readers out there who want to contribute such a function to the next issue. I’m sure the other readers will appreciate it and you’ll earn some free issues to boot!
//Remove semi-colon from query $query = substr($query,0,-1); //Execute query (ADODB) $rs = $db->Execute($query); if($rs === FALSE) { $error = TRUE; echo ‘
’ . $db->ErrorMsg(); } //Reset query $query = ‘’; } } } ?>
December 2003
●
PHP Architect
About the Author
?>
John Holmes is a Captain in the U.S. Army and a freelance PHP and MySQL programmer. He has been programming in PHP for over 4 years and loves every minute of it. He is currently serving at Ft. Gordon, Georgia as a Company Commander with his wife and two sons.
●
www.phparch.com
69
Book Reviews
B O O K
R E V I E W S
by Peter MacIntyre
The Career Programmer: Guerilla Tactics for an Imperfect World by Christopher Duncan Publisher by Apress Paperback 211 pages $29.95 (US) Many of us code because we love it and the thought of having a job coding all day and getting paid sounded too good to be true. We imagined sitting in a nice office with a beautiful view as we had full autonomy to crank out beautiful code that would revolutionize the industry. As we began our first real coding gig, we held onto that dream which, unfortunately, quickly faded for many of us. Often, as programmers we forget that many of us work in corporate America (or corporate Wherever You Are). We may know how to write some beautiful code, but we also need some business skills and a good understanding of the industry if we hope to survive. Welcome to The Career Programmer: Guerilla Tactics for an Imperfect World. This book gives
December 2003
●
PHP Architect
●
you an insight into corporate America and gives you some good weapons to add to your arsenal. The book begins by giving you a general overview of ‘corporate America’. These first few chapters, if nothing else, let you know that you are not alone. You do not work in the only software company with unrealistic deadlines, where you sometimes spend more of your daily time in meetings rather than coding, where you are expected to complete an entire system from a few pages (if that) of specifications. This kind of scenario is the norm—
www.phparch.com
your company isn’t the only crazy one. What you do in these situations, on the other hand, makes all the difference in the world as far as your career’s future and your happiness. The book continues by giving you some good tools to handle most situations. Chapters such as ‘Preventing Arbitrary Deadlines’, ‘Effective Design Under Fire’, ‘Fighting for Quality Assistance’ and ‘Managing your Management’ help you keep your project under control, which makes for a better end product as well as a happier you. Overall, I think that this book is very interesting and engaging. I found the author’s style interesting and humorous. The book brings to light many good points: one of the main ones that still resonates for me is that even though I like to think of my development team as an island where we just code and all are happy, we still work for a company. Large or small, most companies have the same problems and politics. I can’t only view myself within my small team but need to view my place in the “larger picture” if I want to advance and make my job more pleasant for myself and give my company a better product. This book is a great tool for the rookie programmer just entering the industry, as well as for the seasoned pro needing a refresher on good general practices. Even though it has been out for over a year, the tactics mentioned still hold weight and are beneficial to any programmer no matter what language you prefer. Best of all, the book doesn’t try to convert you to any specific methodology or theory—it simply shares some good general ideas and lets you make your own decisions.
php|a
70
Frequently Annoying Questions
e x i t ( 0 ) ;
By Marco Tabini
A
normal person who happened to be the publisher of a magazine would spend the precious fivehundred-to-a-thousand words allocated for this column describing the numerous accomplishment of his publication in occasion of its first anniversary. Being anything but normal—a fact that everybody here relishes in reminding me daily—I will, instead, confine myself to exposing a single item of trivia: in the thirteen issues that we have published so far, you’ll find close to 390,000 words’ worth of content dedicated exclusively to PHP. That is a mindnumbing number in itself, but it has a special significance when you put things into perspective. Following a well-defined format, php|a publishes six “feature” articles every month, giving a total of 78 articles over the past year (plus our other editorial content, like Tips & Tricks). With a word count which is close to any one of the “monumental” PHP books out there, and a price that is well below them, we may well turn out to be an excellent point of reference not only for learning PHP, but also for using it in a programmer’s dayto-day professional life. But enough with the self-congratulatory mush—let me turn, instead, to the future. My prediction is that 2004 will be both an exciting and very difficult
December 2003
●
PHP Architect
●
year for PHP. On the exciting front, PHP 5 will most certainly come out over the next few months, and probably mature toward a production-ready version that people will be ready to adopt before the end of the year. While PHP 5 represents, from many points of view, a significant leap forward over its predecessors, it also introduces a number of changes that are going to be difficult for current PHP users to adopt. Some, particularly in the enterprise space, will find its new features very appealing, perhaps finally choosing PHP over some of the other platforms available for their projects. For others—those who rely on PHP today—porting their software over to PHP 5 could be a little less pleasant. OOP users may well find that moving over to PHP 5 will be akin to forcing a square peg in a round hole using a triangular hammer: objects are handled in a completely different way, new reserved keywords are introduced, and so on. Yet again, 2004 may well turn out to be a difficult year for PHP due to an altogether different (and completely unrelated) reason. Open-source software, particularly if it doesn’t have strong backing from a major industry player, evolves mainly through the voluntary efforts of programmers, who
www.phparch.com
invest their spare time—something they dispose of aplenty in a recessing economy. As the pace of US recovery picks up in 2004 and the remainder of the western world is dragged along for the ride, however, programmers will hopefully find more and more work, and will have less time to dedicate to OSS. Least you have any doubts, while many (many!) people have CVS write access to the PHP source tree, there’s only really a few dozen of them who really contribute to it on a regular basis—and it’s to them that many of us owe their day jobs (and, sometimes, the late nights spent trying to figure out what they have done). With a newly strong economy they, too, will have less time to work on PHP, and its development may suffer as a result. Naturally, there are a handful—but just a handful—of people whose work on PHP is the result of a day job. As time goes by, they’ll have their work cut out for them. Where does php|a fit in all this? Well, 2004 will be a year of challenges for us, too. If you live in North America, you’ll soon see us sitting on the shelves at your local bookstores and newsstands. It’s a difficult step, fraught with peril at every corner, but one that we take gladly to ensure that more and more people can hear about us and help us make the PHP community a better place. After launching two localized editions (for the Japanese and Francophone markets) in our first year, we’ll try to expand into more markets, particularly in Europe, where people expect—and rightly so—that information be delivered not only in their language, but in a way that reflects their unique needs and cultural environment. Finally, we are working toward publishing even more compelling content, with new columns and more great articles coming in the next few issues. After all, one of the beautiful things of working with a language like PHP is that it has so much potential that one never runs out of ideas—and our authors seem to be very creative! Naturally, none of this would be possible without your support. This expression is a cliché, I know, but as proud as I am of what we have done over the last twelve months, nothing makes me happier than to know that every issue we publish helps someone get a little more out of PHP. php|a
71