eZ publish: PHP's Killer App - Parts 1-3
About the Author
Harry Fuecks
Harry has been working in corporate IT since 1994, with everything from start-ups to Fortune 100 companies. Outside of office hours he runs phpPatterns: a site dedicated to software design with PHP that aims to raise standards of PHP development. He also maintains Dynamically Typed: SitePoint's PHP blog.
By: Harry Fuecks
November 15th, 2002
If you've ever trawled the PHP listings over at Hotscripts [1] in search of a content management system to save you from writing your own, you've probably run into eZ publish [2], a PHP-based CMS application, and thought "Wow!" Jubilant, you tried to install it... to no avail. Desperately you tried reading the code, only to discover it made no sense whatsoever. Finally you skulked away to a quiet corner to lick your wounds, resorting to PHP Nuke [3] instead.
This series is all about eZ publish and why it deserves the title of "PHP's killer app". We'll start from the ground up: first, we'll install eZ publish in your development environment. Then, we'll learn how to make use of its features, and customise it to your specific needs. Finally, we'll take a look under the hood, to discover what makes eZ publish tick: how eZ publish is in fact a powerful tool to help you build your own Web-based applications.
By the end, you'll be able not only to set up and customise your own eZ publish Website, but to build your own applications to run within eZ publish.
Here's the lowdown...
Part 1 [4] - an introduction to eZ publish, where we explore, download, and install the required files.
Part 2 [5] - we get right into eZ publish, learning how to customize it to your requirements -- in terms of both looks, and functionality. We'll build our own small site and implement eZ article.
Part 3 [6] - we discuss how you can create your own eZ publish module, taking a look under the hood to explore just how and why building your own modules is so simple with eZ publish.
Let's get started!
Part 1
On the menu in this, the first article in the series, is:
- Introducing eZ publish: the warm-up before we start on the real work
- Installing eZ publish: get eZ publish up and running ready for article 2 in the series
- Homework: tough discipline
Introducing eZ publish
eZ publish is (on the surface) a Website content management system, written in PHP [7] by eZ systems [8], a Norwegian company. Launched in 2000, eZ publish has come on in leaps and bounds, having been successfully put into action on sites like Webservices.org [9], the Austrian National Tourist Office [10] and Simplicity Networks [11] (more sites using eZ publish here [12]), and gaining support from IBM and Siemens, among other business partners [13]. eZ publish is now even included in the Debian Linux distribution [14].
Content management systems, as you probably know from Kevin Yank's famous tutorial [15], allow you to publish dynamic content on your Website in manner that's easy to maintain and update. eZ publish takes that model close to perfection. Firstly, it allows you to store content in a database (eZ publish 2.x currently supports MySQL [16] and PostgreSQL [17], with support for other databases like Oracle coming in version 3.x). On top of this database, a set of PHP applications provides a powerful administration interface for site maintenance, while a wealth of front end modules allow eZ publish to be rapidly implemented as anything from a news and community site like SitePoint or an online shop, to a B2B portal or even a corporate Intranet. In other words, it's the solution to all your Website needs!
What's more, eZ publish already has a foot in the door of the Web services [18] arena, with a commercially licensed Desktop Edition [19] that allows you to update an eZ publish site using a C++ Windows client communicating with an XML-RPC server interface.
Good news for paupers like me and you is eZ publish has a dual licensing scheme [20]. It provides both a GNU open source license [21], and (for those wishing to do things like resell or rebrand eZ publish), a Professional license. For what is at heart an Open Source project, eZ systems are an excellent example of how to turn publicly-licensed software into a commercial success.
What's on Offer?
So, blurb aside, what does eZ publish actually have to offer? The easiest way to get answers is to visit the demo site at http://publishdemo.ez.no/ [22], and then login to the back end administration system by heading to http://admin.publishdemo.ez.no/ [23] using:
username: admin
password: publish
Once you've logged in, you'll see the demo site control panel with a row of icons along the top (time to have a play!). Each of these corresponds to an eZ publish module, which provides a range of functionalities for the front end of your Website.
Among the modules you'll find:
- eZ article: this the "main" module, allowing you to publish articles or "static" pages on your site
- eZ trade: a powerful shopping cart application you'll find in action over at MyGold [24].com
- eZ filemanager: a great tool for intranets and file distribution, providing an online filesystem for your visitors to browse
- eZ forum: forums for your site, similar to (although not as powerful as) VBulletin [25], the software behind the SitePoint Forums [26]
...and many more that are described in detail in the User Manuals [27]. In other words, eZ publish gives you all you need to build your very own version of the SitePoint Network! You'll notice on both the demo site [28] and the main site [29] that even the URLs are similar to SitePoint's. For example:
http://developer.ez.no/article/articleview/367
and
http://www.webmasterbase.com/article/228
Of course there's a long way to go from building a site that works like SitePoint and convincing all the SitePoint fans to pay you a visit, but you get the point.
Yet another CMS?
"OK", I hear you say, "another content management system. Big deal."
What makes eZ publish special is not the impressive list of features, but what's going on behind the scenes. eZ publish is in fact an application development framework, providing PHP developers with a structure in which to build applications and, from there, rapidly deploy them into a live environment, saving you many hours of coding.
If you've ever written you own PHP Website from scratch, you've probably found yourself wishing there was an easier way to do things. You¡¦re confronted with a multitude of issues: where to place the scripts for your site, how to build an integrated site navigation system, how to prevent reproducing the same blocks of code for every page of your site¡K and a whole host of other problems (meanwhile, your social life packs up and heads to Hawaii).The long-awaited completion of a site can often feel like a miracle... then someone comes up with a great idea for some new features and you're re-writing the entire site again, weeping quietly to yourself.
An application development framework eradicates the burdens of building Websites. By conforming to its rules and guidelines, you'll find the maintenance and expansion of your site a breeze. A good framework should make 90% of the design and development decisions for you. Solutions to issues such as naming conventions, where to place your scripts within your Website file system, how to design and structure your code, and all those other fine details you struggle with, are simply a matter of following a set of clear guidelines. And what about all that code you used to waste hours on, such as database connection and query functions, the work you reproduced over and over again for every new site? All that should be available within the framework for you to re-use as needed.
This is what eZ publish really offers: the chance to make your life as a developer a carefree and pleasant one.
PHP Coder General's Warning:
Developing with eZ publish means writing Object Oriented code!
Much as we all love to write "hacked" scripts using procedural code (perhaps with a few PHP functions included only when we really have to), the truth is, if you want to have any hair left a year or two from now, you need object orientation. Writing object oriented code in PHP is not just about saving time: it allows you to solve coding problems that simply cannot be solved by procedural code. Once you've got two or three PHP classes working together in an application, you'll realise you never want to go back. And then you'll be able to hack away like you've never hacked before!
Put another way, object orientation give us the means to write re-usable code and integrate different applications and functionality into a single whole. For example, you may already have run into difficulties on one of your sites, where you implemented a user authentication system and signed loads of people up. You're now scratching your head as to how to integrate it with a forum application like VBulletin [30] or phpBB [31] without forcing your users to sign up again. If we all wrote more object oriented code, at least half these problems would be solved.
In general, the way eZ publish functions in tying together its various modules, is conceptually similar to the Fusebox [32] approach. This strategy for building Web applications was conceived by developers working with Coldfusion, and has now made its way to PHP at Bombusbee.com [33]. The basic concept behind a fusebox-like site is to have a single script (usually index.php in the Web root) that acts as "traffic cop" for the site, all pages being served "through" index.php. For further details on the Fusebox approach, try the Fusebox Newbie Guide [34]. From an application design perspective, eZ publish loosely conforms to the Fusebox approach, so it may help to read about Fuseboxes in preparation for the third article in this series. But don't panic! We'll leave further discussion of object orientation and application design until the third article in this series.
Back to eZ publish! To whet the appetite of those who know your way around PHP classes, have a look at the eZ publish Class Index [35]. Then wander over to the eZ publish sdk [36] currently in development for eZ publish v.3.
Hopefully by now you're at least sold on the idea that eZ publish is worth your effort. In that case, it's time to roll up our sleeves and get installing! eZ publish, here we come.
Installing eZ publish
In setting up a working version of eZ publish, I'll dare to assume you use Windows and are willing to set up a demo on your own machine. The installation process used here can very easily be applied to a LAMP (Linux, Apache, MySQL, PHP) virtual host environment common to many Web hosts.
It's recommended you have at least PHP version 4.0.6 installed (even better if it's PHP 4.1+) under Apache 1.3x as well as MySQL 3.23 or later. If you don't have those installed, look no further than Firepage's [37] phpdev, an install set that gives you Apache, PHP and MySQL in one tidy package (plus a few other nice things like PHP-GTK [38] and phpMyAdmin [39]). The current phpdev5 beta3 is very stable but to save yourself some headaches, it's recommended you use Apache 1.3.x rather than 2.x (phpdev5 beta 3 comes with both).
The version of eZ publish used in this article series is 2.2.6. The install process may vary for different versions, and be aware that eZ publish is very sensitive to the correct php.ini settings. If you have any problems with the instructions used in this article, please drop your questions in the SitePoint Forums [40] discussion at the end of this article. Otherwise, you'll find plenty of people willing to help at http://developer.ez.no/developer/forums/ [41].
Down to Business
eZ publish 2.2.6 can be found from the filemanager at http://developer.ez.no/filemanager/list/85/ [42]. Download and unzip to somewhere under your local Apache or PHP Web server -- with phpdev5, this will probably be something like "C:\phpdev5\www\public\".
Note: an eZ publish Windows installer exists -- it also installs Apache, PHP and MySQL. However, we'll do things little differently, so that we can re-use the install process on our live LAMP virtual server.
Having unzipped eZ publish, you'll now have a directory that looks something like C:\phpdev5\www\public\ezpublish_2_2_6\ (I'll call this directory your "eZ publish Root" from now on). Under there you'll find an installation directory, which contains a file called INSTALL.pdf -- Chapter 3 of this document is the method we'll be using to install eZ publish.
Should we be virtuous?
As you saw at the start of this article when we looked at the online demo of eZ publish, there were two separate URLs: one for the main site and another for the back end admin interface:
http://publishdemo.ez.no/ and
http://admin.publishdemo.ez.no/
This is accomplished by some clever tricks with Apache's virtual host features, described in Chapter 2 of INSTALL.pdf. Trying to install eZ publish this way can be extremely complex -- if not impossible -- on a typical Web host, which is why we're taking the alternative option where no Apache configuration is required. Having said that, installation by the easier path will change slightly the way eZ publish behaves:
- http://www.yourdomain.com/index.php is your main page
- http://www.yourdomain.com/index_admin.php is your admin interface
Also, where eZ publish URLs usually look like:
- http://www.yourdomain.com/article/articleview/3/
using the easier approach, the URLs will look like:
- http://www.yourdomain.com/index.php/article/articleview/3/
That's the trade-off, but it's minor compared to the pain you'll experience if you take the Virtual Hosts route for your first eZ publish installation.
Image Magick
The other piece of software we need is Image Magick [43], a powerful set of command line image manipulation tools that come in very handy when our PHP scripts need a little extra "oomph" in the graphics arena. eZ publish uses Image Magick to control image sizes, to allow them to be rendered consistently.
The file we need to install Image Magick can be found at ftp://ftp.nluug.nl/pub/ImageMagick/binaries/ [44] (or one of the mirrors [45]). The file you're looking for will be called ImageMagick-5.4.9-xp.exe, or something similar. Once you've downloaded and run the executable, install Image Magick to the directory C:\Imagemagick\.
Ready for Action
You'll need to take the following steps to get eZ publish up and running.
- Copy the file
htaccess-nVHfrom[eZ publish_root]\installationto[eZ publish_root]\, rename the current file named .htaccess to htaccess_old and rename htaccess-nVH to .htaccess (note that, depending on your version, Windows Explorer may complain about beginning a file with a full stop (or "period"). In that case, you'll need to rename the file using the DOS command line (see Kev's Command Prompt Cheat Sheet [46]).Change the eZ publish root directory and use
rename htaccess-nVH .btaccessto rename the file. This step ensures the eZ publish installation is secure, and that site visitors can only access index.php. - Now head over to your MySQL database (I'm assuming you've got phpMyAdmin -- if you've got phpdev5 running, it's available at http://localhost/phpmyadmin [47]) and create a database called "publish".
- You now need to import the MySQL database for eZ publish. In phpMyAdmin (version 2.3.0+) you need to click on the SQL tab when viewing the publish database, and use the file upload box "Location of the textfile:" to select the file. The file you need is
[eZ publish_root]\sql\publish_mysql.sql, which builds the eZ publish database structure. We also want some sample data to play with, so load the file[eZ publish_root]\sql\data_mysql.sql, which will populate the database. - Rename the file
[eZ publish_root]\site.inito[eZ publish_root]\site.ini.phpand edit it with your favorite editor. The reason for doing this is -- again -- security (files with a .php extension, apart from index.php, will be unavailable to visitors). - Edit the file sitedir.ini in eZ publish root and set the following variable:
- Next, you need to create the directories eZ publish uses to cache pages (getting a good feeling about eZ publish yet?). Download the file at
make_cache.zip[48]. This contains a batch filemake_cache.batthat will create the directories for you. Extract this to your eZ publish root and with a DOS command line, from the eZ publish root directory typemake_cache. All the cache directories should now be created, such as[eZ publish_root]\classes\cache. Make sure you deletemake_cache.batwhen you're finished. - Finally, we need to make sure our php.ini settings are correct. Open the php.ini file (from your C:\Windows\ directory) and check the following settings:
short_open_tag = OneZ publish often uses the<? echo($variable); ?>syntax rather than<?php echo($variable); ?>, hopefully something they'll correct in version 3.x, to help with portability.allow_call_time_pass_reference = On
Passing variables by reference is a "bad practice" used commonly in XML parsing scripts -- another item we hope they'll get time to fix in eZ publish 3.x
error_reporting = E_ALL & ~E_NOTICEregister_globals = On
Development of eZ publish 2.x began before the announcement that register globals would be switched off by default from PHP 4.2. As Kevin Yank warned us in Write Secure Scripts with PHP 4.2! [49], leaving register globals on is asking for trouble. Again, we hope this will be resolved in eZ publish version 3.x
magic_quotes_gpc = Off
Magic quotes are evil!
magic_quotes_runtime = Offshould also be set.Those are the important settings. Save your php.ini and restart Apache.
Now, in site.ini.php, you need to change the following settings to match your environment:
[site]
SiteURL=localhost/public/ezpublish_2_2_6
AdminSiteURL=localhost/public/ezpublish_2_2_6
UserSiteURL=localhost/public/ezpublish_2_2_6
...
SiteTitle=phpPoint
...
SiteTmpDir=C:/Windows/temp
...
# Database settings set DatabaseImplementation to
mysql|postgresql|informix.
DatabaseImplementation=mysql
Server=localhost
Database=publish
User=MySql_User
Password=MySql_User_Password
...
ImageConversionProgram=C:\Imagemagick\convert.exe
Save the file.
$siteDir = "C:/phpdev5/www/public/ezpublish_2_2_6/";
The Magic Moment...
Head to http://localhost/public/ezpublish_2_2_6/index.php to view your site! To login to the admin interface, head to http://localhost/public/ezpublish_2_2_6/index_admin.php and login with:
userid: admin
password: publish
Don't worry about the "failed to create image variation" images -- this simply tells us that the images it expected weren't available (they don't come with the eZ publish installation zip). If, after you view the eZ publish homepage, you find that you've made mistakes in the eZ publish installation, and need to further alter site.ini.php after you make a change, you'll need to delete any files you find in [eZ publish_root]\classes\cache (where eZ publish caches your site.ini.php file). Otherwise, if you have any problems, don't hesitate to ask around in the SitePoint Forums [50] discussion at the end of this article.
Installing on Linux Hosts
If I can make yet another assumption -- that you host your PHP site on a shared Linux server -- the installation process will be very similar, but there are a few pitfalls. Here are a few tips to help you out.
- Make sure you have "ssh" access to your Website. On Windows, putty [51] makes a great SSH client. SSH is a secure (encrypted) version of telnet. There are loads of resources on Google [52] to help you out, and you Web host should be able to provide some good advice, too.
- When you transfer the eZ publish code to your site, be sure to transfer it as a complete zipped file. If you download it to your PC, unpack it, and then transfer the separate files to your site, you will have problems, due to file permissions. Either upload the entire archive in one go, or, when logged in with ssh, fetch it from eZ systems using the
wgetcommand line:wget http://developer.ez.no/filemanager/ download/413/ezpublish_2_2_6.tar.gzOnce you have the archive on your Web server, you can unzip it with the command:tar -zxvf ezpublish_2_2_6.tar.gzThis will extract the lot to a new directory "ezpublish_2_2_6" below the directory where you typed the command. - In step 7 of the Windows installation above, we used a DOS batch file to create the cache directories. The Linux equivalent is the file called
modfix.sh, which you can run by changing the[eZ publish_root]on your server and typing: - To make use of Image Magick, the
convertbinary needs to be installed on the Web server. Time to beg your Web host for a favour (it's not hard for them to install), and get them to tell you where they put it.
./modfix.sh
Note this file also sets up security permissions on the cache directories which are not entirely secure (although the risk is low). And alternative secure_modfix.sh exists, but you'll probably run into trouble if you use this in a typical shared Linux environment.
If you have any problems installing on your Web host, feel free to add your questions to the discussion at the end of this article.
Homework
eZ publish is now up and running! Before heading into the second part of this series, your mission, should you choose to accept it, is (when logged in as Admin):
ez Image Catalogue [53]
- Delete the existing categories
- Create a new category called Logos to which only Admins and Article Authors can "Write" and "Upload"
- Below the Logos folder, create a category called "Software" with the same permissions as "Logos"
- Upload a couple of images to the Software folder, such as the PHP logo [54] and the MySQL Logo [55].
ez Article [56]
- This gets a little tougher: edit the "Article Tags Demo" article and see if you can get it to use the two logos you've added.
- The big one: learn about eZ article tags. If you download the eZ article documentation [57] and read
Adminguide.pdf, you'll find full details -- a more brief description is provided online at http://doc.ez.no/article/articleview/3/1/7/ [58]. It's important for the next article that you're comfortable with the eZ article module -- grade A students will work out how the static page "Thank you for your feedback" relates to the eZ form module [59].
And if that's not enough, have a look at the files in [eZ publish_root]/checklists -- these are meant to help you test whether eZ publish is functioning correctly, but also serve as a nice series of exercises to get used to all the features. Oh, and don't forget to have a look at the error.log file when you're finished. You'll find in your eZ publish root -- it makes interesting reading!
In the next part of this series, we'll assume you're fairly happy using the modules, and move on to how to customize your eZ publish site to give it your own look and feel. Until then, may all your publishing be... well eZ, in fact!
Part 2
In Part 1 of this series, I introduced you to eZ publish [60] and dared to dub it "PHP's killer App". By the end of the last article, you'd installed eZ publish on your development system and of course, being extremely diligent, you've done all the homework set and know what eZ publish has to offer.
By now though, you're probably wondering how to change that boring grey background and apply your own design. In this article, that's exactly what I'll explain: how to make eZ publish look and behave the way you want it to. I'll be helping you create a fictional Website called "phpPoint" which will act as a demonstration of how you can customize eZ publish.
Today's specials are:
- Fools rush in: pre-requisite knowledge about the eZ publish directory structure, the site ini file and the caching system.
- Going Global: how to modify the "global" design of eZ publish and how Sections are used to create multiple sites.
- Hitting the Module: here you'll see how module customisation works. Concentrating on eZ Article, you'll see how to use it's "API", how to modify it's templates and how to use static pages, URL translation and forms.
- phpPoint Finished: the happy ending!
By the time you're finished, you'll know all you need to build your own state-of-the-art eZ publish site using the existing modules.
Fools Rush In
Ok. Before you starting hacking away, there are a few things that are worth having a look at so you know where you are.
The eZ publish directory structure
If you open up your eZ publish installation directory C:\phpdev5\www\public\ezpublish_2_2_6\ (or wherever you installed it) with Windows Explorer you'll see a long list of subdirectories. Most of these relate to eZ publish modules -- they're easy to spot because they all look like "ezsomething", for example: "ezarticle", "eztrade" and "ezforum". I'll come back to module directories later, but first, let's look at some other important directories and files:
- admin directory: This contains code and templates for the general look and feel of the eZ publish administration back end. Modules add further code for specific admin tasks, stored under their own directories (as you'll see shortly). I'll leave this directory alone though, as I don't care so much what the admin interface looks like: it's private after all. One thing to note is that any "intl" directories you might come across contain files that allow eZ publish to handle languages other than English -- you'll be seeing more of this later.
- checklists directory: If you really got stuck into the homework from the last article, you'll already know that this directory contains a set of tests that you can run on the eZ publish modules. eZ publish still has some minor bugs, as you might expect with an application this big. If you run into any problems, and you're sure it's not your configuration, have a look at the eZ publish Bug Tracker [61].
- classes directory: This contains general PHP classes that are used throughout eZ publish, as described at http://developer.ez.no/doc/view/index [62]. If you're just interested in applying your own design, you don't need to worry about these. I'll look at them in more detail in the third article of this series. Underneath the classes directory you'll also find the all-important cache directory. We'll cover more on caching in a moment, but you need to be aware that this directory caches the global configuration files like site.ini.php.
- images directory: Contains images used throughout eZ publish. Further images appear under the module directories.
- sitedesign directory: This one is important, as it contains the files that determine the overall eZ publish design. We'll look at this in depth shortly.
- site.ini.php file: This stores the global configuration options for eZ publish. More in a minute!
- sitedir.ini file: Used by eZ publish to locate itself for installations, as you saw in part one.
- cron.php file: A useful file that helps with scheduled updates to eZ publish while you sleep! Intended for the Unix Cron tool [63] (it should also be possible to activate this using a Windows scheduling tool, such as the Task Scheduler).
- index.php file: This is the "traffic cop" of eZ publish, activating code as required. It should never need changing -- any attempts to do so will probably break things unless you know what you're doing.
- index_admin.php file: This is the "traffic cop" for the back end admin system. Again, best not to touch!
- index_xmlrpc.php file: This provides and XML-RPC interface to eZ publish, for use with their Desktop Edition [64]. Not one you should need to interfere with.
site.ini
In Part 1 of this series you renamed this to site.ini.php and made some basic changes to get your site up and running. I'll refer to it from now on as just site.ini, to stay in line with the eZ publish documentation.
This file contains global settings for the whole of eZ publish, as well as settings that apply to specific eZ publish modules. Given the size of the file, I'm not going to cover every setting, but rather highlight some important ones, and explain the mechanisms for updating site.ini. You should find most of the settings self-explanatory.
But first of all, make a backup and put it somewhere safe.
At the top of site.ini you'll find settings that apply to the whole site. Let's begin by modifying these.
# Meta content variable
SiteAuthor=HarryF
SiteCopyright=HarryF © 2002
SiteDescription=PHP: Getting the point?
SiteKeywords=PHP, Development, Tutorials, Articles
These determine HTML <meta> tags that appear on the top of each page. Note that eZ publish modules like eZ article may override SiteDescription and SiteKeywords with values you create with your articles: this is very useful if you want to tell Google all about an article you've written. The settings here amount to the default settings.
Let's see some more:
DefaultPage=/article/frontpage/1/
LogFile=error.log
EnabledModules=eZArticle;eZTrade;eZForum;eZLink;eZPoll;eZAd;
eZNewsfeed;eZBug;eZContact;eZTodo;eZCalendar;eZFileManager;
eZImageCatalogue;eZMediaCatalogue;eZAddress;eZForm;eZBulkMail;
eZMessage;eZQuiz;eZStats;eZURLTranslator;eZSiteManager;eZUser
CacheHeaders=true
...
URLTranslationKeyword=section-standard;section-intranet;section-trade;
section-news
Sections=enabled
DefaultSection=1
Ok, let's go through this code step-by-step.
The DefaultPage determines what appears on your home page, i.e. http://localhost/public/ezpublish_2_2_6/index.php is, in this case, the same as http://localhost/public/ezpublish_2_2_6/index.php/article/frontpage/1/.
The log file you saw in Part 1 of this series is used to keep track of what administrators do to the site, and any application errors that occur. It's very useful if you have a big site with many people working on it.
EnabledModules is self-explanatory.
CacheHeaders controls the browser caching headers delivered with the HTTP protocol (it determines whether a browser use its own, locally-stored copy, or fetches a new one from your site) -- best to leave this as it is.
URLTranslaterionKeyword you'll see more of when we get to eZ article -- essentially it helps make "pretty" URLs, so that, rather than http://localhost/public/ezpublish_2_2_6/index.php/forum/forumlist/1/ you could have http://localhost/public/ezpublish_2_2_6/index.php/forum/.
Sections allows you to have multiple "styles" for your site (a bit like skins), each with a different look and feel, offering access to different eZ publish modules. In conjunction with this, DefaultSection tells eZ publish which to use on the home page, or if it's uncertain which style to use at any point. I'll look more at sections shortly.
After this, the other settings you'll need to worry about apply to the modules, such as:
[eZAdMain]
AdminTemplateDir=templates/standard/
TemplateDir=templates/standard/ImageDir=images/standard/
Language=en_GB
DefaultCategory=1
DefaultSection=1
You'll be seeing more of this soon. In each of the eZ publish module directories, you'll find a file called site.ini.txt, which gives the meaning of each of these variables.
Caching
Aside from the generation of HTTP headers that tell browsers what to do when caching Web pages, eZ publish has its own internal caching mechanism used to store "parsed" output. Once a page has been viewed, eZ publish can deliver the cached version rather than rebuilding the entire page, thereby improving overall performance. When you modify eZ publish's design, you'll need to be aware of how caching works -- otherwise you'll be wondering why you can't see your changes. The internal caching mechanism is used essentially in three ways:
- The directory
[ezPublish_root]/classes/cache/is used to store PHP scripts and variables. The contents ofsite.iniwill be found in parsed form here, along with variables that eZ publish has assigned for a whole range of operations. If you make changes tosite.iniyou'll need to delete the files in this directory for your changes to take effect. - In
site.iniyou'll find the following settings: - For certain modules, such as eZ article, you'll also find a "
PageCaching" setting insite.ini, which can also be enabled or disabled. When you work on the visual layout of a module like eZ article, it's a good idea to set this to "disabled", so you'll be able to see any changes as you make them. Also, if you publish an article and then make changes to it, you'll need to delete its cached version in[ezPublish_root]/ezarticle/cache/.
SiteCache=disabled
SiteCacheTimeout=60
The SiteCache attempts to cache every page of eZ publish. This is only useful for sites where most of the content is static. For sites using the forum module, for example, it's best to leave this disabled. The SiteCacheTimeout tells eZ publish when a cached page can be regarded as having expired (in minutes) and requires re-creating.
In the administration back end of eZ publish you'll find Cache Admin tool under the Site Manager section (second from far right). This works nicely if you've got eZ publish installed under Linux, but sadly not with Windows (you'll have to delete cached files manually with Explorer).
Going Global
Now you have an idea of how eZ publish fits together, it's time to get down to the nitty gritty of applying your own design (well, mine in fact -- but pretend it's yours!).
Before you start doing anything with eZ publish, you need a "sketch" of how you want your site to look. Subjecting you to my own personal bias, the first thing to do is download a copy of Opera from http://www.opera.com/download/ [65]. Why? Because, generally speaking, if your design looks right in Opera, it will look right in any other browser. If you're running Windows, the non-Java version is fine.
I've put together a simple HTML page and a style sheet that I want to "apply" to eZ publish. If you download sampler.zip [66], you'll find a simple design for how the site should look in HTML, with a CSS file alongside. The site is going to be very simple -- it'll make use of only the eZ article module. What you'll learn here, though, should be applicable to more complicated eZ publish sites. It's worth having sampler.html (from the ZIP) open while you read the rest of this article, to help visualise what's happening.
The Sitedesign Directory
The first move is to find the [ezpublish_root]/sitedesign/standard/ directory and make a copy of it, which you'll need to rename [ezpublish_root]/sitedesign/phppoint/. You'll keep all the design work in the phppoint directory. It's a good idea to leave the original directories untouched, to have examples to work from.
Next, from the administration back end, head to the site manager (second from far right). The default view is the "Section List". Delete the Intranet, Trade and News sections, as you don't need them, then edit the Standard Section, setting the name and Sitedesign to "phpPoint". Further down the edit page, you'll see a section called "columns". The aim is to have four articles appear at once on the "Frontpage", each taking the complete width of the page. So add a row, and then set them all to "1 Column Article" with the Category "All".
Now head to the URLTranslator (third from the right) and rename /section-standard to /section-phppoint, then delete the other three.
Finally, in site.ini, update the following variables:
SiteDesign=phppoint
SiteStyle=ezpublish
SiteTitle=phpPoint
...
EnabledModules=eZArticle;eZImageCatalogue;eZMediaCatalogue;
eZForm;eZStats;eZURLTranslator;eZSiteManager;eZUser
...
URLTranslationKeyword=section-phppoint
Sections=disabled
The SiteDesign variable points eZ publish at the directory I'll be putting my design in. SiteTitle is the title that appears at the top of the browser. SiteStyle I leave untouched, as this applies to the back end admin section. You need to make sure that only the modules you want are available using the EnababledModules variable. URLTranslation and Sections are discussed in more detail under "Sections" in a moment.
Lastly, clear out the cache in [ezpublish_root]/classes/cache/.
You're now ready to start hacking!
frame.php
In your [ezpublish_root]/sitedesign/phppoint/ directory, you'll find a file called frame.php. This file is basically the main template within eZ publish: more or less all your pages are generated by eZ publish through this template.
Before you go any further, if you'll allow me a momentary rant: this is what templating in PHP is really about. As you've seen in this discussion [67] on the SitePoint Forums [68], any templating system that doesn't use PHP's own language structure and variables is a complete waste of time, memory and processing power. frame.php is a prime example of how templates should be constructed.
Now, looking at the contents of frame.php, you'll see HTML interspersed with PHP. Before you edit it, I'll introduce you to some examples of what you'll see there. First you have:
<title><?php
// set the site title
$SiteTitle = $ini->read_var( "site", "SiteTitle" );
if ( isset( $SiteTitleAppend ) )
print( $SiteTitle . " - " . $SiteTitleAppend );
else
print( $SiteTitle );
?></title>
Here, the code first assigns the $SiteTitle variable, by fetching it from the $ini object using the read_var() method. If you're at all uncertain about the syntax used there, have a look at Kevin Yank's introductory tutorial on Object Oriented PHP [69].
After that, the code checks for a $SiteTitleAppend variable, which, if it exists, usually contains something like the title of an article. If this variable isn't found, the contents of the <title /> tag will be just the $SiteTitle variable you set in site.ini.
Moving further down the script, you have:
<img src="<? print $GlobalSiteIni->WWWDir; ?>/sitedesign/<?
print ($GlobalSiteDesign); ?>/images/ezpublish-yourcontentmadeeasy.gif"
height="20" width="290" border="0" alt="" />
Here's eZ publish builds an URL, fetching the URL of the site with:
$GlobalSiteIni->WWWDir
It's nice how these eZ publish classes work, isn't it? For anyone who's been looking at the Kevin Yank's articles on the .NET framework [70], you'll notice a remarkable similarity. As I mentioned in the first part of this series, eZ publish is also a framework designed specifically for building PHP applications. And this is one of the things a good framework is about: making your life easy, by providing a well designed class library you can reuse.
Anyway, back to business. Some of the variables used in frame.php are described at http://doc.ez.no/article/articleview/204/1/54/ [71]. To cut a long story short, here's the sampler file you saw earlier applied to frame.php. Note that I haven't literally copied and pasted everything. In particular, the left menu needs more work, which I'll get to when we look at the eZ article module.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="no" lang="no">
<head>
<title><?php
// set the site title
$SiteTitle = $ini->read_var( "site", "SiteTitle" );
if ( isset( $SiteTitleAppend ) )
print( $SiteTitle . " - " . $SiteTitleAppend );
else
print( $SiteTitle );
?></title>
<?php
// check if we need a http-equiv refresh
if ( isset( $MetaRedirectLocation ) && isset( $MetaRedirectTimer ) )
{
print( "<META HTTP-EQUIV=Refresh ".
"CONTENT=\"$MetaRedirectTimer; URL=$MetaRedirectLocation\" />" );
}
?>
<link rel="stylesheet" type="text/css"
href="<?
print $GlobalSiteIni->WWWDir;
?>/sitedesign/<?
print ($GlobalSiteDesign);
?>/style.css" />
<script language="JavaScript1.2">
<!--//
function MM_swapImgRestore()
{
var i,x,a=document.MM_sr;
for(i=0;a&&i<a.length&&(x=a[i])&&x.oSrc;i++) x.src=x.oSrc;
}
function MM_preloadImages()
{
var d=document; if(d.images){ if(!d.MM_p) d.MM_p=new Array();
var i,j=d.MM_p.length,a=MM_preloadImages.arguments;
for(i=0; i<a.length; i++)
if (a[i].indexOf("#")!=0){
d.MM_p[j]=new Image; d.MM_p[j++].src=a[i];
}
}
}
function MM_findObj(n, d)
{
var p,i,x; if(!d) d=document;
if((p=n.indexOf("?"))>0&&parent.frames.length) {
d=parent.frames[n.substring(p+1)].document; n=n.substring(0,p);
}
if(!(x=d[n])&&d.all) x=d.all[n];
for (i=0;!x&&i<d.forms.length;i++) x=d.forms[i][n];
for(i=0;!x&&d.layers&&i<d.layers.length;i++)
x=MM_findObj(n,d.layers[i].document);
return x;
}
function MM_swapImage()
{
var i,j=0,x,a=MM_swapImage.arguments; document.MM_sr=new Array;
for(i=0;i<(a.length-2);i+=3)
if ((x=MM_findObj(a[i]))!=null) {
document.MM_sr[j++]=x; if(!x.oSrc) x.oSrc=x.src; x.src=a[i+2];
}
}
//-->
</script>
<!-- set the content meta information -->
<meta name="author" content="<?php
$SiteAuthor = $ini->read_var( "site", "SiteAuthor" );
print( $SiteAuthor );
?>" />
<meta name="copyright" content="<?php
$SiteCopyright = $ini->read_var( "site", "SiteCopyright" );
print( $SiteCopyright );
?>" />
<meta name="description" content="<?php
if ( isset( $SiteDescriptionOverride ) )
{
print( $SiteDescriptionOverride );
}
else
{
$SiteDescription = $ini->read_var( "site", "SiteDescription" );
print( $SiteDescription );
}
?>" />
<meta name="keywords" content="<?php
if ( isset( $SiteKeywordsOverride ) )
{
print( $SiteKeywordsOverride );
}
else
{
$SiteKeywords = $ini->read_var( "site", "SiteKeywords" );
print( $SiteKeywords );
}
?>" />
<meta name="MSSmartTagsPreventParsing" content="TRUE" />
<meta name="generator" content="eZ publish" />
</head>
<body bgcolor="#FFFFFF" topmargin="6"
marginheight="6" leftmargin="6" marginwidth="6">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td class="sitetitle">phpPoint.com</td>
</tr>
</table>
<table width="100%" border="0" cellspacing="6" cellpadding="6">
<tr valign="top">
<td width="15%" class="menuback">
<!-- Left menu start -->
<?
$CategoryID = 0;
include( "ezarticle/user/menubox.php" );
?>
<br />
<table class="menu">
<tr>
<td class="menuitem">
<a href="?PrintableVersion=enabled">Printable page</a>
</td>
</tr>
<tr>
<td class="menuitem">
<a target="_blank" href="http://developer.ez.no">
<img src="<?
print $GlobalSiteIni->WWWDir;
?>/images/powered-by-ezpublish-100x35-trans-lgrey.gif"
width="100"
height="35" border="0" alt="Powered by eZ publish" /></a>
</div>
</td>
</tr>
</table>
<!-- Left menu end -->
</td>
<td width="95%">
<!-- Main content view start -->
<?
print( $MainContents );
?>
<!-- Main content view end -->
</td>
</tr>
</table>
<?
// Store the statistics with a callback image.
// It will be no overhead with this method for storing stats
//
$StoreStats = $ini->read_var( "eZStatsMain", "StoreStats" );
if ( $StoreStats == "enabled" )
{
// create a random string to prevent browser caching.
$seed = md5( microtime() );
// callback for storing the stats
$imgSrc = $GlobalSiteIni->WWWDir . "/stats/store/rx$seed-" .
$REQUEST_URI . "1x1.gif";
print( "<img src=\"$imgSrc\" height=\"1\"".
" width=\"1\" border=\"0\" alt=\"\" />" );
}
?>
</body>
</html>
With that code in place, copy the style.css file from the sample ZIP to [ezpublish_root]/sitedesign/phppoint and have a look! This is the first version of frame.php. You'll make some more changes later, but for now, that's enough for us to see how the new design looks within eZ publish.
As a side note, one other thing that's been snuck in here is XHTML. It looks very similar to HTML, but has to conform to the rules of "well formed" XML. For example, <img src="myimage.png"> in HTML is <img src="myimage.png" /> in XHTML.
Sections
Now you may have noticed that I changed a few settings here and there that apply to something called "sections". In fact I eliminated sections completely from the site, because I'm aiming for something simple. Just so you know, sections are a means to produce "multiple versions" of your site, each delivering different functionality, and perhaps a different design. You could use this for anything from simply "skinning" your site for kicks, to something more complicated, such as SitePoint, for example: you might have multiple sections corresponding to SitePoint [72], WebmasterBase [73], eCommerceBase [74], PromotionBase [75], ServicesBase [76] and SitePointForums [77].
In general, the trick with designing sections is to look at the other folders under sitedesign directory, and copy the bits you need in order to include the relevant modules in your design. Once you've set your own directories to correspond to different designs, alter site.ini:
Sections=enabled
Then create the sections in the "site manager".
Sections are accessed by placing a number at the end of the URL, corresponding to the section ID shown in "site manager". For example, try the following URLs:
- http://publishdemo.ez.no/article/articleview/1/1/1/ [78]
- http://publishdemo.ez.no/article/articleview/1/1/2/ [79]
- http://publishdemo.ez.no/article/articleview/1/1/3/ [80]
- http://publishdemo.ez.no/article/articleview/1/1/4/ [81]
Using the URLTranslator module, you can build easy links to point to particular sections from within the respective frame.php scripts. You'll see exactly how this works when we build a static page.
You'll find more information on sections at http://developer.ez.no/article/articleview/142/1/19/ [82]
Hitting the Module
The overall design is now in place. The next step is to apply it to the eZ publish modules you're using, namely eZ article in this example. Before you go any further, though, let's have a quick look at the directory structure of eZ article, examining the directories you need to know about. This will be more or less the same for all other eZ publish modules:
- admin directory: Contains all the code for the back end interface to the eZ article module. If you're interested in modifying the look of the admin interface, below the
admindirectory you'll find atemplatesdirectory with yet another directory calledstandard. The same principle as thesitedesigndirectory applies here. You can create your own directory along side thestandarddirectory, then point eZ publish to it using theAdminTemplateDir:variable insite.iniunder the eZ article section. The other important directory under theadmindirectory iscache, which is used to store pages from the admin interface to the eZ article module. - cache directory: this is used to cache eZ article pages viewed by your site's visitors. As I mentioned, the
PageCachingvariable insite.inican be used to switch this caching on or off. While apply a design, it's best to leave it disabled, then enable it when you're finished. - classes directory: contains all the classes used to make eZ article tick. In general, you won't need to worry about these, although documentation can be found at http://developer.ez.no/doc/view/index [83].
- user directory: in here you'll find a series of PHP scripts that can be used handle the eZ article module in different ways. These tie the eZ article classes together with the overall eZ publish classes to deliver eZ article pages. You'll see more of this in practice shortly. Below the
userdirectory you'll find atemplatesdirectory, which contains another directory calledstandard-- you'll be making use of this in a moment. Images used within by the templates are found in theuserdirectory inimages/standard. And finally, in theintldirectory are files that allow parts of the page to be displayed in multiple languages -- you'll need this directory when applying your design.
Now that you have an idea of a modules directory structure, it's time to apply that design!
eZ article
As you already now, eZ article is used to create "normal" Web pages. Using eZ article you can publish both "dynamic" articles and "static" pages. Static pages are used for sections such as "About Us" content, which will rarely change and might always be available from a menu. Also, making use of the eZ form module, you can easily attach HTML forms to articles -- another thing I'll be examining shortly.
The first step, though, is to apply the design to eZ article. Open up the directory [ezpublish_root]/ezarticle/user/templates/ and copy the standard directory to phppoint. Do the same with [ezpublish_root]/ezarticle/user/images/standard for good practice, although we won't be changing the images in this article.
Now edit the site.ini to point eZ publish at the new directories:
[eZArticleMain]
AdminTemplateDir=templates/standard/
TemplateDir=templates/phppoint/
ImageDir=/images/phppoint/
While you're there, update this setting as well (also under the eZ article section):
UserSubmitArticles=disabled
On your site, you'll only want administrators to be able to submit articles. So don't forget to clear out the [ezpublish_root]/classes/cache/ directory again as you updated the ini file!
Returning to frame.php for a moment, notice that in building the left hand site menu, there was the code:
<!-- Left menu start -->
<?
$CategoryID = 0;
include( "ezarticle/user/menubox.php" );
?>
This puts a "menu box" from eZ article in frame.php so that it will appear on all pages. Of you open menubox.php from the specified directory, and search the file for ".tpl" you'll find it makes use of a template called menubox.tpl.
In general, there are two ways to access elements of the eZ article module -- via include, or via a URL. Here I've included a script that generates a menu box. You'll see later how to use the URL method when you create a link to a static page. All the available elements are described at http://doc.ez.no/article/archive/55/ [84].
Now head to your [ezpublish_root]/ezarticle/user/templates/phppoint directory and open up the file menubox.tpl. Also, open up the file [ezpublish_root]/ezarticle/user/intl/en_GB/menubox.php.ini.
Template Alert
Now as I mentioned in earlier rant, PHP is a templating system. Unfortunately eZ systems opted to create their own templating system -- and their own markup language for it. There's even an entire templating class, and if you look in menubox.tpl you'll find code like:
$t->set_var( "sitedesign", $GlobalSiteDesign );
...where a PHP variable is mapped to a template variable. This constitutes a significant processing overhead, and slows down your Web pages -- why eZ systems chose not to use the same approach as frame.php I can't say. A large amount of processing could have been avoided had the system used include() [85], or opened the file with eval() [86]. Hopefully this is something that'll change with the release of eZ publish 3.0, to either utilise PHP's native ability to template, or even use XML and XSLT to allow eZ publish to deliver multiple interfaces like XHTML, WML, PDF etc. However, right now we'll have to work with the existing templating system.
Take a look at the start of menubox.tpl and you'll find:
<tr>
<td colspan="2" class="menuhead">{intl-title}</td>
</tr>
The template variable {intl-title} lives in [ezpublish_root]/ezarticle/user/intl/en_GB/menubox.php.ini so let's update that first:
[strings]
title=Inside...
latest=Recent
...
front_page=Home
I'm going to take a liberty now, and update the entire menubox.tpl in one step:
<table class="menu">
<tr>
<td colspan="2" class="menutitle">{intl-title}</td>
</tr>
<tr>
<td class="menuitem">
<a href="{www_dir}{index}/article/frontpage/1">{intl-front_page}</a>
</td>
</tr>
<tr>
<td class="menuitem">
<a href="{www_dir}{index}/article/archive/0/">{intl-latest}</a>
</td>
</tr>
<tr>
<td class="menuitem">
<a href="{www_dir}{index}/article/author/list/">{intl-authors}</a>
</td>
</tr>
<tr>
<td class="menuitem">
<a href="{www_dir}{index}/article/sitemap/">{intl-site_map}</a>
</td>
</tr>
</table>
<br />
<table class="menu">
<tr>
<td colspan="2" class="menutitle">{intl-categories}</td>
</tr>
<!-- BEGIN article_category_tpl -->
<tr>
<td class="menuitem">
<a href="{www_dir}{index}/article/archive/{articlecategory_id}/
{section_id}">
{articlecategory_title}</a>
</td>
</tr>
<!-- END article_category_tpl -->
</table>
Two things are worth noting in the above template.
First of all, I've added a "1" at the end of the following link, so that when visitors click on "Home", they'll end up in the correct section. This is not strictly necessary, but it gives you an idea of how to use sections should you need to.
<a href="{www_dir}{index}/article/frontpage/1">{intl-front_page}</a>
Also, the HTML comments (shown again below) are used by eZ publish to insert loops into templates -- this row will appear more than once, depending on how many categories of article you create from the eZ publish administration interface.
<!-- BEGIN article_category_tpl -->
...HTML here...
<!-- END article_category_tpl -->
Now that you have some idea of how templates work and how to apply them, I'm going to pull that "Here's one I prepared earlier..." trick they use in all the cookery programs...
To cut a long story short, I've updated the following template files:
articleheaderlist.tplarticlelist.tplarticlestatic.tplarticleprint.tplarticleview.tplauthorlist.tplauthorview.tplfrontpage.tplmailtofriend.tplmenubox.tplsearch.tplsitemap.tpl
Also in [ezpublish_root]/ezarticle/user/intl/en_GB/menubox.php.ini I updated:
articlelist.ini.phpfrontpage.ini.phpnubox.tpl
All these and main frame.php you'll find in a ZIP file below (I wouldn't want you to have to do any really hard work, after all)...
A Static Page
phpPoint.com is now almost ready! All you're missing is the two final links on the menu for "Contact" and "About Us". Beginning with the last first, I'll make the "About Us" page, that tells visitors what the site is, and who's running it etc. To do this I'll make use of a "static page".
First, head to the eZ publish back end administration and go to the eZ article module (on the far left). The default view is the "Article Archive" -- in here you should see five categories (unless you changed them while you did your homework): Standard, Intranet, Trade, News and Static Pages.
First things first! So that your site looks right, rename the first four to "Design", "Develop", "Deploy" and "Reviews", respectively.
Next, edit the Static Pages category -- notice that "Exclude from search" is selected. This prevents any pages put here from turning up in eZ publish searches, which is what's required.
Now click on "New Article" and create an article called "About phpPoint". Make sure the category is set to "Static Pages" and that "Publish" is checked. Enter some text in the Content box, using the eZ article tags you taught yourself about. For those who haven't done their homework, here's some text for you to use:
<header 1>About phpPoint</header>
phpPoint is a delivers high quality articles on PHP
to developers looking to take their expertise to
the next level. Favourite topics include:
<bullet>
Object Orientation
N-Tier Design
Application Integration
And much, much more...</bullet>
Once you've created the page, click on its name within the Article Archive and see what ID it's been assigned in the URL -- the number at the end is what you're interested in. For example, in the URL "index_admin.php/article/articlepreview/12/" the article ID is 12.
Now, edit site.ini:
URLTranslationKeyword=section-phppoint;aboutus
Head to the URLTranslator in the admin section and click "New". Enter the source URL: /aboutus and destination URL of /article/articlestatic/12 (assuming your article ID was 12). Notice the URL I've entered there -- that makes use of the "articlestatic" element within the eZ article module, as opposed to a normal "dynamic" article URL like /article/articleview/12.
You can now place a link to this page from frame.php like this:
<a href="<? print $GlobalSiteIni->
WWWDir; ?>/index.php/aboutus">About Us</a>
And with that, our first static page is created!
Using eZ form
There's just one page (the Contact page) to go before phpPoint is finished!
First things first. Let's create another static article called "Thank You" and insert this content:
<header 1>Thank you for your feedback!</header>
An email has been sent to the phpPoint staff. We'll get
back to you shortly.
As before, note the ID of this article.
Next, head to the eZ form admin tool and your default view will be the Form List -- click "Create Form" here. Enter your email address in "Send form to:" and "Your e-mail address:" Then, for "Show this page on completion:" enter "/article/articlestatic/13" (or whatever your ID was), then click OK. Now select the name of the form in the form list and click "New Element". The eZ form tool takes a little getting used to, but it allows you to build all the parts of a normal HTML form. In eZ form terms, the following element types correspond to these HTML form elements:
Textline: <input type="text" />
Textarea: <textarea />
Radiobox: <input type="radio" />
Multiple Select: <select multiple />
Dropdown: <select />
Checkbox: <input type="checkbox" />
Note also that "Breaks" adds a <br /> after element, so that the next one appears on a new line. "Size" determines the length for certain fields (like Textlines) and "Required" forces this field to be completed. Use "Update" to add elements or change their settings.
Now, our mission is to build a form that has these fields:
[Field Name] [Type] [Size] [<br />] [Required]
[Your Name:] [Textline] [20] [Yes] [Yes]
[Your Email:] [Textline] [30] [Yes] [Yes]
[Message:] [Textline] [n/a] [n/a] [Yes]
When you're done, head back to the eZ article admin and create another static page called "Contact". In the Content field place simply the tag <form>, then at the bottom click "Add Item", making sure "Form" rather than "Images" is selected when you do so.
Now select the form called "Contact". Assuming this article has an ID of 14, you can add the following link to frame.php:
<a href="<? print $GlobalSiteIni->
WWWDir; ?>/index.php/article/articlestatic/14/">Contact</a>
Now visitors will see the form when they click on the "Contact" link. Completing the form will send them to the "Thank You" page you just made.
The ZIP File
As mentioned, I've done all the hard graft for you, so you can just copy and paste all the template files etc. to the right places in your eZ publish installation. It's all stored in the phpPoint ZIP [87].
Just so you know, the ZIP contains only those files I've modified, and I've stored them in a directory structure that mirrors the eZ publish directory structure, to help you copy them to the right places. For safety's sake, it's best to extract the ZIP to a temporary directory, rather than straight into the eZ publish directory, and then copy the files across manually.
phpPoint Finished!
Congratulations! You're now the proud owner of an eZ publish site! Are you starting to see why they call it "eZ"?
One final thing remains, which is to enable page caching again for the eZ article module, now that you're finished updating the design. And of course, you know how to do that yourself now!
The site you created was very simple, using only the eZ article module, but it already allows you to collaborate with a team of writers to publish anything you like with images, flash etc. etc. Now imagine all the things you can do with eZ trade, eZ forum, eZ filemanager, eZ newsfeed and all the other eZ publish modules..!
With eZ publish, we're talking state-of-the-art Websites. Even better is the fact that eZ publish doesn't require that its users know irritating details like HTML. As you've seen using the eZ article admin module, a non-technical person could update the site, once they'd been trained to use eZ publish. Better yet, they could use eZ publish Desktop Edition [88] to write articles on their own machine, and then update the site directly (unknowingly making use of XML-RPC [89]) without risking the site going down at the moment they try to add update their document, which would of course see them lose all their work.
So if you're happy with the existing modules, you now have all you need to get building with eZ publish. But if you really want it all, the third part of this series will show you how to write your own eZ publish modules in PHP, allowing you to take advantage of the framework to rapidly develop and deploy your own applications.
Until then, may all your publishing be eZ! (Did I use that gag already?)
Part 3
Welcome to the third installment of this series. If you've been keeping up with parts one and two, you'll have been introduced to the eZ publish [90] content management system (which is in fact an application development framework in disguise) and installed your own version. By the end of part 2, you built your own fictional Website, phpPoint, and in doing so, learnt how to quickly apply your own design to the main eZ publish module: eZ article.
So if you've come this far, you'll hopefully be starting to agree that eZ publish is PHP's killer application. However, you may have been disappointed that despite all the many eZ publish modules available, there's still something you need, which isn't already on offer. That's what this article is all about.
I'll be taking you through building your own module for eZ publish. You'll get to see under the hood of eZ publish and find out how, by taking advantage of its structure and code library, writing and deploying your own applications is... eZ!
As an application development framework, eZ publish is designed to take the burden off busy Web developers like you and me, allowing us to spend more time soaking up rays by the pool (or at least dreaming about it).
But first, a word of warning. So far in this series no real PHP has been needed. That all changes here; we'll be taking the plunge head first into object orientation using PHP! Given the mission -- building our own eZ publish module -- I'm going to assume you have a basic knowledge of PHP's object orientation, so we can concentrate on the problem at hand. If you're unsure, make sure you've read Kevin Yank's Object Oriented PHP: Paging Result Sets [91], a great primer on how PHP classes work. That should give you all the skills you need for this article. If you want more, you'll find a comprehensive list of resources here at the SitePointForums [92].
Wait! Before you run off and hide, let me try to sell you the idea that object orientation is a good thing. As PHP coders, we're an impatient bunch -- we want it done now, usually with the first code that comes to mind. Perhaps we throw in a few user-defined functions when we really have to, but otherwise, it's code first, think second. That's all well and good, and PHP is a great language with which to build quick and dirty solutions, but how often have you found yourself entirely re-writing a piece of code you wrote months ago? And what about when you need to add new functionality to your Web application? Do you end up having to modify numerous scripts to make it all fit together?
With object orientation we can save ourselves massive amounts of time, producing code that is easy to read, update and re-use. If you make it your mission to put together a PHP class library with each project you work on, then the longer you develop, the bigger your library will get, saving you a lot of time on future projects.
But there's more to object orientation the just saving development time. Using PHP's classes, you can build far more powerful applications, accomplishing feats that would be either insane or nigh-on impossible with procedural code.
You first encounters with object orientation may leave you with the impression that it's just a nice way to put all your user defined functions in one place, for you to include in your otherwise procedural scripts. But the true power of object orientation is in how multiple classes interact. Imagine building a Website where index.php only contains...
include_once ('./phpbin/webengine.class.php');
$website= new WebEngine;
Once you get a toe in the door of object orientation, you'll probably never look back, but having said all that, building a fully object oriented application from scratch can often be a daunting task. That's where eZ publish comes in. As an application development framework, all the tough decisions have already been made for you. All you need to do is follow some simple guidelines and you'll be constructing your first object oriented PHP application in no time.
So by the time you finish reading this article you should have a good idea of how to write code which slots nicely into the eZ publish framework, while having gained a deeper insight into how to build object oriented PHP applications.
Here's today's shopping list:
- The Drawing Board: designing the application
- Class Act: putting together the classes
- Plugging In: fitting the code into eZ publish
- Zipping Up: the final act
Time to roll up those sleeves...
The Drawing Board
Before you reach for that text editor and start hacking, let's make a plan.
Our fantasy Website, phpPoint, regularly receives mail from people asking questions about PHP. To prevent site administrators from answering the same question twice, you decide it's time to put up a list of commonly asked PHP FAQs. The big question is: how to display them?
Now you could take advantage of eZ article and publish a either the entire FAQ as a single article or new article for every FAQ. Either way, you're not convinced. Better would be a categorised list of questions and answers that visitors can browse and search. So it's time to build the eZFaq module...
The FAQs will come in two main groups: categories, which are broad "overall" groupings, and headings which are "sub groups" of category. The FAQs themselves will appear under these headings.
When viewing the contents of a category, all headings under that category should appear, along with all FAQs associated with each heading, so that a simple view of the page might look like:
Category: "Object Orientation" [Select Box to change category]
Heading: "Basics"
Question: "How do I access a PHP class from my script?" [Link to view FAQ]
Question: "PHP Class Syntax" [Link to view FAQ]
Heading: "Inheritance"
Question: "How do I extend an existing class?" [Link to view FAQ]
Question: "Why doesn't the parent constructor work?" [Link to view FAQ]
Meanwhile, the phpPoint administrators need to be able to create, edit and delete FAQs. You decide they will first see a list of categories. Clicking on a category gives a list of headings then clicking on each heading, the FAQs within that heading will be available. The end result should be something along the lines of a Web directory like dmoz.org [93].
We have a rough idea of where we're going now, and if you look closely at the above description, you'll notice it describes a data relationship, and makes the suggestion that users should be able to search FAQs. How would this look as a database schema? Using the eZ publish table and column naming conventions it might be:
#
# Table structure for table `ezfaq_faq`
#
CREATE TABLE ezfaq_faq (
ID int(11) NOT NULL auto_increment,
Question varchar(255) default NULL,
Answer text,
Published enum('1','0') NOT NULL default '0',
Date int(11) NOT NULL default '0',
HeadingID int(11) NOT NULL default '0',
PRIMARY KEY (ID),
FULLTEXT KEY Question (Question,Answer)
) TYPE=MyISAM COMMENT='Many to one on ezfaq_heading';
Note: eZ systems have opted to store date information in MySQL as simple integers (UNIX time stamps will be stored in them, in fact) rather than using the MySQL 'DATETIME' column type -- in general, a good idea for database abstraction.
Another note: if you're querying the FULLTEXT index on Question and Answer -- you'll see this in action later, but it's worth having a look at the MySQL manual on Full Text Searches [94] as well as Building a full text search engine with PHP [95].
#
# Table structure for table `ezfaq_heading`
#
CREATE TABLE ezfaq_heading (
ID int(11) NOT NULL auto_increment,
Heading varchar(255) default NULL,
Published enum('1','0') NOT NULL default '0',
CategoryID int(11) NOT NULL default '0',
PRIMARY KEY (ID)
) TYPE=MyISAM COMMENT='One to many on ezfaq_faq
- many to one on ezfaq_category';
#
# Table structure for table `ezfaq_category`
#
CREATE TABLE ezfaq_category (
ID int(11) NOT NULL auto_increment,
Category varchar(255) default NULL,
Published enum('1','0') NOT NULL default '0',
PRIMARY KEY (ID)
) TYPE=MyISAM COMMENT='One to many on ezfaq_heading';
Notice the relationships between the tables (see the comment with each table for a description)? We'll need these when we access the tables. Also, we'll use the "Published" column that appears in each table as a switch -- this way, rows can be made visible to phpPoint administrators so they can edit them without being visible to our site's visitors until they're ready.
So far, so good. The next question is: what "events" should the FAQ module be able to perform? And not just for users, but also for site administrators...
- Users (site visitors)
- List categories from the ezfaq_category table
- List headings from ezfaq_heading table, grouped under categories
- List FAQs from the ezfaq_faq table, grouped under headings.
- Search for FAQs
- View a single FAQ
- phpPoint Administrators
- List/ Create / Edit / Delete categories
- List/ Create / Edit / Delete headings under categories
- View / List/ Create / Edit / Delete FAQs under headings
So what have we got here? We've effectively outlined multiple "tiers" of an application. We began be looking at how the application should be presented to its users. Then we created a database schema that the application will access. Finally we defined a list of events we can use to build the logic of our application.
In general, thinking about applications this way -- regarding each as a series of layers -- allows us to treat each one as a separate problem and go about solving it without needing to worry about what's happening elsewhere. If I can interrupt your reading for a moment, you might find this an interesting read [96].
As an application, eZ publish structures itself, more or less, into "presentation" (XHTML), logic (the classes and code that make it hum), and data (the database and abstract classes used to access the data). By following the above design strategy, we'll find it fits nicely with building the module itself.
Class Action
We'll begin by tackling the "logic" in the middle first. I'll focus on particular scripts throughout the rest of this tutorial, to save column inches. The complete code is included for you in a ZIP at the end of the article. Note that the code here is based upon the code you'll find in the [eZ publish_root]/ezexample/ directory of your eZ publish installation. The eZ example module is a simple example of how constructing modules works, backed up by the tutorial: How to write database independent eZ publish modules [97]. We're aiming to go a lot further than eZ example here, but for starting your own modules, eZ example is useful code to kick off with.
We've already created the database schema and eZ publish has an abstract database class [98] with which to access it. The eZPublish framework will handle all matters, like connecting to the database and setting up the environment for us, so all we need to do is develop our classes "in a vacuum".
Specifically we're aiming to write code that will be capable delivering the data required to respond to the "events" we defined above. Also, we don't care about "presentation" issues right now (such as generating XHTML): all we're going to do is collect some data from the database, transform it, then deliver the results as PHP variables to anyone who happens to be passing through. This is an important point -- it's what the separation of logic from presentation is all about. As we're only going to deliver data with our logic classes, the "presentation" of that data is a separate problem. What we gain by doing this is the ability to present any user interface we like -- XHTML, WML, Flash, PDF, XML-RPC, SOAP -- you name it (PHP can do them all)! But don't let me distractou with all that right now-- just keep it in the back of your mind as you read on.
Looking at the events we've listed, it's pretty clear that the classes can be grouped around three different "things": categories, headings and FAQs. So let's decide we'll have three classes to deal with the user (site visitor) events, and three more to handle the administrator events.
But hold on a second! Some of what the administrators need to accomplish will be very similar to the users' events, such as being able to list and view FAQs. Aren't we going to be reproducing some of our code here? The answer is 'No', thanks to something called inheritance -- the ability of one object (a child) to inherit member functions from another (parent) object.
In PHP, this works using the extends [99] keyword [100]. The easiest way to see this is with an example.
<?php
// The parent class
class Senior {
function growUp ( $string ) {
return strtoupper($string);
}
}
// The child class extending the parent
class Junior extends Senior {
function spellIt ($string) {
return strrev($string);
}
}
// Define a string
$text = 'Hello World!';
// Instantiate the child class to an object
called "george" (just for fun)
$george= new Junior;
// Look - using the parent member function!
$text = $george->growUp ( $text );
// Now use the child member function
$text= $george->spellIt ( $text );
echo ( $text );
?>
Results in...
!DLROW OLLEH
Hmmmm?!?
Using PHP's extends keyword, we can create child classes that inherit the "traits" of their parents: a very powerful technique for creating re-usable code.
So, going back to our eZFaq module, here's what we do: create three parent classes; eZFaq, eZFaqHeading and eZFaqCategory. Then we extend the parents with the child classes eZFaqAdmin, eZFaqHeadingAdmin and eZFaqCategoryAdmin respectively. Designing the classes this way, the advantage is not only re-usability. If we keep the parent classes to "secure" operations, such as fetching data, while the child "Admin" classes are the only place where "insecure" operations like UPDATEs and DELETEs can occur, we can ensure that our site visitors can never accidentally delete data through a mistake or hole we've left in our code.
I'm going to be concentrating on the eZFaq class and its child here, but you'll find all the classes in the ZIP at the end of this article. Down to business...
Sketching a Class
A good trick help you start to write a PHP class is to begin by just laying it out as a whole, while avoiding writing code for particular member functions. Here's my sketch of the eZFaq class:
<?php
//!! eZFaq
//! Base class for public use
class eZFaq {
// CREATORS
/*!
Constructs a new eZFaq object.
*/
function eZFaq( ) {
}
// MANIPULATORS
/*!
Gets a single faq from the database
*/
function get( ) {
}
/*!
Gets a list of FAQs from the database
*/
function &getAll( ) {
}
/*!
Searches the ezfaq_faq table for a string
*/
function &search() {
}
// ACCESSORS
/*!
Returns the current faq id, for links etc.
*/
function id() {
}
/*!
Returns the current faq question
*/
function question() {
}
/*!
Returns the current answer
*/
function answer () {
}
/*!
Returns the date
*/
function date () {
}
/*!
Returns the Published switch
*/
function published () {
}
/*!
Returns the current heading id
*/
function headingId () {
}
}
Easy wasn't it?
Let's see what we have here. First we have "CREATORS": methods that create something when the eZFaq object is instantiated. Then we have "MANIPULATORS": member functions that collect data from somewhere and transform it. These correspond to the events we want our site visitors to be able to perform. And finally we have "ACCESSORS": functions we can use to access the objects member variables (notice that these correspond to the fields in the database).
One thing to pay attention to is: function &getAll() {. This will return a reference to some data, rather than the data itself. For more detail on references, try this article: PHP References explained [101].
You may also been wondering about the exclamation marks that appear in some of the comments. The eZ publish class documentation [102] is generated automatically from the code and comments using eZ phpdoc [103] -- a nice way to save time writing documentation...
OK -- that deals with all the actions we want to allow site visitors to perform. What about the phpPoint administrators?
class eZFaqAdmin extends eZFaq {
// CREATORS
/*!
Constructs a new eZFaqAdmin object.
Calls the parent constructor
*/
function eZFaqAdmin ( ) {
eZFaq::eZFaq ( );
}
// MANIPULATORS
/*!
Inserts or updates a faq
*/
function store() {
}
/*!
Deletes a faq
*/
function delete( ) {
}
// ACCESSORS
/*!
Assigns faq question to local member variable
for store() method
*/
function setQuestion() {
}
/*!
Assigns faq answer to local member variable
for store() method
*/
function setAnswer($Answer) {
}
/*!
Assigns the Published switch for the store() method
*/
function setPublished ($Published) {
}
/*!
Sets the current heading id
*/
function setHeadingID ($HeadingID) {
}
}
?>
Now we've covered all the administrator "events" as well, providing the method store() to edit and create FAQs, while delete covers the deletion of FAQs. The ACCESSORS we'll be using here will give data to the object. What's more, as eZFaqAdmin extends eZFaq: if we instantiate eZFaqAdmin, we can access to everything eZFaq has to offer as well.
Note: in the CREATOR method eZFaqAdmin(), we find eZFaq::eZFaq ( );. Here we've use something called the paamayim nekudotayim operator [104] -- ::. This is used to invoke a member function of a class without instantiating the entire class. In this case it's important -- when you instantiate a child class, only its own constructor will be invoked, not the parent constructor. But for our class, we want the parent constructor to be invoked (for reasons we'll see in a moment), so we specifically tell the child to do so.
And that's it. Our eZFaq classes our finished! Well... almost: we just need to fill in the blanks.
Total Class
Reaching for the oven: "Here's one I prepared earlier"...
Breaking the finished class down, first we have:
<?php
//!! eZFaq
//! Base class for public use
// Include the "abstract" eZ publish classes */
include_once( "classes/ezdb.php" );
include_once( "classes/ezdate.php" );
include_once( "classes/ezdatetime.php" );
include_once( "classes/ezlocale.php" );
class eZFaq {
The first thing we need to do is make some eZPublish base classes available by including them, namely: eZDb [105], eZDate [106], eZDateTime [107] and eZLocale [108] (have a glance at the documentation so you know what they do).
Next:
// CREATORS
/*!
Constructs a new eZFaq object.
If $id is set the object's values are fetched from the
database.
*/
function eZFaq( $id=-1 ) {
if ( $id != -1 ) {
$this->ID = $id;
$this->get( $this->ID );
}
}
The constructor function eZFaq (with the same name as the class) does something very interesting. If it receives an $id (of a FAQ from the database), it assigns the value to a membevariable, and then invokes the get() member function (which, you remember, fetches a single FAQ from the database).
// MANIPULATORS
/*!
Gets a single faq from the database
*/
function get( $id=-1 ) {
$db =& eZDB::globalDatabase();
$ret = false;
if ( $id != -1 ) {
$db->array_query($faq_array,
"SELECT * FROM ezfaq_faq ".
"WHERE ID='$id'" );
if ( count( $faq_array ) > 1 ) {
die( "Error: Duplicate faq ID found in database.".
"This shouldn't happen." );
} else if ( count( $faq_array ) == 1 ) {
$this->ID =& $faq_array[0][$db->fieldName("ID")];
$this->Question =& $faq_array[0][$db->fieldName("Question")];
$this->Answer =& $faq_array[0][$db->fieldName("Answer")];
$this->Date =& $faq_array[0][$db->fieldName("Date")];
$this->Published =
& $faq_array[0][$db->fieldName("Published")];
$this->HeadingID =
& $faq_array[0][$db->fieldName("HeadingID")];
$ret = true;
}
}
return $ret;
}
Now the get() method is used to fetch a single FAQ from the database. It starts off by instantiating an eZDB object which it can then use to perform a query and fetch the results. eZ systems have pulled some clever tricks in the background to allow us to use a static method from the eZDB class. We won't worry about that here, but suffice it to say that because the eZ publish documentation is good, we can just use the class without caring about what's going on behind the scenes. That's one of the joys of object orientation!
The get() method then queries the database to fetch a single row and assign the results to local member variables (e.g. $this->Question).
Moving on...
/*!
Gets a list of faqs from the database
*/
function &getAll( $headingID = -1 ) {
$db =& eZDB::globalDatabase();
$return_array = array();
$result_array = array();
if ($headingID != -1 ) {
$where = "WHERE HeadingID = '$headingID' ";
}
$db->array_query($result_array,
"SELECT ID ".
"FROM ezfaq_faq ".
$where.
"ORDER BY Question" );
for ( $i=0; $i<count($result_array); $i++ ) {
$return_array[$i]=
new eZFaq($result_array[$i][$db->fieldName("ID")] );
}
return $return_array;
}
The getAll() method fetches a list of FAQs from the database. If it receives a "headingID", it adds a conditional WHERE to the query, fetching only those FAQs for a heading we're interested in.
The really cunning part is here though:
$return_array[$i]=new eZFaq($result_array[$i][$db->fieldName("ID")] );
See what that's doing? The getAll() method is instantiating the eZFaq class itself, using the FAQ ID if found for each row it selected. Now have a look at the constructor method eZFaq() again. Making sense now? For each row that getAll() finds, the constructor calls the get() method which performs another query to fetch all the details for that row. What this does for us is place the all code for handling a single row within a single method. Now imagine we change the ezfaq_faq table at some later date, to add a new column: using this approach, for our MANIPULATORS, we only need to make one change!
Next we have:
/*!
Searches the ezfaq_faq table for a string
*/
function &search($searchstring) {
$db =& eZDB::globalDatabase();
$return_array = array();
$result_array = array();
$db->array_query($result_array,
"SELECT ID, Question, Date ".
"FROM ezfaq_faq WHERE ".
"MATCH(Question,Answer) ".
"AGAINST ('$searchstring') " .
"ORDER BY Question" );
for ( $i=0; $i<count($result_array); $i++ ) {
$return_array[$i] =
new eZFaq( $result_array[$i][$db->fieldName("ID")] );
}
return $return_array;
}
The search() method is very similar to the getAll() method, except that it accepts $searchstring, which is use to perform a FULLTEXT search against the FULLTEXT index we defined in the database. In addition to the recommended reading above, on FULLTEXT searching, check out Optimizing your MySQL Application [109] as well, to find out more about indexes.
Next we have the ACCESSORS, which we use to access member variables (that might have been set by the get() method, for example):
// ACCESSORS
/*!
Returns the current faq id, for links etc.
*/
function id() {
return $this->ID;
}
/*!
Returns the current faq question
*/
function question() {
return $this->Question;
}
/*!
Returns the current answer
*/
function answer () {
return $this->Answer;
}
/*!
Returns the date
*/
function date () {
$locale= new ezLocale;
$datetime = new ezDate;
$datetime->setTimeStamp($this->Date);
return $locale->format($datetime);
}
/*!
Returns the Published switch
*/
function published () {
return $this->Published;
}
/*!
Returns the current heading id
*/
function headingId () {
return $this->HeadingID;
}
One thing to notice is the date() method, which takes advantage of the ezLocale and ezDate classes to convert a UNIX time stamp into a "user friendly" date. In case you weren't aware, a UNIX time stamp is the number of seconds that have passed since the beginning of 1970 (it could have been 1969 but no one's too sure how many seconds passed that year). For an in-depth discussion of dates and times in PHP, head to Date/Time Processing With PHP [110].
Finally we have the data members:
// DATA MEMBERS
var $ID;
var $Question;
var $Answer;
var $Date;
var $Published;
var $HeadingID;
}
For those of you used to putting the data members at the start of a class, I'm following the eZ publish convention. There's nothing to stop you placing the var definitions at the end of the class in PHP, and in terms of the way people think, perhaps it's more logical to start with the class member functions, then follow with the member variables, to help make the code readable.
So that's our parent eZFaq class complete! Notice that it makes no comment on what the database is (it uses the eZDB abstract class) and it doesn't output anything directly to the end user (do you see any XHTML anywhere?). We can do anything we like with the variables it returns to us -- put them in an XHTML page, convert them into an XML document, place them in a flash movie -- whatever takes our fancy!
Moving on again, let's look at the child eZFaqAdmin class.
class eZFaqAdmin extends eZFaq {
// CREATORS
/*!
Constructs a new eZFaqAdmin object.
If $id is set the object's values are
fetched from the database.
*/
function eZFaqAdmin ( $id=-1 ) {
eZFaq::eZFaq ( $id );
}
It starts of by extending the parent, and is capable for receiving FAQ IDs as well, which it hands off to the parent constructor using eZFaq::eZFaq ( $id );.
Next, the store() method:
// MANIPULATORS
/*!
Inserts or updates a faq
*/
function store() {
$db =& eZDB::globalDatabase();
$Question = $db->escapeString( $this->Question );
$Answer = $db->escapeString( $this->Answer );
$timeStamp =& eZDateTime::timeStamp( true );
$Published = ( $this->Published );
$HeadingID = ( $this-HeadingID );
$db->begin();
$db->lock( "ezfaq_faq" );
$nextID = $db->nextID( "ezfaq_faq", "ID" );
if ( empty( $this->ID ) ) {
$ret = $db->query( "INSERT INTO ezfaq_faq SET ".
"ID = '$nextID', Question = '$Question', ".
"Answer = '$Answer', Date = '$timeStamp', ".
"HeadingID = '$HeadingID', Published='$Published'" );
$this->ID = $nextID;
} else {
$ret = $db->query( "UPDATE ezfaq_faq SET ".
"ID = '$nextID', Question = '$Question', ".
"Answer = '$Answer', Date = '$timeStamp', ".
"HeadingID = '$HeadingID', Published='$Published' " .
"WHERE ID='$this->ID'" );
}
$db->unlock();
if ( $ret == false )
$db->rollback();
else
$db->commit();
return $ret;
}
The store() method allows us to both INSERT and UPDATE records in the ezfaq_faq table, depending on whether it finds that the member variable $id has been set (remember, the child inherits everything from the parent; member functions and variables).
Near the start of the store() method we have:
$Question = $db->escapeString( $this->Question );
$Answer = $db->escapeString( $this->Answer );
$timeStamp =& eZDateTime::timeStamp( true );
Anyone who's run into trouble with quotes contained in strings that they want to place in a database will be relieved to see $db->escapeString(). And those of your concerned about MySQL and ANSI SQL will be happy to see how eZ publish escapes strings to be stored in Informix [111]. Basically what's happening here is we're transforming the data.
Note that eZDateTime::timeStamp( true ); creates a UNIX time stamp for the current time, which we can use to tell our visitors when a FAQ was published.
As to the database locking, rollback and committing that's going on here, I'll let you compare the relevant class documentation for MySQL [112], PostGRESQL [113] and Informix [114]. In general, all you'll need to do is copy and paste what I've done here for SQL INSERTs, UPDATESs and DELETEs.
/*!
Deletes a faq
*/
function delete( $id ) {
$db =& eZDB::globalDatabase();
if ( is_numeric( $id ) ) {
$this->ID = $id;
}
$db->begin();
$ret = $db->query(
"DELETE FROM ezfaq_faq WHERE ID='$this->ID'" );
if ( $ret == false )
$db->rollback( );
else
$db->commit( );
return $ret;
}
The delete() method is pretty straightforward. It uses the ezDB class to delete a record from the ezfaq_faq table.
And finally we have the ACCESSORS:
// ACCESSORS
/*!
Assigns faq question to local member variable
for store() method
*/
function setQuestion($Question) {
$this->Question = $Question;
}
/*!
Assigns faq answer to local member variable
for store() method
*/
function setAnswer($Answer) {
$this->Answer = $Answer;
}
/*!
Assigns the Published switch for the store() method
*/
function setPublished ($Published) {
$this->Published = $Published;
}
/*!
Sets the current heading id
*/
function setHeadingID ($HeadingID) {
$this->HeadingID = $HeadingID;
}
}
These assign the variables they receive to member variables defined in the parent eZFaq class. So, when we need to INSERT some data into the database, we first make use of these methods, and then use the store() method to add them.
And that's it: the logic for our module is done! The eZFaqHeading and eZFaqCategory classes are very similar, so I'll leave them to the ZIP file at the end of this article (they should be straightforward to understand, now that you've seen the eZFaq class). Note that I've used the same member function names in those classes as well (for example the eZFaqCategory class also has a method called getAll()).
Now it's time to plug our classes into the eZ publish framework...
Plugging In
So how do we use our classes with eZ publish and where does the creation of XHTML pages come in?
In part 2 of this series, we had a good look at the eZ publish directory structure. For our eZFaq module we need to recreate that structure, so here's what we'll need to create below our [eZPublish_root] directory:

The ZIP at the end of this article contains this structure, so you should be able unzip it directly into your [eZPublish_root].
The next thing we need to do is tell eZ publish that it has a new module, by updating site.ini (remember ours is called site.ini.php as a result of the way we installed eZ publish in part 1). We need to change this line:
EnabledModules=eZArticle;eZFaq;eZImageCatalogue;eZMediaCatalogue;
eZForm;eZStats;eZURLTranslator;eZSiteManager;eZUser
Then we need to add a section for eZFaq lower down to tell eZ publish about any "module specific" variables:
[eZFaqMain]
AdminTemplateDir=templates/standard/
TemplateDir=templates/phppoint/
Language=en_GB
DefaultSection=1
Note that with "TemplateDir" we're pointing eZ publish at the directory that contains our user templates.
Don't forget to clear your cache after making the change!
Our next move is to put together the user interface that will be presented to phpPoint visitors.
The Data Supplier
In the [eZ publish_root]/ezfaq/user directory, we need to create a file called datasupplier.php. The eZ publish framework "looks" for this file and passes it information about the current "state" of eZ publish (i.e. what a user is doing). The data supplier then acts on that information to call our logic classes then generate the right user interface (actually by including some other PHP scripts). Here's the data supplier for eZ faq:
<?php
// Decides which "event" to perform based on the URL
switch ( $url_array[2] ) {
case "faqview":
// Default to first category
if ( !$url_array[3] )
$categoryID=1;
else
$categoryID=$url_array[3];
// Default to first heading
if ( !$url_array[4] )
$headingID=1;
else
$headingID=$url_array[4];
// Default to first faq
if ( !$url_array[5] )
$faqID=1;
else
$faqID=$url_array[5];
include( "ezfaq/user/faqview.php" );
break;
case "faqlist":
// Default to first category
if ( !$url_array[3] )
$categoryID=1;
else
$categoryID=$url_array[3];
include( "ezfaq/user/faqlist.php" );
break;
case "faqsearch":
include( "ezfaq/user/faqsearch.php" );
break;
default :
// go to default module page or show an error message
print( "Error: your page request was not found" );
}
?>
The important variable here is $url_array. eZ publish hands the data supplier this variable using a switch statement, and the data supplier includes the relevant PHP script to handle presentation of the user interface. The $url_array variable is an array that's populated using the URI of the current page the user is looking at. This is easiest to see with an example:
http://localhost/public/ezpublish_2_2_6/index.php/faq/faqlist/
Here $url_array[1] is the eZ publish module name (i.e. "faq" for ezFaq).
$url_array[2] will contain the value "faqlist" -- this is the one our data supplier will respond to.
We can put further variables into the URL, and eZ publish will provide these to the data supplier as well.
For example:
http://localhost/public/ezpublish_2_2_6/index.php/faq/faqview/1/2/4/
This gives:
$url_array[1]="faq";
$url_array[2]="faqview";
$url_array[3]="1";
$url_array[4]="2";
$url_array[5]="4";
The last three might refer to a category ID, a heading ID and a FAQ ID, respectively. That should give you a big clue as to how our module will do things like displaying an individual FAQ.
The User Interface
Now let's look at one of the files PHP includes, to see what it does (I won't cover all of them, but, again, you'll find them completed in the ZIP at the end -- it's an eZ life!). Let's take include( "ezfaq/user/faqview.php" ); as the example. Here's what it looks like:
<?php
// include the class files.
include_once( "classes/INIFile.php" );
include_once( "classes/eztemplate.php" );
include_once( "classes/eztexttool.php" );
include_once( "ezfaq/classes/ezfaq.php" );
include_once( "ezfaq/classes/ezfaqheading.php" );
include_once( "ezfaq/classes/ezfaqcategory.php" );
First, include all the class files we need. Have a look at the documentation for the INIFile [115], ezTemplate [116] and ezTextTool [117], so you know what they're doing.
// Instantiate eZFaq classes
$category = new eZFaqCategory ($categoryID);
$heading = new eZFaqHeading ($headingID);
$faq= new eZFaq ($faqID);
This script allows the viewing of a FAQ, so our data supplier script provides us with these variables. We use them to instantiate our logic classes, which in turn prepare the data we want to display. One thing to note as we look back at our data supplier: I'm "re-mapping" values from the $url_array to variables with other names, like $categoryID. This serves two purposes: first it makes the faqview.php script easier to read, and second, if our URL structure changes, we only need to modify the datasupplier.php script.
Next we have:
// Instantiate the INIFile class and set language
$ini =& INIFile::globalINI();
$Language = $ini->read_var( "eZFaqMain", "Language" );
// Instantiate the template class and define the intl file using site.ini
$tpl = new eZTemplate( "ezfaq/user/" . $ini->read_var( "eZFaqMain",
"TemplateDir" ),
"ezfaq/user/" . "intl", $Language,
"faqview.php" );
This fires up the INIFile class to assign the language variable we placed in site.ini, then starts the eZTemplate class, getting the ]TemplateDir value from site.ini as well, while reading a language file we store as [eZ publish_root]/ezfaq/user/intl/en_GB/faqview.php.ini (we'll see that in a moment).
// Loads all intl template variables for replacement
$tpl->setAllStrings();
// Defines the template to be used
$tpl->set_file( "faqview_tpl", "faqview.tpl" );
setAllStrings() automatically loads international template variables for replacement. Then set_file() tells eZ publish which template file we're using here.
// Set template variable to display current category
$tpl->set_var( "category", $category->category() );
$tpl->set_var( "category_id", $categoryID );
// Set template variable to display current heading
$tpl->set_var( "heading", $heading->heading() );
Here we set some template variables for replacement with their PHP equivalent. Notice we're already using methods from our logic classes, namely $category->category(), which returns the current category name and $heading->heading(), which returns the current heading name (the "current" category/heading was defined when we instantiated the classes in this script, you remember, the values coming from the data supplier).
// Define template sections for "looping", such as table rows
$tpl->set_block( "faqview_tpl", "category_list_tpl",
"category_list" );
"Blocks" in templates are sections we wish to "loop", such as rows in a table or <option />'s in a <select /> tag. The eZ publish template parser needs to know what they're called so it can find them in a template and loop through them.
// Prepare the category_list template for parsing
$tpl->set_var( "category_list", "" );
When you use blocks, it's a good idea to start by setting them to an empty string, so that if the array we use to loop through the block has no elements, you won't get template variables turning up in the XHTML.
// Fetch all faq categories
$categoryArray = & $category->getAll();
Now we use our getAll() method to fetch a list of categories from the database (in fact an array of objects for each row in the ezfaq_category table). This allows use to loop through the database result set, parsing the template as we go.
// Insert <option>'s into categories <select>
for( $i=0; $i< count($categoryArray); $i++ ) {
// If category not published, continue
if ( $categoryArray[$i]->published() == 0 )
continue;
// Set template variables for this heading
if ( $categoryArray[$i]->category() != "" ) {
if ( $categoryArray[$i]->id() == $categoryID )
$tpl->set_var( "category_list_selected", "selected" );
else
$tpl->set_var( "category_list_selected", "" );
$tpl->set_var( "category_list_id", $categoryArray[$i]->id() );
$tpl->set_var( "category_list_category",
$categoryArray[$i]->category() );
} else {
$tpl->set_var( "category_list_id", "" );
$tpl->set_var( "category_list_category", " " );
}
// Parse the template and append to the "template buffer"
$tpl->parse( "category_list", "category_list_tpl", true );
}
So we've looped through the array, assigning template variables as we go, and finishing each loop by adding a parsed copy of the template block to the "template buffer". Note that we haven't actually displayed anything to our users yet.
$tpl->set_var( "question", $faq->Question );
$answer=eZTextTool::nl2br( $faq->Answer, true );
$tpl->set_var( "answer", $answer );
Here we assign a FAQ Question and answer to template variables, taking advantage of our eZFaq ACCESSORS. Notice the use of eZTextTool to convert new lines found in the fact to XHTML <br /> tags.
// Output the "template buffer" and print
$tpl->pparse( "output", "faqview_tpl" );
?>
Finally we use the ezTemplate pparse() method to parse and print the final template -- our users have their interface!
So that you know what they look like, here's the template file faqview.tpl that was used by faqview.php:
<table width="99%" class="nav">
<tr>
<td class="path">:: FAQ View</td>
<td align="right">
<form action="{www_dir}{index}/faq/faqsearch/" method="get">
<input class="searchbox" type="text" name="SearchText" size="10" />
<input class="searchbutton" type="submit" value="{intl-search}" />
</form>
</td>
</tr>
<tr>
<td colspan="2" align="right" class="path">
<form>
{intl-selectcategory}:
<select class="path" onChange="document.location.href='{www_dir}
{index}/faq/faqlist/' + this.options[selectedIndex].value">
<!-- BEGIN category_list_tpl -->
<option value="{category_list_id}" {category_list_selected}>
{category_list_category}</option>
<!-- END category_list_tpl -->
</select>
</form>
</td>
</tr>
</table>
<table width="99%" class="article">
<tr class="articletop">
<td class="articletitle">
<a class="articletitle" href="{www_dir}
{index}/faq/faqlist/{category_id}/">{category}</a> ->
{heading} ()</td>
</tr>
<tr>
<td class="bold">{question}</td>
</tr>
<tr>
<td>{answer}</td>
</tr>
</table>
And the international language file faqview.php.ini...
[strings]
search=Search
selectcategory=Select Category
Now I'm itching to rant again about templates but this time I'll leave it to this comment on PHP and Templates [118] from the creator of XML-RPC Class Server [119].
To summarize, there are three steps we need to take when we create the files to be included from datasupplier.php:
- Create the file itself, e.g.
faqview.php - Create the template it will read, e.g.
[eZ publish_root]/ezfaq/user/templates/phppoint/faqview.tpl - Create the international language file e.g.
[eZ publish_root]/ezfaq/user/intl/en_GB/faqview.php.ini
The Admin Interface
Moving now to the files we need for the phpPoint administrators, again eZ publish uses a datasupplier.php script, this time stored in [eZ publish_root]/ezfaq/admin/. Here's the data supplier for our eZFaq module:
<?php
// Decides which "event" to perform based on the URL
switch ( $url_array[2] ) {
// Categories
case "categorylist":
include( "ezfaq/admin/categorylist.php" );
break;
case "categorynew":
include ( "ezfaq/admin/categoryedit.php" );
break;
case "categoryedit":
$categoryID=$url_array[3];
include ( "ezfaq/admin/categoryedit.php" );
break;
case "categorydelete":
$categoryID=$url_array[3];
include ( "ezfaq/admin/categorydelete.php" );
break;
// Headings
case "headinglist":
$categoryID=$url_array[3];
include ( "ezfaq/admin/headinglist.php" );
break;
case "headingnew":
$categoryID=$url_array[3];
include ( "ezfaq/admin/headingedit.php" );
break;
case "headingedit":
$categoryID=$url_array[3];
$headingID=$url_array[4];
include ( "ezfaq/admin/headingedit.php" );
break;
case "headingdelete":
$categoryID=$url_array[3];
$headingID=$url_array[4];
include ( "ezfaq/admin/headingdelete.php" );
break;
// FAQs
case "faqlist":
$categoryID=$url_array[3];
$headingID=$url_array[4];
include ( "ezfaq/admin/faqlist.php" );
break;
case "faqnew":
$categoryID=$url_array[3];
$headingID=$url_array[4];
include ( "ezfaq/admin/faqedit.php" );
break;
case "faqedit":
$categoryID=$url_array[3];
$headingID=$url_array[4];
$faqID=$url_array[5];
include ( "ezfaq/admin/faqedit.php" );
break;
case "faqdelete":
$categoryID=$url_array[3];
$headingID=$url_array[4];
$faqID=$url_array[5];
include ( "ezfaq/admin/faqdelete.php" );
break;
case "faqview":
$categoryID=$url_array[3];
$headingID=$url_array[4];
$faqID=$url_array[5];
include ( "ezfaq/admin/faqview.php" );
break;
default:
include( "ezfaq/admin/categorylist.php" );
break;
}
?>
Basically what occurs on the admin side is the same as what happened on the user side. The two things to be aware of on the admin side are:
- When you fire up the ezTemplate class, you'll use the
AdminTemplateDirvariable fromsite.ini. - Also remember that we're using the children from our logic classes to provide the additional "admin logic", for example:
// Instantiate the template class and define
the intl file using site.ini
$tpl = new eZTemplate( "ezfaq/admin/" . $ini->read_var
( "eZFaqMain", "AdminTemplateDir" ),
"ezfaq/admin/" . "intl", $Language,
"categorylist.php" );
$faq = new eZFaqAdmin ();
I'll leave you to digest the rest of the admin and user templates from the ZIP file (you're almost there!).
There are a couple of additional things to be aware of when you create the admin interface.
If you put a gif file called module_icon.gif in [eZ publish_root]/faq/admin/images/, this will be used to display the module icon when you're logged in to the "back end" of eZ publish.
Additionally, you'll find a file [eZ publish_root]/ezfaq/admin/menubox.php in the zip (this appears in every eZ publish module). The file looks like this:
$menuItems = array(
array( "/faq/categorylist/", "{intl-categorylist}" ),
array( "/faq/categorynew/", "{intl-categorynew}" ),
);
?>
The first value is a URI, relative to the eZ publish admin Web root, which eZ publish will send a user to the place to "use" this menu item. The template variables (the second element in the array) appear in the file:
[eZ publish_root]/ezfaq/admin/menubox.php
Such as:
[strings]
module_name=faq
categorylist=Category List
categorynew=New Category
Also note that the first array within $menuItems array will be used by eZ publish as the "default view" for the module -- where an administrator gets sent when they click on the eZFaq icon.
And finally!
We're almost there. The final step is simply to give our site visitors a link to get to the FAQ module. I did this by editing [eZ publish_root]/ezarticle/user/templates/phppoint/menubox.tpl to include:
<tr>
<td class="menuitem">
<a href="{www_dir}{index}/faq/faqlist/">PHP FAQs</a>
</td>
</tr>
The allows you to access the eZ faq module using a URL such as:
http://localhost/public/ezpublish_2_2_6/index.php/faq/faqlist/
If you do the same, don't forget to clear out the eZ article cache after doing so.
As an alternative, you might choose to place the link within [eZ publish_root]/sitedesign/phppoint/frame.php as we saw in part 2 of this series.
Zipping Up
So, to summarize what we've done:
- We first designed our module, sketching the user interface, defining a database schema, and listing the "events" that the module should be able to respond to.
- Next we built the "logic" classes that are responsible for transforming data from the database into a form the module can use to respond to events.
- Then we built
datasupplier.php, the "traffic cop" of our module, which directs "events" to the correct PHP scripts. - Next, we wrote the presentation scripts, which access our logic classes and use them to "populate" XHTML templates.
We now have a fully functional eZ publish module!
Phew!
If you got this far, congratulations! Here's that ZIP file [120] I've been promising...
Now you may be feeling pretty overwhelmed if you just read this whole article in one go. Don't dismay! The first thing to do is add the eZFaq module to your eZ publish installation (I've included a README file containing quick installation instructions in the ZIP). Have a look at the code and review the article as you do so, to check it makes sense.
If you come across any problems or anything you don't understand, feel free to post in the SitePointForums [121] from the link at the end of this article.
Of course, this tutorial wouldn't be complete without some homework...
Homework
Here are some ideas to help you put what you've learnt about eZ publish into practice. One general tip -- if you're ever stuck, have a look at the code of the other eZ publish modules (more or less every common problem has already been solved somewhere).
- When categories, headings and FAQs are created, right now the results are displayed using a simple
print()statement. This means administrators have to navigate back to the point where they began editing (and it will no doubt drive them crazy). Update this using template blocks (you'll see what I mean once you try it). - Right now, eZFaq has one big problem: if an administrator deletes a heading or a category, eZFaq has no means to either delete related FAQs/headings or re-assign FAQs/Headings that have lost their "parents". Something must be done!
- When an administrator creates FAQs, right now they're
POSTing "plain text", perhaps with some HTML thrown in. As you know, the eZ article module has it's own tags to allow for simple markup of pages, without requiring the user to know anything about HTML. Wouldn't it be great if eZFaq could use the tags as well? Have a look at how the eZ article module achieves this... - eZ publish has a user and groups authentication system. It would be nice, especially for big FAQs, if eZFaq could take advantage of these, so that phpPoint administrators could be assigned to maintain a single category?
- phpPoint gets an email from another Webmaster saying they want to use the FAQs. Create an XML-RPC server interface to the FAQs, as an alternative to the XHTML interface. Note that eZ publish comes with an XML-RPC class.
Otherwise, head over to the PHP section at Hotscripts [122], where there's a whole of bunch of applications just crying out to be eZ publish modules. For example, eZ publish doesn't have a Shoutbox [123] or graph creation [124] tool. Other people, such as the phpBB team, are talking about integration with eZ publish [125], too...
Wrap Up
In part 1 of this article series, I introduced you to eZ publish, and showed how to install it. Then, in part 2 we created a fictional Website called phpPoint as a means to discover how to apply our own design to eZ publish. And in this final part of the series, we saw how to create our own eZ publish modules, and learned a bit more about object orientation in PHP in the process. You now have all you need to get started building enterprise level Websites with eZ publish!
Overall, when you consider what eZ systems has accomplished so far, it's hard not to be extremely impressed. Not only have they put together a solid class library for solving all sorts of common problems that Web developers like you and I encounter on a daily basis, but they've also built a complete framework around it -- and a series of enterprise level applications to make it possible to build anything from building a simple news site, to a corporate intranet or even an ecommerce portal.
And they're not stopping there! Looking at http://sdk.ez.no/ [126], a software developers' kit is in progress that could benefit PHP developers everywhere!
So, in my opinion, "eZ publish is PHP's killer application". What do you think?
If this article has given you a taste for object oriented PHP, please do drop by SitePoint's Advanced PHP Forum [127], where you'll find plenty of information and people to help you explore the topic further. I'd personally like to thank voostind [128], author of the Eclipse PHP Library [129] and regular at the SitePointForums [130], whose insightful posts are indirectly responsible for this article.
There may be one more surprise in store (I'll leave you guessing -- check back next week!) but otherwise, may all your publishing be eZ!
[1] http://www.hotscripts.com
[2] http://developer.ez.no
[3] http://www.phpnuke.org/
[4] http://webmasterbase.com/article/917/2
[5] http://webmasterbase.com/article/917/6
[6] http://www.sitepoint.com/article/917/13
[7] http://www.php.net
[8] http://ez.no
[9] http://webservices.org/
[10] http://www.austria-tourism.at/
[11] http://www.symplicitynetworks.com/
[12] http://www.ez.no/developer/links/
[13] http://shop.ez.no/partner/list/
[14] http://developer.ez.no/article/view/358
[15] http://www.webmasterbase.com/article/228
[16] http://www.mysql.com
[17] http://www.postgresql.org
[18] http://www.webmasterbase.com/article/827
[19] http://shop.ez.no/trade/productview/25/2/
[20] http://www.ez.no/article/articlestatic/222/1/30/
[21] http://www.gnu.org/copyleft/gpl.html1
[22] http://publishdemo.ez.no/
[23] http://admin.publishdemo.ez.no/
[24] http://www.mygold.com/
[25] http://www.vbulletin.com/
[26] http://www.sitepointforums.com
[27] http://doc.ez.no/article/archive/2/
[28] http://publishdemo.ez.no/
[29] http://developer.ez.no
[30] http://www.vbulletin.com
[31] http://www.phpbb.com
[32] http://www.fusebox.org/
[33] http://bombusbee.com/
[34] http://bombusbee.com/downloads/files/FuseboxNewbieGuideV3PHP.pdf
[35] http://developer.ez.no/doc/view/index
[36] http://sdk.ez.no
[37] http://www.firepages.com.au
[38] http://www.webmasterbase.com/article/839
[39] http://www.phpmyadmin.org/
[40] http://www.sitepointforums.com
[41] http://developer.ez.no/developer/forums/
[42] http://developer.ez.no/filemanager/list/85/
[43] http://www.imagemagick.org
[44] ftp://ftp.nluug.nl/pub/ImageMagick/binaries/
[45] http://www.imagemagick.org/www/archives.html
[46] http://www.webmasterbase.com/article/846
[47] http://localhost/phpmyadmin
[48] http://www.webmasterbase.com/examples/ezpub/make_cache.zip
[49] http://www.webmasterbase.com/article/758
[50] http://www.sitepointforums.com
[51] http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html
[52] http://www.google.com/search?q=ssh+putty+tutorial
[53] http://doc.ez.no/article/archive/24/
[54] http://www.php.net/logos/php-med-trans-light.gif
[55] http://www.mysql.com/images/poweredbymysql-88.png
[56] http://doc.ez.no/article/archive/7/
[57] http://developer.ez.no/filemanager/list/8/
[58] http://doc.ez.no/article/articleview/3/1/7/
[59] http://doc.ez.no/article/archive/9/
[60] http://developer.ez.no
[61] http://developer.ez.no/bug/archive/15
[62] http://developer.ez.no/doc/view/index
[63] http://www.unixgeeks.org/security/newbie/unix/cron-1.html
[64] http://shop.ez.no/trade/productview/25/2/
[65] http://www.opera.com/download/
[66] http://www.webmasterbase.com/examples/ezpub/sampler.zip
[67] http://www.sitepointforums.com/
[68] http://www.sitepointforums.com
[69] http://www.webmasterbase.com/article/662
[70] http://www.webmasterbase.com/article/815
[71] http://doc.ez.no/article/articleview/204/1/54/
[72] http://www.sitepoint.com
[73] http://www.webmasterbase.com
[74] http://www.ecommercebase.com
[75] http://www.promotionbase.com
[76] http://www.servicebase.com
[77] http://www.sitepointforums.com
[78] http://publishdemo.ez.no/article/articleview/1/1/1/
[79] http://publishdemo.ez.no/article/articleview/1/1/2/
[80] http://publishdemo.ez.no/article/articleview/1/1/3/
[81] http://publishdemo.ez.no/article/articleview/1/1/4/
[82] http://developer.ez.no/article/articleview/142/1/19/
[83] http://developer.ez.no/doc/view/index
[84] http://doc.ez.no/article/archive/55/
[85] http://www.php.net/include
[86] http://www.php.net/eval
[87] http://www.webmasterbase.com/examples/ezpub/phppoint.zip
[88] http://shop.ez.no/trade/productview/25/2/
[89] http://www.webmasterbase.com/article/827
[90] http://developer.ez.no
[91] http://www.webmasterbase.com/article/662
[92] http://www.sitepointforums.com/showthread.php?s=&threadid=78687
[93] http://dmoz.org/
[94] http://www.mysql.com/doc/en/Fulltext_Search.html
[95] http://www.zez.org/article/articleview/83/
[96] http://www.martinfowler.com/isa/layers.html
[97] http://developer.ez.no/article/articleview/141/1/19/
[98] http://developer.ez.no/doc/view/ezdb
[99] http://www.php.net/manual/sv/keyword.extends.php
[100] http://www.php.net/manual/sv/keyword.extends.php
[101] http://www.zez.org/article/articleview/77/
[102] http://developer.ez.no/doc/view/index
[103] http://developer.ez.no/article/articlestatic/35/1/42/
[104] http://www.php.net/manual/sv/keyword.paamayim-nekudotayim.php
[105] http://developer.ez.no/doc/view/ezdb
[106] http://developer.ez.no/doc/view/ezdate
[107] http://developer.ez.no/doc/view/ezdatetime
[108] http://developer.ez.no/doc/view/ezlocale
[109] http://www.webmasterbase.com/article/402
[110] http://www.melonfire.com/community/columns/trog/article.php?id=118
[111] http://developer.ez.no/doc/
[112] http://developer.ez.no/doc/view/ezmysqldb
[113] http://developer.ez.no/doc/view/ezpostgresqldb
[114] http://developer.ez.no/doc/view/ezinformixdb
[115] http://developer.ez.no/doc/view/INIFile
[116] http://developer.ez.no/doc/view/eztemplate
[117] http://developer.ez.no/doc/view/eztexttool
[118] http://www.webkreator.com/php/techniques/php-and-templates.html
[119] http://www.webkreator.com/php/xcs/
[120] http://www.webmasterbase.com/examples/ezpub/ezfaq.zip
[121] http://www.sitepointforums.com
[122] http://www.hotscripts.com/PHP/Scripts_and_Programs/
[123] http://www.hotscripts.com/PHP/Scripts_and_Programs/Chat_Scripts/
[124] http://www.hotscripts.com/PHP/Scripts_and_Programs/Graphs_and_Charts/
[125] http://www.phpbb.com/phpBB/viewtopic.php?t=20202
[126] http://sdk.ez.no/
[127] http://www.sitepointforums.com/forumdisplay.php?s=&forumid=147
[128] http://www.sitepointforums.com/member.php?s=&action=getinfo&userid=13867
[129] http://www.students.cs.uu.nl/people/voostind/eclipse/
[130] http://www.sitepointforums.com