Tải bản đầy đủ (.pdf) (71 trang)

PHP Architect

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (2.42 MB, 71 trang )

OCTOBER 2004
VOLUME III - ISSUE 10
OCTOBER 2004
VOLUME III - ISSUE 10
www.phparch.com
The Magazine For PHP Professionals
TM
Certification Central

5 Editorial
Home turf advantage!
6 What’s New!
59 Security Corner
File Uploads
By Chris Shiflett
62 Tips & Tricks
By John W. Holmes
65 e x i t ( 0 ) ;
PHP and the Enterprise
by Andi Gutmans and Marco Tabini
9 Row, Row, Row Your Boat
ZIP on the Fly with the Streams API
by Chung W. Leong
17 Driving Multiple Databases Anywhere
by Geoffrey Mondoux
25 Roll Your Own Template
by Sérgio Machado
34 Exposing Web Application Data
Semantically Using RAP
(RDF API for PHP)
by Paul Cowles


40 Integrating PHP and OpenOffice
Using PHP to Dynamically Manipulate and
Convert OO documents
by Bård Farstad
47 PHP-GTK and the Glade GUI Builder
Building Client Applications with Style
by Tony Leake
3
October 2004

PHP Architect

www.phparch.com
TABLE OF CONTENTS
II NN DD EE XX
II NN DD EE XX
php|architect
Features
Departments
TM
*By signing this order form, you agree that we will charge your account in Canadian
dollars for the “CAD” amounts indicated above. Because of fluctuations in the
exchange rates, the actual amount charged in your currency on your credit card
statement may vary slightly.
Choose a Subscription type:
CCaannaaddaa//UUSSAA $$ 9977..9999 CCAADD (($$6699..9999 UUSS**))
IInntteerrnnaattiioonnaall AAiirr $$113399..9999 CCAADD (($$9999..9999 UUSS**))
CCoommbboo eeddiittiioonn aadddd--oonn $$ 1144..0000 CCAADD (($$1100..0000 UUSS))
((pprriinntt ++ PPDDFF eeddiittiioonn))
Your charge will appear under the name "Marco Tabini & Associates, Inc." Please

allow up to 4 to 6 weeks for your subscription to be established and your first issue
to be mailed to you.
*US Pricing is approximate and for illustration purposes only.
php|architect Subscription Dept.
P.O. Box 54526
1771 Avenue Road
Toronto, ON M5M 4N5
Canada
Name: ____________________________________________
Address: _________________________________________
City: _____________________________________________
State/Province: ____________________________________
ZIP/Postal Code: ___________________________________
Country: ___________________________________________
Payment type:
VISA Mastercard American Express
Credit Card Number:________________________________
Expiration Date: _____________________________________
E-mail address: ______________________________________
Phone Number: ____________________________________
Visit: for
more information or to subscribe online.
Signature: Date:
To subscribe via snail mail - please detach/copy this form, fill it
out and mail to the address above or fax to +1-416-630-5057
php|architect
The Magazine For PHP Professionals
YYoouu’’llll nneevveerr kknnooww wwhhaatt wwee’’llll ccoommee uupp wwiitthh nneexxtt
S
ubscribe to the print

edition and get a copy of
Lumen's LightBulb — a
$499 value
absolutely FREE

!
In collaboration with:
Upgrade to the
Print edition
and save!
For existing
subscribers
Login to your account
for more details.
EXCLUSIVE!
EXCLUSIVE!
† Lightbulb Lumination offer is valid until 12/31/2004 on the purchase of a 12-month print subscription.
October 2004

PHP Architect

www.phparch.com
EE DD II TT OO RR II AA LL RR AA NN TT SS
php|architect
Volume III - Issue 10
October, 2004
Publisher
Marco Tabini
Editorial Team
Arbi Arzoumani

Peter MacIntyre
Eddie Peloke
Graphics & Layout
Arbi Arzoumani
Managing Editor
Emanuela Corso
Director of Marketing
J. Scott Johnson

Account Executive
Shelley Johnston

Authors
Paul Cowles, Bård Farstad, John Holmes, Tony Leake,
Chung W. Leong, Geoffrey Mondoux, Sérgio Machado,
Chris Shiflett
php|architect (ISSN 1709-7169) is published twelve times a year by Marco Tabini &
Associates, Inc., P.O. Box 54526, 1771 Avenue Road, Toronto, ON M5M 4N5, Canada.
Although all possible care has been placed in assuring the accuracy of the contents of this
magazine, including all associated source code, listings and figures, the publisher assumes
no responsibilities with regards of use of the information contained herein or in all asso-
ciated material.
Contact Information:
General mailbox:
Editorial:
Subscriptions:
Sales & advertising:
Technical support:
Copyright © 2003-2004 Marco Tabini & Associates, Inc.
— All Rights Reserved

A
s you may know, last month we held our first on-
land conference right here in our hometown of
Toronto, Canada (I refer to it as an “on-land”
conference simply because our actual first conference
was php|cruise, which took place onboard a cruise
ship). Someone stopped me on the way to lunch one
day (note to prospective discussion-starters: never stop
Tabini on his way to anything related to food) and told
me that he bet I couldn’t wait for the end of the con-
ference to find out whether people thought it was a
success. Maybe it was the fact that I had not had break-
fast and it was one o’clock in the afternoon, but I sim-
ply answered, in a sort-of offhand way, that I knew that
the conference was a success and I didn’t need anyone
to tell me so. The other person looked at me in a funny
way—probably thinking that I was some sort of self-
centered egomaniac (which is probably not far from
the truth—I am a small business owner, after all)—and
walked away.
The truth, however, is that I actually meant what I
said. Once you’ve booked the speakers, reserved the
meeting space, fought with the hotel over every little
detail and made sure that nobody was going to be
asked to sleep in the broom closet, the best you can do
is to sit down and watch the event unfurl in front of
your eyes—and you’ll know immediately whether
you’ve done your job well: if you have, you’ll get some
sleep. I am happy to report that I managed eight hours
of sleep during every night of the conference (to be

fair, I actually overslept one day, but the main advan-
tage of holding a conference five minutes from your
home is that you can be there by 8AM even if you wake
up at 8:15).
Naturally, making the conference happen was a team
effort, and we couldn’t have done it, had we not had
the best speakers around and a surprisingly (in a good
way) attentive audience—the number of “defections”
that I noticed were very low. In the aftereffects of the
conference, I received lots of congratulatory e-mails
not only from the attendees, but from the speakers as
well (and from significant others, who were all-around
happy for the existence of the huge shopping centre
right next to the hotel). To all of you, thanks for mak-
ing php|w such an enjoyable experience—and see you
next year!
EDITORIAL
Home turf
advantage!
TM
October 2004

PHP Architect

www.phparch.com
6
NNEEWW SSTTUUFFFF
What’s New!
NN EE WW SS TT UU FF FF
PHP 5.0.2 released!

“The PHP Development Team is proud to
announce the immediate release of PHP
5.0.2 (
hhttttpp::////wwwwww..pphhpp..nneett//ddoowwnnllooaaddss..pphhpp##vv55
).
This is a maintenance release that in addition
to many non-critical bug fixes, addresses a
problem with GPC input processing. All Users
of PHP 5 are encouraged to upgrade to this release as soon as possible.”
Some changes include:
• Added
iinntteerrffaaccee__eexxiissttss(())
and made
ccllaassss__eexxiissttss(())
only
return true for real classes.
• Implemented periodic PCRE compiled regexp cache
cleanup, to avoid memory exhaustion.
• Added new boolean (fourth) parameter to
aarrrraayy__sslliiccee(())
that turns on the preservation of keys in the returned array.
Get the latest release from
pphhpp..nneett
.
eZ publish 3.5 alpha
(unstable)
eezz..nnoo
announces:
” eZ systems is proud to present this first
alpha release of eZ publish 3.5. The new

version has a lot of new features, but
most noticeably, the administration
frontend has been completely renewed.
Of course, all the bugfixes from the 3.4
branch are incorporated in eZ publish
3.5.”
Get more information from
eezz..nnoo
phpMyFAQ 1.4.2 RC2
pphhppmmyyffaaqq..ddee
announces the
release of phpMyFAQ 1.4.2 RC 2.
” This version includes tons of
bugfixes. Do not use this version
in production systems, but test
this version and report bugs!”
Get all the info at
pphhppmmyyffaaqq..ddee
Phase 1.0.5 default
Freshmeat.net announces:
”Phase is a very small text editor written in PHP. It uses HTML for the inter-
face, and is easily customized. It can access any directory that your plat-
form allows. Phase was designed for localhost access in mind (on your PC
running Apache with PHP), and thus it has no security built-in.”
Grab the latest download from Phase’s soureforge homepage:
hhttttpp::////wwwwww..vvssyy..ccaappee..ccoomm//~~jjeennnniinnggss//pphhaassee..hhttmmll
Cgiapp.class.php 1.4
(Default)
What is it?
“Cgiapp is a PHP framework for cre-

ating reusable web applications. It is
a port of the perl module
CGI::Application, with a few minor
additions. It uses Smarty as its
default template engine. It has been
tested with both PHP4 and PHP5.”
To view the online documenta-
tion or to download, check out the
project’s homepage at:
wweeiieerroopphhiinnnneeyy..nneett//mmaatttthheeww//ddoowwnnllooaadd??
mmooddee==vviieeww__ddoowwnnllooaadd&&iidd==1111
October 2004

PHP Architect

www.phparch.com
7
NNEEWW SSTTUUFFFF
Looking for a new PHP Extension? Check out some of the lastest offerings from PECL.
apd 1.0.1
hhttttpp::////ppeeccll..pphhpp..nneett//ppaacckkaaggee--iinnffoo..pphhpp??ppaacckkaaggee==aappdd
APD is a full-featured profiler/debugger that is loaded as a zend_extension. It aims to be an analog of C’s gprof
or Perl’s Devel::DProf.
WinBinder 0.23.080
hhttttpp::////ppeeccll..pphhpp..nneett//ppaacckkaaggee--iinnffoo..pphhpp??ppaacckkaaggee==WWiinnBBiinnddeerr
WinBinder is an extension that allows PHP programmers to build native Windows applications. It wraps a lim-
ited but important subset of the Windows API in a lightweight, easy-to-use library so that program creation is
quick and straightforward.
id3 0.2
hhttttpp::////ppeeccll..pphhpp..nneett//ppaacckkaaggee--iinnffoo..pphhpp??ppaacckkaaggee==iidd33

id3 enables to to retrieve and update information from ID3 tags in MP3 files. It supports version 1.0, 1.1 and
2.2+ (only reading text- and url-frames at the moment).
zeroconf 0.1.2
hhttttpp::////ppeeccll..pphhpp..nneett//ppaacckkaaggee--iinnffoo..pphhpp??ppaacckkaaggee==zzeerrooccoonnff
Provides an interface for browsing and publishing network services via ZeroConf using Apple's
Rendezvous/OpenTalk library. You can browse the network for specific services like database servers
(PostgreSQL, Sybase, InterBase), Apple File Sharing, web services via Apache's mod_rendezvous, etc. and dis-
cover the IP address and port for each found service.
MaxDB™ 7.5.00.18 now available for Linux/AMD x86-64
Mysql.com announces:
”With the new version 7.5.00.18, MaxDB is shipping with 64-bit support for
Linux/AMD platforms.
MaxDB™ has a long history in supporting 64-bit platforms since 1995, [when] the database structures were
adapted to 64-bit requirements to support the DEC OSF/1 platform. Subsequently, MaxDB was ported to the
major 64-bit architectures. Since 1997/1998 IBM AIX, HP-UX, Sun Solaris, and FSC Reliant have been sup-
ported.
With SAP DB 7.4, the next platform joined the club in 2001: Windows (NT) on the IA64 Itanium architecture.
Together with the rapid adoption of Linux, Linux on IA64 was supported in 2002 with SAP DB 7.4.03.
MaxDB for HP-UX/IA64 recently has been launched and there are further ongoing porting activities for MaxDB
on Linux/IBM PowerPC/64, which should become available with the MaxDB 7.6 alpha-Version in late 2004.
At the end of the current list of porting targets are MaxDB on Windows/AMD x86-64 and on Windows/Intel
x86-64.
Summing up, this shows that MaxDB development has always been aware of the 64-bit landscape and has
extensive experience with the challenges of these architectures for nearly 10 years.”
Get all the latest info from
mmyyssqqll..ccoomm
.
October 2004

PHP Architect


www.phparch.com
8
NNEEWW SSTTUUFFFF
Check out some of the hottest new releases from PEAR.
XML_Parser 1.2.1
hhttttpp::////ppeeaarr..pphhpp..nneett//ppaacckkaaggee//XXMMLL__PPaarrsseerr//
This is an XML parser based on PHP’s built-in xml extension. It supports two basic modes of operation: “func”
and “event”. In “func” mode, it will look for a function named after each element (xmltag_ELEMENT for start
tags and xmltag_ELEMENT_ for end tags), and in “event” mode it uses a set of generic callbacks.
Since version 1.2.0, there’s a new XML_Parser_Simple class that makes parsing of most XML documents eas-
ier, by automatically providing a stack for the elements. Furthermore it’s now possible to split the parser from
the handler object, so you do not have to extend XML_Parser anymore in order to parse a document with it.
I18Nv2 0.8.0
hhttttpp::////ppeeaarr..pphhpp..nneett//ppaacckkaaggee//II1188NNvv22//
This package provides basic support to localize your application, such as locale based formatting of dates,
numbers and currencies. In addition, it attempts to provide an OS independent way to
sseettllooccaallee(())
and aims
to provide language and country names translated into many languages.
LiveUser 0.13.1
hhttttpp::////ppeeaarr..pphhpp..nneett//ppaacckkaaggee//LLiivveeUUsseerr//
LiveUser is a set of classes for dealing with user authentication and permission management. Basically, there
are three main elements that make up this package:
• The LiveUser class
• The Auth containers
• The Perm containers
The LiveUser class takes care of the login process and can be configured to use a certain permission con-
tainer and one or more different auth containers. That means that you can have your users’ data scattered
amongst many data containers and have the LiveUser class try each defined container until the user is found.

For example, you can have all website users who can apply for a new account online on the webserver’s local
database. Also, you want to enable all your company’s employees to login to the site without the need to cre-
ate new accounts for all of them. To achieve that, a second container can be defined to be used by the
LiveUser class. You can also define a permission container of your choice that will manage the rights for each
user. Depending on the container, you can implement any kind of permission schemes for your application
while having one consistent API. Using different permission and auth containers, it’s easily possible to inte-
grate newly written applications with older ones that have their own ways of storing permissions and user
data. Just make a new container type and you’re ready to go! Currently available are containers using:
PEAR::DB, PEAR::MDB, PEAR::MDB2, PEAR::XML_Tree and PEAR::Auth.
HTTP_Request 1.2.3
hhttttpp::////ppeeaarr..pphhpp..nneett//ppaacckkaaggee//HHTTTTPP__RReeqquueesstt//
Supports
GGEETT//PPOOSSTT//HHEEAADD//TTRRAACCEE//PPUUTT//DDEELLEETTEE
, Basic authentication, Proxy, Proxy Authentication, SSL, file uploads
etc.
Services_Weather 1.3.1
hhttttpp::////ppeeaarr..pphhpp..nneett//ppaacckkaaggee//SSeerrvviicceess__WWeeaatthheerr//
Services_Weather searches for given locations and retrieves current weather data and, dependent on the used
service, also forecasts. Up to now, GlobalWeather from CapeScience, Weather XML from EJSE (US only), a
XOAP service from Weather.com and METAR/TAF from NOAA are supported. Further services will get includ-
ed, if they become available, have a usable API and are properly documented.
O
ne thing about PHP that I’ve always found inter-
esting is how much it resembles a real human
language. It is extremely flexible. It has a few
quirks and irregularities (that tend to drive beginners to
the language crazy). And it has an enormous “vocabu-
lary” that is constantly growing. The last time I
checked, PHP has somewhere in the neighborhood of
3,500 functions.

At times, browsing through the PHP manual can feel
like reading the dictionary. You will come across func-
tions you never knew existed, even if you are a experi-
enced coder, much like fluent English speakers would
find such words as ‘idempotent’ or ‘spoonerism’ in the
OED. Whereas learning these obscure words probably
won’t do much for your English prose, though, on
more than one occasion I have stumble across func-
tions that made major impacts in projects I worked on.
In this article, I will share with you one of these discov-
eries: the PHP Streams API.
Having a Life Offline
On our web site, we have a large collection of training
materials designed to help people who are trying to
learn foreign languages. These are highly interactive
HTML pages that make heavy use of graphics and
audio. The materials are contained in a database-driven
content management system written in PHP.
A feature that many of our users have requested is the
ability to download multiple lessons in a single ZIP file.
Because some of them often travel to places in the
world where there is no easy access to the Internet (or
where access to American web sites is blocked), they
wanted to have offline versions of our lessons that they
could burn onto a CD-ROM and take with them.
My initial thought on how to implement this was to
save the pages, along with the associated media files,
to a temporary folder, then spawn an external program
to compress them. I realized quickly, though, that this
was unworkable. A single package could contain hun-

dreds, sometimes thousands of files. Writing them all to
disk would simply take too long. Either the web brows-
er would drop the connection for lack of network activ-
ity, or our user would run out of patience and click can-
cel. Any reasonable solution therefore must involve cre-
ating the ZIP file using PHP.
ZIP on the Fly
ZIP is a relatively straightforward file format. At
Zend.com, you can find an excellent article by John
Coggeshall that describes how to create one from with-
in a script; I will, therefore, refrain from going into
much detail here. A ZIP file consists of two major parts:
the data segments and the central directory. A data
segment is the compressed contents of a file sand-
wiched between a header and a trailer. For each file in
the archive there is a data segment. The central direc-
October 2004

PHP Architect

www.phparch.com
9
FF EE AA TT UU RR EE
Row, Row, Row Your Boat
ZIP on the Fly with the Streams API
by Chung W. Leong
PHP: 4.3.0+
OS: N/A
Other software: N/A
Code Directory: streams

REQUIREMENTS
In human languages, the meaning of words tends to
change over time. The word “porcelain” traces its root to
“porcus”—Latin for pig. Functions in PHP also have a way
of acquiring capabilities beyond what their names sug-
gest. Once upon a time, the “f” in [fopen()] had stood for
“file.” Nowadays, [fopen()] can open many other things.
tory is simply the headers of all the files, tacked on at
the end for easy access.
Figure 1 shows the basic structure of a ZIP file. As you
can see, building a ZIP file is just a matter of stacking
the various parts in the correct order. In Listing 1, you
will find the code for
FFllyyZZIIPP
, a ZIP generating class. It
takes a list of “filenames” (I will shortly explain the quo-
tation marks), places them into a ZIP file, and sends it
to the browser. The class is a rewrite from John
Coggeshall’s original sample code. The main difference
is that, instead of building the entire archive in memo-
ry first, the
FFllyyZZIIPP
class outputs the data segment of
each file as soon as its contents are fetched and com-
pressed. This reduces the memory requirement of the
operation, which is important for downloads that can
be over a hundred megabytes in size. It also means that
the download dialog pops up much more quickly on
the user’s computer.
The

AAddddFFiillee(())
method of the
FFllyyZZIIPP
class accepts
two parameters,
$$ddeesstt__ppaatthh
and
$$ssrrcc__ppaatthh
. The former
is the name of the file within the archive. The latter is
the source “filename.” The reason I put quotation
marks around the word is that the parameter doesn’t
necessarily have to be a filesystem path. It can be a URL
as well, as the class makes use of
ffiillee__ggeett__ccoonntteennttss(())
,
October 2004

PHP Architect

www.phparch.com
10
FFEEAATTUURREE
ZIP on the Fly with the Streams API
1 <?
2
3 class FlyZip {
4 var $zipname;
5 var $urls;
6

7 var $src_unc_lens;
8 var $src_c_lens;
9 var $src_crcs;
10 var $cd_num;
11 var $ds_len;
12 var $cd_len;
13 var $comp_level;
14
15 function FlyZip($filename, $level = 9) {
16 $this->zipname = $filename;
17 // don’t use compression if we can’t do it
18 $this->comp_level =
19 function_exists(‘gzdeflate’) ? $level : 0;
20 $this->urls = array();
21 $this->src_unc_lens = array();
22 $this->src_c_lens = array();
23 $this->src_crcs = array();
24 }
25
26 function AddFile($dest_path, $src_path) {
27 $this->urls[$src_path] = $dest_path;
28 }
29
30 function AddComment($comment) {
31 $this->comment .= $comment;
32 }
33
34 function EchoToClient() {
35 $disp = “attachment; filename=$this->zipname”;
36 header(“Content-type: application/octet-stream”);

37 header(“Content-disposition: $disp”);
38 header(“Cache-control: “);
39 error_reporting(0);
40 $this->EchoDataSegments();
41 $this->EchoCDEntries();
42 $this->EchoZipSummary();
43 }
44
45 function EchoDataSegments() {
46 foreach($this->urls as $src_path => $dest_path) {
47 $unc_data = file_get_contents($src_path);
48 if($this->comp_level > 0)
49 $c_data = gzdeflate($unc_data,
50 $this->comp_level);
51 else
52 $c_data = $data;
53 $this->src_unc_lens[$src_path] =
54 $unc_len = strlen($unc_data);
55 $this->src_c_lens[$src_path] =
56 $c_len = strlen($c_data);
57 $this->src_crcs[$src_path] =
58 $crc = crc32($unc_data);
59
60 $dest_path_len = strlen($dest_path);
61
62 $s = “\x50\x4b\x03\x04”;
63 $s .= “\x14\x00”;
64 $s .= “\x00\x00”;
65 $s .= “\x08\x00”;
66 $s .= “\x00\x00\x00\x00”;

67
68 $s .= pack(“V”, $crc);
69 $s .= pack(“V”, $c_len);
70 $s .= pack(“V”, $unc_len);
71 $s .= pack(“v”, $dest_path_len);
72 $s .= pack(“v”, 0 );
73 $s .= $dest_path;
74
75 $t = pack(“V”, $crc);
76 $t .= pack(“V”, $c_len);
77 $t .= pack(“V”, $unc_len);
78
79 echo $s;
80 echo $c_data;
81 echo $t;
82
83 $this->ds_len
84 += (42 + $dest_path_len + $c_len);
85 }
86 }
87
88 function EchoCDEntries() {
89 $ds_offset = 0;
90 foreach($this->urls as $src_path => $dest_path) {
91 $unc_len = $this->src_unc_lens[$src_path];
92 $c_len = $this->src_c_lens[$src_path];
Listing 1
93 $crc = $this->src_crcs[$src_path];
94 $dest_path_len = strlen($dest_path);
95

96 $s = “\x50\x4b\x01\x02”;
97 $s .= “\x00\x00”;
98 $s .= “\x14\x00”;
99 $s .= “\x00\x00”;
100 $s .= “\x08\x00”;
101 $s .= “\x00\x00\x00\x00”;
102 $s .= pack(“V”, $crc);
103 $s .= pack(“V”, $c_len);
104 $s .= pack(“V”, $unc_len);
105 $s .= pack(“v”, $dest_path_len);
106 $s .= pack(“v”, 0 );
107 $s .= pack(“v”, 0 );
108 $s .= pack(“v”, 0 );
109 $s .= pack(“v”, 0 );
110 $s .= pack(“V”, 32 );
111
112 $s .= pack(“V”, $ds_offset);
113 $s .= $dest_path;
114
115 echo $s;
116
117 $ds_offset += (42 + $dest_path_len + $c_len);
118 $this->cd_len += (46 + $dest_path_len);
119 $this->cd_num++;
120 }
121 }
122
123 function EchoZipSummary() {
124 $s .= “\x50\x4b\x05\x06\x00\x00\x00\x00”;
125 $s .= pack(“v”, $this->cd_num);

126 $s .= pack(“v”, $this->cd_num);
127 $s .= pack(“V”, $this->cd_len);
128 $s .= pack(“V”, $this->ds_len);
129 $comment_len = strlen($this->comment);
130 $s .= pack(“v”, $comment_len);
131
132 echo $s;
133 echo $this->comment;
134
135 return (22 + $comment_len);
136 }
137 }
138
139 ?>
Listing 1:
Continued...
which can retrieve data from the web and other
sources. In our case, in order to obtain the dynamic
HTML pages from our content management system, I
would simply make HTTP requests to the local web
server.
Putting it All Together
In Listing 2, you will find a simplified version of the
download script I employed in our project. In the pre-
ceding web page, the user has checked off a number of
lessons that she/he wishes to download. The user selec-
tions arrive via HTTP POST as an array of lesson identi-
fiers. The script loops through this array, inserting the
files for each lesson into the
$$ffiillee__lliisstt

array. It then
adds the supporting images, Javascript, and CSS files to
the list. The keys of
$$ffiillee__lliisstt
contain the source file-
names, while the values contain the destination file-
names. After the script has finished building the list, it
creates a FlyZIP object and calls its
AAddddFFiillee(())
method
for each of the files. When that’s done, it invokes the
EEcchhooTTooCClliieenntt(())
method to send the ZIP file to the
browser. For the purpose of simplifying the debugging
process, the script will simply copy everything into a
folder if the variable
$$DDEEBBUUGG__PPAATTHH
is defined.
In Listing 3, you will find the code for the functions
used in the download script. The
AAddddLLeessssoonn(())
function,
with help from
AAddddUURRLL(())
, adds the URLs that constitute
a given lesson to the file list. Since URLs are usually not
valid filesystem names (‘?’ is not allowed),
AAddddUURRLL(())
calls
OOfffflliinnee(())

to obtain a suitable destination file-
name. This function simply takes the script name
(minus the
..pphhpp
extension), appends to it all the GET
October 2004

PHP Architect

www.phparch.com
11
FFEEAATTUURREE
ZIP on the Fly with the Streams API
1 <?
2
3 require_once(‘flyzip.php’);
4 require_once(‘functions.php’);
5
6 $file_list = array();
7
8 /* add pages that the user has selected */
9 foreach($_POST[‘lession_ids’] as $lession_id) {
10 AddLessonPages($file_list, $lession_id);
11 }
12
13 /* add support files to file list */
14 $root = $_SERVER[‘DOCUMENT_ROOT’];
15 AddFolder($file_list, “$root/images”, “images”);
16 AddFolder($file_list, “$root/css”, “css”);
17 AddFolder($file_list, “$root/javascript”, “javascript”);

18
19 if(!isset($DEBUG_PATH)) {
20 $zip = new FlyZip(‘lession.zip’);
21 foreach($file_list as $src => $dest) {
22 $zip->AddFile($dest, $src);
23 }
24 $zip->EchoToClient();
25 }
26 else {
27 foreach($file_list as $src => $dest) {
28 copy($src, “$DEBUG_PATH/$dest”);
29 }
30 }
31
32 ?>
Listing 2
Figure 1
1 <?
2
3 /* make a filename from a URL */
4 function Offline($url) {
5 $parts = parse_url($url);
6
7 /* name of the script (without extension) */
8 $name = substr(basename($parts[‘path’]), 0, -4);
9
10 /* append get variables to the name */
11 $get = array();
12 parse_str($parts[‘query’], $get);
13 ksort($get);

14 foreach($get as $var => $val) {
15 $name .= “_$var$val”;
16 }
17 return “$name.html”;
18 }
19
20 /* add a URL to the list */
21 function AddURL(&$file_list, $url) {
22 $file_list[$url] = Offline($url);
23 }
24
25 /* add dynamic pages for a lession to the list */
26 function AddLesson(&$list, $l) {
27 $root = “http://localhost”;
28 AddURL($list, “$root/lesson.php?lession=$l”);
29 AddURL($list, “$root/intro.php?lession=$l”);
30 AddURL($list, “$root/glossary.php?lession=$l”);
31
32 for($i = 1; $i <= 5; $i++) {
33 $url = “$root/chapter.php?lesson=$l&chapter=$i”;
34 AddURL($list, $url);
35
36 for($j = 1; $j <= 4; $j++) {
37 $url = “$root/section.php?lesson=$l&chapter=$i&sec-
tion=$j”;
38 AddURL($list, $url);
39 }
40 }
41 }
42

43 function AddFolder(&$list, $src_path, $dest_path) {
44 $dir = opendir($src_path);
45 while($file = readdir($dir)) {
46 if($file != “.” && $file != “..”) {
47 if(is_dir(“$src_path/$file”)) {
48 AddFolder($list, “$src_path/$file”,
49 “$dest_path/$file”);
50 }
51 else {
52 $list[“$src_path/$file”]
53 = “$dest_path/$file”;
54 }
55 }
56 }
57 closedir($dir);
58 }
59
60 ?>
Listing 3
variables, and attaches
..hhttmmll
at the end.
While the download script managed to create ZIP
files containing the correct files, there is one major
snag: all the hyperlinks are broken. Anchor tags in the
original online pages that looked like
<<aa
hhrreeff==““//iinnttrroo..pphhpp??lleessssoonn==11””>>
need to be converted to
<<aa hhrreeff==““iinnttrroo__lleessssoonn11..hhttmmll””>>

in the offline versions.
There were a number of ways to fix this. For example,
I could have inserted a conditional statement into every
link. Depending on the value of
HHTTTTPP__UUSSEERR__AAGGEENNTT
, it
would echo either the online URL or the offline file-
name. Alternatively, I could have all the links point to
static HTML files, then use Apache Rewrite rules to redi-
rect them to the correct PHP scripts. Both of these solu-
tions involve making a lot of changes to the existing
code, something I would rather avoid.
A more attractive solution would be to replace the
links after a page has been retrieved. With Regular
Expression, this is far from difficult. The tricky part is
how to invoke the code that does the replacement.
Since the page retrieval occurs within the
FFllyyZZIIPP
class
(more precisely, in the
EEcchhooDDaattaaSSeeggmmeennttss(())
method),
the replacement has to somehow take place there. I
could add a third parameter,
$$ccaallllbbaacckk__ffuunncc
, to the
AAddddFFiillee(())
method, but that seems too much like a
hack. A much more elegant way, as I will show, is to use
PHP’s Streams API.

The Streams API
The Streams API was introduced in PHP 4.3 “as a way
of generalizing file, network, data compression, and
other operations which share a common set of func-
tions and uses.” To put it simply, it lets you treat any
blob of data as though it were a file. Whereas in ver-
sions of PHP prior to 4.3, “URL” is essentially synony-
mous with “Internet address,” the acronym now truly
lives up to what it stands for—Uniform Resource
Locator. You can use one to identify any blob of data,
located anywhere. The data could be a file sitting on a
web server, in the case of an HTTP stream. It could be
a global variable in the current script. It could even be
a text string returned by a function.
The code that provides “streamable” access to a par-
ticular type of resource is called a stream wrapper. PHP
provides built-in stream wrappers for the HTTP, HTTPS,
October 2004

PHP Architect

www.phparch.com
12
FFEEAATTUURREE
ZIP on the Fly with the Streams API
1 <?php
2
3 class VariableStream {
4 var $position;
5 var $varname;

6
7 function stream_open($path, $mode, $options, &$opened_path)
8 {
9 $url = parse_url($path);
10 $this->varname = $url[“host”];
11 $this->position = 0;
12
13 return true;
14 }
15
16 function stream_read($count)
17 {
18 $ret = substr($GLOBALS[$this->varname], $this->posi-
tion, $count);
19 $this->position += strlen($ret);
20 return $ret;
21 }
22
23 function stream_write($data)
24 {
25 $left = substr($GLOBALS[$this->varname], 0, $this-
>position);
26 $right = substr($GLOBALS[$this->varname], $this->posi-
tion + strlen($data));
27 $GLOBALS[$this->varname] = $left . $data . $right;
28 $this->position += strlen($data);
29 return strlen($data);
30 }
31
32 function stream_tell()

33 {
34 return $this->position;
35 }
36
37 function stream_eof()
38 {
39 return $this->position >= strlen($GLOBALS[$this->var-
name]);
40 }
41
42 function stream_seek($offset, $whence)
43 {
44 switch($whence) {
45 case SEEK_SET:
46 if ($offset < strlen($GLOBALS[$this->varname])
&& $offset >= 0) {
47 $this->position = $offset;
Listing 4
48 return true;
49 } else {
50 return false;
51 }
52 break;
53
54 case SEEK_CUR:
55 if ($offset >= 0) {
56 $this->position += $offset;
57 return true;
58 } else {
59 return false;

60 }
61 break;
62
63 case SEEK_END:
64 if (strlen($GLOBALS[$this->varname]) + $offset
>= 0) {
65 $this->position = strlen($GLOBALS[$this-
>varname]) + $offset;
66 return true;
67 } else {
68 return false;
69 }
70 break;
71
72 default:
73 return false;
74 }
75 }
76 }
77
78 stream_wrapper_register(“var”, “VariableStream”)
79 or die(“Failed to register protocol”);
80
81 $myvar = “”;
82
83 $fp = fopen(“var://myvar”, “r+”);
84
85 fwrite($fp, “line1\n”);
86 fwrite($fp, “line2\n”);
87 fwrite($fp, “line3\n”);

88
89 rewind($fp);
90 while(!feof($fp)) {
91 echo fgets($fp);
92 }
93 fclose($fp);
94 var_dump($myvar);
95
96 ?>
Listing 4:
Continued...
FTP, and FTPS protocols, as well as for accessing stan-
dard streams (stdin, stdout, and stderr) and contents
inside compressed files. It also lets you make your own
stream wrappers and register them to a custom proto-
col. In the PHP manual, for example, you will find the
VVaarriiaabblleeSSttrreeaamm
wrapper, registered to the protocol
vvaarr::////
(see listing 4). This stream wrapper lets you
access a global variable as though it were a file.
A stream wrapper is a class that implements a defined
set of methods. When PHP opens a stream, it creates an
instance of the stream wrapper class and invokes its
ssttrreeaamm__ooppeenn
method. To read from the stream, it
invokes
ssttrreeaamm__rreeaadd
with the number of bytes desired
as a parameter. To write to one, it invokes

ssttrreeaamm__wwrriittee
with the data to be written. When a script calls
ffsseeeekk(())
or
fftteellll(())
on a stream, PHP satisfies the request by
invoking
ssttrreeaamm__sseeeekk(())
or
ssttrreeaamm__tteellll(())
. The stream
wrapper, in turn, must then update or return the posi-
tion of the current pointer within the stream.
When PHP needs to know whether it has reached the
end of a stream, it invokes the
ssttrreeaamm__eeooff(())
method.
When it needs the statistics of a file—its size, for exam-
ple—it invokes
ssttrreeaamm__ssttaatt(())
. When time comes to
close the stream, PHP invokes
ssttrreeaamm__cclloossee(())
if the
method is defined. If it is not, PHP assumes that no
clean up is necessary and nothing happens. Besides
responding to explicit calls to
ffcclloossee(())
, PHP will also
close a stream when the resource pointer returned by

ffooppeenn(())
goes out of scope. For example:
function Test() { $f = fopen(“var://Hello/”); }
Test();
echo “Hello”;
Had
VVaarriiaabblleeSSttrreeaamm::::ssttrreeaamm__cclloossee(())
been declared,
PHP would invoke it before it prints
HHeelllloo
, because
$$ff
goes out of scope when
TTeesstt(())
returns.
To connect a stream wrapper to a protocol, you use
ssttrreeaamm__wwrraappppeerr__rreeggiisstteerr(())
(or
ssttrreeaamm__rreeggiisstteerr__wwrraapp--
ppeerr(())
in PHP 4.3.0 and 4.3.1). The function takes two
parameters, the protocol name and the name of the
stream wrapper class. Also known as “schema,” the
protocol is the part of a URL that comes before the
colon. It basically denotes the method for accessing a
particular resource. When registering your own wrap-
per, you must connect it to a unique protocol. You can-
not override the built-in wrappers (HTTP, FTP, etc) or
those registered earlier and, once a wrapper is regis-
tered, it cannot be unregistered. The name of the pro-

tocol must be longer than one letter (for otherwise
Windows machines would get confused). It can contain
letters, numbers, dashes, plus signs, and periods (but
not underscores). Contrary to the recommendation in
RFC 1738, protocol names in PHP are case sensitive.
October 2004

PHP Architect

www.phparch.com
13
FFEEAATTUURREE
ZIP on the Fly with the Streams API
1 <?php
2
3 class FunctionStream {
4 var $position;
5 var $data;
6
7 function stream_open($path, $mode, $options, &$opened_path)
8 {
9 $url = parse_url($path);
10 $this->data = call_user_func($url[‘host’],
$url[‘path’], $url[‘query’]);
11 $this->position = 0;
12
13 return true;
14 }
15
16 function stream_read($count)

17 {
18 $ret = substr($this->data, $this->position, $count);
19 $this->position += strlen($ret);
20 return $ret;
21 }
22
23 function stream_write($data)
24 {
25 $left = substr($this->data, 0, $this->position);
26 $right = substr($this->data, $this->position +
strlen($data));
27 $this->data = $left . $data . $right;
28 $this->position += strlen($data);
29 return strlen($data);
30 }
31
32 function stream_tell()
33 {
34 return $this->position;
35 }
36
37 function stream_eof()
38 {
39 return $this->position >= strlen($this->data);
40 }
41
42 function stream_seek($offset, $whence)
43 {
44 switch($whence) {
45 case SEEK_SET:

46 if ($offset < strlen($this->data) && $offset >=
0) {
47 $this->position = $offset;
48 return true;
49 } else {
50 return false;
51 }
52 break;
53
54 case SEEK_CUR:
55 if ($offset >= 0) {
56 $this->position += $offset;
57 return true;
58 } else {
59 return false;
60 }
61 break;
62
63 case SEEK_END:
64 if (strlen($this->data) + $offset >= 0) {
65 $this->position = strlen($this->data) +
$offset;
66 return true;
67 } else {
68 return false;
69 }
70 break;
71
72 default:
73 return false;

74 }
75 }
76
77 function stream_stat() {
78 return array( ‘size’ => strlen($this->data) );
79 }
80 }
81
82 stream_wrapper_register(“func”, “FunctionStream”)
83 or die(“Failed to register protocol”);
84
85 ?>
Listing 5
A Function As a File
Now, back to the problem at hand. I need a stream that
does the following: retrieve a page from our content
management system through HTTP, then perform a
search and replace on all the hyperlinks. Instead of
making a stream wrapper for this very specific task, I
have created a general purpose one that gets its data
from a function. In Listing 5, you will find the code for
the
FFuunnccttiioonnSSttrreeaamm
wrapper class. The class largely
resembles the
VVaarriiaabblleeSSttrreeaamm
class from the PHP man-
ual. In fact, I created it simply by performing a search-
and-replace operation, changing occurrences of
$$__GGLLOOBBAALLSS[[$$tthhiiss-->>vvaarrnnaammee]]

to
$$tthhiiss-->>ddaattaa
. I did imple-
ment
ssttrreeaamm__ssttaatt(())
, which was missing from
VVaarriiaabblleeSSttrreeaamm
.
ffiillee__ggeett__ccoonntteennttss(())
invokes this
method and would throw a warning if it is missing.
I have registered the
FFuunnccttiioonnSSttrreeaamm
class to the
“func” protocol. A URL to a function stream has the for-
mat “
ffuunncc::////<<ffuunnccttiioonn__nnaammee>>//<<ppaarraammss11>>??<<ppaarraamm22>>
”.
When FunctionStream’s
ssttrreeaamm__ooppeenn(())
method is
invoked, it parses the URL with
ppaarrssee__uurrll(())
. Using the
host part as the function name, it calls the function,
passing the path and the query part as parameters. The
return value is then saved in
$$tthhiiss-->>ddaattaa
.
Listing 6 shows the updated version of the

AAddddLLeessssoonn(())
function. The only difference is the URL
root: a URL that looked like
hhttttpp::////llooccaallhhoosstt//lleessssoonn..pphhpp??lleessssoonn==11
before has now
become
ffuunncc::////GGeettPPaaggee//lleessssoonn..pphhpp??lleessssoonn==11
.
When the FlyZIP class retrieves the contents of this
URL via
ffiillee__ggeett__ccoonntteennttss(())
, the stream wrapper calls
GGeettPPaaggee(())
. The function retrieves the page from
llooccaall--
hhoosstt
, then changes all the
hhrreeff
attributes with the help
of
pprreegg__mmaattcchh__ccaallllbbaacckk(())
. For those not completely flu-
ent in regular expressions, the pattern here matches
any string situated between
hhrreeff==””
and
““
. The
((??<<==<<ppaatttteerrnn>>))
and

((??==<<ppaatttteerrnn>>))
expressions are
look-behind and look-ahead assertions. The patterns
contained in assertions are not considered part of the
match and would not be replaced. The question mark
after the plus sign denotes an “ungreedy pattern”—
that is to say, the regex engine will not try to find the
longest possible match (for more information on regu-
lar expressions, you may want to check out George
Schlossnagle’s excellent series on the subject that
appeared in the March, April and May 2004 issues of
php|a). When there is a match, PHP calls the callback
function, which returns the offline filename as the
replacement string.
GGeettPPaaggee(())
returns the resulting
text, which is then fed into the stream.
And… that’s it. The FlyZIP class gets the new, correct-
ed contents and happily puts it into the ZIP file as it did
before. One of the key advantages of the using streams
is that there’s a very clear separation between the data
consumer and the data source. It allowed me to substi-
tute one data source for another without changing any
code “downstream.” I might swap it yet again in the
future, replacing the page generation mechanism with
something radically different. As long as the data
remains the same, I know the script will continue to
function correctly.
Other Uses For Function Streams
The

FFuunnccttiioonnSSttrreeaamm
wrapper is very versatile. It lets you
create a new stream-based resource type simply by
declaring a function. Here are some of its possible uses:
• Using
ggeettiimmaaggeessiizzee(())
,
ffggeettccssvv(())
, and
ppaarrssee__iinnii__ffiillee(())
on data stored in a data-
base.
• Including PHP code stored in a database.
You might want to do this in order to keep
sensitive code from prying eyes on a shared
server. Obviously, the database username
and password have to be secured in the first
place.
• Including dynamically generated PHP code.
One scenario here might be a situation in
which there is a namespace collision
between two third party components. You
can resolve the conflict by changing the
name in question on the fly within a func-
tion stream. Similarly, this approach can
come in handy when you need to change
calls to a PHP function en masse to your
own debug version. An intriguing possibility
October 2004


PHP Architect

www.phparch.com
14
FFEEAATTUURREE
ZIP on the Fly with the Streams API
1 <?
2
3 /* add dynamic pages for a lession to the list */
4 function AddLesson(&$list, $l) {
5 $root = “func://GetPage”;
6 AddURL($list, “$root/lesson.php?lession=$l”);
7 AddURL($list, “$root/intro.php?lession=$l”);
8 AddURL($list, “$root/glossary.php?lession=$l”);
9
10 for($i = 1; $i <= 5; $i++) {
11 AddURL($list, “$root/chapter.php?lesson=$l&chapter=$i”);
12
13 for($j = 1; $j <= 4; $j++) {
14 AddURL($list, “$root/section.php?lesson=$l&chap-
ter=$i&section=$j”);
15 }
16 }
17 }
18
19 /* retrieve a page from localhost, fix up its hyperlinks */
20 function GetPage($path, $query) {
21 $url = “http://localhost$path?$query”;
22 $contents = file_get_contents($url);
23 $contents = preg_replace_callback(‘/(?<=href=”).+?(?=”)/i’,

24 ‘url_callback’, $data);
25 return $contents;
26 }
27
28 function url_callback($m) {
29 return Offline($m[0]);
30 }
31
32 ?>
Listing 6
is to use this mechanism for on-the-fly trans-
lation: a PHP5 class could be adjusted so
that it works in PHP4, for example. Or a
ColdFusion template could be translated and
be included into a PHP page with a simple
iinncclluuddee((““ccffmm::////..//ffoorrmm..ccffmm””))
statement.
Other Stream Functions
Aside from user-defined stream wrappers, the Streams
API provides a number of other useful functions. These
have enhanced, in particular, PHP’s remote retrieval
capabilities. A complete description of every function is
somewhat beyond the scope of this article, so I will only
examine a couple of them—hopefully, it will at least
give you a sense of what’s possible..
ssttrreeaamm__ccoonntteexxtt__ccrreeaattee(())
lets you modify the HTTP
request that gets sent by PHP. The function accepts an
array of options. You can set the request method, the
headers, as well as the request content. First you create

the context, then you pass it to
ffooppeenn(())
as the fourth
parameter. See Listing 7 for examples on how to
include a cookie in the request and how to perform a
POST operation. You may also want to read up on Ilia
Alshanetsky’s Out of Context article, which appeared in
the May 2004 issue of php|a.
ssttrreeaamm__ggeett__mmeettaa__ddaattaa(())
gives you access to informa-
tion contained in the HTTP response. Simply pass it the
resource pointer returned by
ffooppeenn(())
and this function
will return an array containing the HTTP response head-
ers, which may include the last modified date, the serv-
er type, the expiration date, and perhaps even a cook-
ie.
ssttrreeaamm__sseelleecctt(())
, in conjunction with
ssttrreeaamm__sseett__bblloocckkiinngg(())
, lets you retrieve multiple pages
simultaneously. After opening each of the URLs with
ffooppeenn(())
, you use
ssttrreeaamm__sseett__bblloocckkiinngg(())
to place the
streams into non-blocking mode. Then you pass the
resource pointers to
ssttrreeaamm__sseelleecctt(())

. The function will
block until data is available from at least one of the
streams.
Limitations and Pitfalls
The Streams API is a relatively new addition to PHP. As
most of you probably know from experience, whenev-
er you use a new and flashy feature, you run the risk of
the new and flashy not working properly. As of version
4.3.6, the Streams API is fairly solid. Can the same be
said for the earlier versions? I know, for instance, that
ffooppeenn(())
can’t perform a POST in 4.3.3, even when
given the proper context. Does it work in 4.3.4 and
4.3.5? I don’t know, as there’s no mention in either the
manual or the changelog. Basically, your mileage might
vary. Be especially careful if your development environ-
ment has a version of PHP newer than your production
environment. Always test before you invest too much
time and effort into anything new!
Another problem you may encounter is third-party
components not accepting URLs in place of filenames.
For example, the
aaddddAAttttaacchhmmeenntt
method of the
MMaaiill__MMiimmee
class in the PEAR library will not accept any-
thing except a bona fide filesystem path, even though
it is perfectly reasonable to attach dynamically generat-
ed content to e-mails. Of course, the code was proba-
bly developed prior to PHP 4.3.

As more developers become aware of the Streams
API, hopefully more of them will take into account that,
in PHP, a “file” might not actually be a file anymore.
October 2004

PHP Architect

www.phparch.com
15
FFEEAATTUURREE
ZIP on the Fly with the Streams API
1 <?
2
3 /* passing along a cookie */
4
5 $sessid = ‘5cc3bbdf6683062d7db930eb0c2a5f49’;
6
7 $opts = array(
8 ‘http’ => array(
9 ‘method’ => “GET”,
10 ‘header’ =>
11 “Cookie: PHPSESSID=$sessid\r\n”
12 )
13 );
14
15 $context = stream_context_create($opts);
16 $f = fopen(“http://localhost/info.php”, “rb”,
17 false, $context);
18 fpassthru($f);
19

20
21 /* performing a POST */
22
23 $content = “tazmienna=1&tamtazmiena=2”;
24 $content_type = “application/x-www-form-urlencoded”;
25 $content_len = strlen($content);
26
27 $opts = array(
28 ‘http’ => array(
29 ‘method’ => “POST”,
30 ‘content’=> $content,
31 ‘header’ =>
32 “Content-type: $content_type\r\n” .
33 “Content-length: $content_len\r\n”
34 )
35 );
36
37 $context = stream_context_create($opts);
38 $f = fopen(“http://localhost/info.php”, “rb”, false,
39 $context);
40 fpassthru($f);
41
42 ?>
Listing 7
About the Author ?>
To Discuss this article:
/>Chung Wing Leong is a senior programmer at the National Foreign
Language Center at the University of Maryland. His interest is in lan-
guages, both of the human and computer varieties. When not coding in
PHP, he watches corny Polish soap operas to pass his time.


T
rying to manage data from disparate database
systems can be painful at best. The configura-
tions are different, the SQL dialects are different
and the table structures are definitely different. What if
there was a way to remove these barriers? What if you
could access these different systems in a semi-transpar-
ent way, and use the result sets as if there was only one
database system back there? That would be good,
right? Enter Database_Universal.
Let’s dig in.
Universal Database Communication
As we will be discussing the means of how information
in different (but similar) databases can be shared, we
will need some sort of abstraction layer for accessing
them. For this task, we will rely heavily on the PEAR::DB
class (
hhttttpp::////ppeeaarr..pphhpp..nneett
). PEAR::DB is a wonderful
connection class and I recommend readers review the
documentation and consider using it in future projects.
But PEAR won’t be enough. We need to be able to
work with these different databases seamlessly. The
Database_Universal class (see Listing 1) will handle our
database connections, regardless of connection type. It
consists of just five methods: two connection methods,
two configuration methods, and one query method.
We’ll explore those now.
The

sseettCCoonnnneeccttiioonn(())
configuration method takes two
arrays. The first array is used to create the DSN string;
the second array is used to set options about the con-
nection. This is the same information that PEAR::DB
uses, so for more detail, see the PEAR::DB documenta-
tion for connecting, at:
hhttttpp::////ppeeaarr..pphhpp..nneett//mmaannuuaall//eenn//ppaacckkaaggee..ddaattaabbaassee..ddbb..iinn
ttrroo--ccoonnnneecctt..pphhpp
The
sseettFFeettcchhMMooddee(())
configuration method allows you
to change the type of record sets returned to you, by
passing in one of the PEAR::DB result set options. The
DDBB__FFEETTCCHHMMOODDEE__AASSSSOOCC
option, for example, returns result
rows in an associative array keyed by field name,
whereas the
DDBB__FFEETTCCHHMMOODDEE__OOBBJJEECCTT
option returns result
rows as objects. I have used
DDBB__FFEETTCCHHMMOODDEE__AASSSSOOCC
as the
default. Your preference may differ.
The
ccoonnnneecctt(())
and
ddiissccoonnnneecctt(())
methods are self
explanatory, and simply connect and disconnect from

the currently configured database. The
qquueerryy(())
method takes care of executing all queries, and returns
a standard PEAR::DB_Result object for us to work with.
We’ll see more about this method later on.
Now that we have our wrapper in place, let’s get
started.
Setting the stage
Suppose we have two different databases running
under different database server engines on different
October 2004

PHP Architect

www.phparch.com
17
FF EE AA TT UU RR EE
Driving Multiple
Databases Anywhere
by Geoffrey Mondoux
PHP: 4.2
OS: Any
Other: PEAR::DB and a database software
Code Directory: databases
REQUIREMENTS
As the communication era continues to grow, the need
and desire to link many different systems and environ-
ments together increases. This article is designed to show
you how to link different types of databases safely, and
how to establish meaningful connections to read, write,

and understand the wealth of information held in them.
servers, and the information in each database needs to
be combined into one interface.
As an example, let’s say the owner of Gecko’s, a
beach and body shop, recently acquired another store,
Maniac’s Skate Shop. Each store will be keeping all of
their existing systems since they will not be merging
companies, but the new owner of Maniac’s wants to
be able to view all inventory in both stores at once.
Suppose Maniac’s system happens to be Postgres-driv-
en, but Gecko’s is MySQL-driven. Needless to say, the
table structures are not the same, either.
We have been given the responsibility of developing
the unified interface, which will be used daily by the
owner to assess purchases, mark downs, and to bundle
products into packages.
Let’s see how we are going to implement it.
The adapter file
Before we can do anything, we will need to view the
table structures for both stores. Table 1 shows the lay-
out of Gecko’s and Maniac’s product tables. The
columns of these tables will play an important role in
the crafting of the SQL statements and the retrieval of
the data.
In order to unify data like this, we need to have a
method of mapping fields between the two databases.
This is where the adapter file comes in (see Listing 2).
This file defines an SQL query to get product data from
each database, and then specifies a mapping between
“virtual” fields and actual fields in each database.

The adapter file is the key to providing flexible and
immersible database collaboration.
The connection settings
Since we are only connecting to two databases, we’ll
store the server connection settings in an array (see
Listing 3). In larger systems, I’d recommend keeping a
database table with this information in it and passing
the required information to an array or object when
required.
Each server setting is broken up into four different
array sets: “connection”, “options”, “universal_name”
and “universal_type.” Only the “connection” and “uni-
versal_type” have to be filled in. The “options” setting,
as suggested by its name, is optional. The
“universal_name” setting simply provides a friendly
string that can be used to output which server is being
queried.
Putting it together
With the three files we have so far—the
Universal_Database class, the server connection set-
October 2004

PHP Architect

www.phparch.com
18
FFEEAATTUURREE
Driving multiple databases anywhere
1 <?
2

3 // Include PEAR::DB once
4 require_once “DB.php”;
5
6 class Database_Universal {
7
8 // Database Handler
9 var $db;
10 // Connnection Preferences
11 var $conn_prefs;
12 // Connection Options
13 var $conn_options;
14 // Last error presented
15 var $lasterror;
16 // How do we return the record sets
17 var $fetch_mode = DB_FETCHMODE_ASSOC;
18
19 // Sets preferences and options for use with connect()
20 function setConnection($conn_prefs,$options) {
21 $this->conn_prefs = $conn_prefs;
22 $this->conn_options = $options;
23 }
24
25 // Sets how to return the record sets
26 function setFetchMode($mode) {
27 $this->fetch_mode = $mode;
28 }
29
30 // Makes a connections to the database
31 function connect() {
32 $this->db = DB::connect($this->conn_prefs,$this-

>conn_options);
33 if(DB::isError($this->db)) {
34 $this->lasterror = $this->db->getMessage();
35 return false;
36 } else {
37 return true;
38 }
39 }
40
41 // Performs the query to the database
42 function query($sql,$data = null) {
43 // If DB object is set then perform query
44 if (is_object($this->db)) {
45 // Set fetch mode of record set
46 $this->db->setFetchMode($this->fetch_mode);
47 if ($data == null) {
48 $results =& $this->db->query($sql);
49 } else {
50 $results =& $this->db->query($sql,$data);
51 }
52 if (DB::isError($results)) {
53 $this->lasterror = $results->getMessage();
54 return false;
55 } else {
56 return $results;
57 }
58 } else {
59 $this->lasterror = “No PEAR::DB object created. Use
connect() first.”;
60 return false;

61 }
62 }
63
64 // Disconnects from the database currently connected on.
65 function disconnect() {
66 return $this->db->disconnect();
67 }
68 }
69
70 ?>
Listing 1
1 <?
2
3 $sql[‘Gecko’][‘get_products’] = “SELECT * FROM inventory”;
4 $sql[‘Gecko’][‘vars’][‘product_id’] = ‘product_id’;
5 $sql[‘Gecko’][‘vars’][‘product_name’] = ‘product_name’;
6 $sql[‘Gecko’][‘vars’][‘product_price’] = ‘price’;
7 $sql[‘Gecko’][‘vars’][‘product_quantity’] = ‘quantity’;
8 $sql[‘Gecko’][‘vars’][‘product_description’] =
‘product_description’;
9
10 $sql[‘Maniac’][‘get_products’] = “SELECT * FROM stock”;
11 $sql[‘Maniac’][‘vars’][‘product_id’] = ‘id’;
12 $sql[‘Maniac’][‘vars’][‘product_name’] = ‘stock_name’;
13 $sql[‘Maniac’][‘vars’][‘product_price’] = ‘cost’;
14 $sql[‘Maniac’][‘vars’][‘product_quantity’] = ‘available’;
15 $sql[‘Maniac’][‘vars’][‘product_description’] = ‘info’;
16
17 ?>
Listing 2

tings, and the adapter file—we can now link Maniac’s
Skate Shop and Gecko’s Beach and Body Shop togeth-
er for the owner.
Listing 4 shows our integration script. Pretty simple,
actually. We first create a Database_Universal object,
and then cycle through the servers we want to connect
to, running the relevant query from our adapter file.
Note how we index the result fields with the informa-
tion specified in the adapter file. You can see how this
method allows us to correlate information from dis-
parate sources.
Taking it further
The owner feels that it is essential to be able to update
inventory for each store on one screen, so in our sec-
ond example we will be inserting data into the databas-
es we are connecting to.
First, let’s look at the second parameter to the
qquueerryy(())
method of our Database_Universal class. This
$$ddaattaa
parameter provides data to be inserted into the
SQL statements before being processed. This function-
ality is actually implemented in the PEAR::DB
qquueerryy(())
method—we just pass it through. It actually works a lit-
tle like the
pprriinnttff(())
function, in that our SQL statement
can contain placeholders, and the
$$ddaattaa

parameter can
contain data to substitute into those placeholders. The
placeholder in PEAR::DB is a “?”.
For more information about the value replacement
functionality of PEAR::DB (also known as prepare/exe-
cute), please see the PEAR::DB documentation for
querying, at :
hhttttpp::////ppeeaarr..pphhpp..nneett//mmaannuuaall//eenn//ppaacckkaaggee..ddaattaabbaassee..ddbb
..iinnttrroo--qquueerryy..pphhpp
Let’s now revisit the adapter file, because in order to
create the new interface, it will definitely need more
work.
NOTE: Remember to safeguard your data from
breaking your SQL statement and making it
unusable by using functions such as
aaddddssllaasshheess(())
. If you are inserting these values
through a form on the web, then
magic_quotes_gpc can be beneficial, as well. It
will automatically add slashes to all
$$__GGEETT
,
$$__PPOOSSTT
, and
$$__CCOOOOKKIIEE
data that needs to be
escaped.
October 2004

PHP Architect


www.phparch.com
19
FFEEAATTUURREE
Driving multiple databases anywhere
1 <?
2
3 $servers[1][‘connection’][‘phptype’] = ‘mysql’;
4 $servers[1][‘connection’][‘hostspec’] = ‘geckosurfboards.com’;
5 $servers[1][‘connection’][‘username’] = ‘gecko’;
6 $servers[1][‘connection’][‘password’] = ‘password’;
7 $servers[1][‘connection’][‘database’] = ‘surf_store’;
8 $servers[1][‘universal_type’] = ‘Gecko’;
9 $servers[1][‘universal_name’] = ‘Gecko\’s Surf and Body Shop’;
10
11 $servers[2][‘connection’][‘phptype’] = ‘pgsql’;
12 $servers[2][‘connection’][‘hostspec’] = ‘streetmania.com’;
13 $servers[2][‘connection’][‘username’] = ‘streeter’;
14 $servers[2][‘connection’][‘password’] = ‘maniac’;
15 $servers[2][‘connection’][‘database’] = ‘mania_store’;
16 $servers[2][‘universal_type’] = ‘Maniac’;
17 $servers[2][‘universal_name’] = ‘Maniac\’s Skate Shop’;
18
19 ?>
Listing 3
1 <?
2
3 include “Database_Universal.php”;
4 include “ServerList.php”;
5 include “Adapter.php”;

6
7 // Create DB object
8 $database = new Database_Universal();
9
10 // Cycle through servers in Server List
11 foreach ($servers as $server) {
12 $database->setConnection($server[‘connection’],$server[‘options’]);
13 // If connected perform query
14 if ($database->connect()) {
15 echo “<br>Inventory Location: “ . $server[‘universal_name’];
16 $results = $database->query($sql[$server[‘universal_type’]][‘get_products’]);
17 // Retrieve record sets and output them
18 while ($record =& $results->fetchRow()) {
19 echo “<br><br>”;
20 echo “<br>Name: “ . $record[$sql[$server[‘universal_type’]][‘vars’][‘product_name’]];
21 echo “<br>Quantity: “ . $record[$sql[$server[‘universal_type’]][‘vars’][‘product_quantity’]];
22 echo “<br>Price: “ . $record[$sql[$server[‘universal_type’]][‘vars’][‘product_price’]];
23 echo “<br>Info: “ . $record[$sql[$server[‘universal_type’]][‘vars’][‘product_description’]];
24 }
25 // Free record set and disconnect
26 $results->free();
27 $database->disconnect();
28
29 } else {
30 echo “<br>Could not connect to database : “ . $database->lasterror;
31 }
32 }
33
34
35 ?>

Listing 4
Revisiting the adapter
Listing 5 contains our expanded adapter file. It has two
new elements in each SQL array for communicating
with Maniac’s and Gecko’s databases: “add_product”
and “change_product_quantity”. We will use
“add_product” in this example and “change_prod-
uct_quantity” later on. As mentioned, PEAR::DB’s pre-
pare/execute functionality will
be used to insert the values into
the SQL statement.
One thing of interest to note
in this example is the difference
in how MySQL and Postgres
handle sequencing (or auto
incrementing). MySQL will
automaticallyl update the
record to the next ID number in
the sequence. However,
Postgres does not do this and
you must specifically identify the next ID value for the
record you are inserting through a sequence index we
called id_ref. This is one of the many minute differences
in database systems. However, the adapter file is able to
cope with it and maintain a universal query method.
Putting it together... again
Listing 6 shows the new integration script. We can now
use a form to add inventory to either store.
With this example, you can see how easy it is to add
your new products to any type of database, regardless

of database engine or structure. We have also specified
the fields in our SQL INSERT commands in the same
order, which allows us to
structure the
$$ddaattaa
array (for
PEAR::DB prepare/execute
functionality) the same for
both databases. This allows
the creation of our array data
to be standardized and any
database should then accept
the
$$ddaattaa
array in the SQL
statements.
We have also protected the
formation of the SQL state-
ment by
aaddddssllaasshheess(())
. If, however, you are using a PHP
configuration with
mmaaggiicc__qquuootteess__ggppcc
turned on, then
you can remove them from the example or check if it’s
enabled and change the code accordingly.
For determining which server to add the product to,
we use a
<<SSEELLEECCTT>>
form element. The

<<OOPPTTIIOONN>>
’s in that
October 2004

PHP Architect

www.phparch.com
20
FFEEAATTUURREE
Driving multiple databases anywhere
1 <?
2
3 $sql[‘Gecko’][‘get_products’] = “SELECT * FROM inventory”;
4 $sql[‘Gecko’][‘add_product’] = “INSERT INTO inventory (product_name,price,quantity,product_description) VALUES (?,?,?,?)”;
5 $sql[‘Gecko’][‘change_product_quantity’] = “UPDATE inventory SET quantity=? WHERE product_id=?”;
6 $sql[‘Gecko’][‘vars’][‘product_id’] = ‘product_id’;
7 $sql[‘Gecko’][‘vars’][‘product_name’] = ‘product_name’;
8 $sql[‘Gecko’][‘vars’][‘product_price’] = ‘price’;
9 $sql[‘Gecko’][‘vars’][‘product_quantity’] = ‘quantity’;
10 $sql[‘Gecko’][‘vars’][‘product_description’] = ‘product_description’;
11
12 $sql[‘Maniac’][‘get_products’] = “SELECT * FROM stock”;
13 $sql[‘Maniac’][‘add_product’] = “INSERT INTO stock (id,stock_name,cost,available,info) VALUES (nextval(‘id_ref’),?,?,?,?)”;
14 $sql[‘Maniac’][‘change_product_quantity’] = “UPDATE stock SET available=? WHERE id=?”;
15 $sql[‘Maniac’][‘vars’][‘product_id’] = ‘id’;
16 $sql[‘Maniac’][‘vars’][‘product_name’] = ‘stock_name’;
17 $sql[‘Maniac’][‘vars’][‘product_price’] = ‘cost’;
18 $sql[‘Maniac’][‘vars’][‘product_quantity’] = ‘available’;
19 $sql[‘Maniac’][‘vars’][‘product_description’] = ‘info’;
20

21 ?>
Listing 5
Gecko's:
TABLE inventory
product_id | product_name | price | quantity | product_description
1 Blue Waves Board 150.00 3 Calm and serene
2 Tropic Ocean Board 250.00 4 Head to the palm trees
3 Insane Torrent Board 350.00 1 For the rough riders
Maniac's:
TABLE stock
id | stock_name | cost | available | info
1 Zero 1999 Deck 150.00 3 Street
2 Big 8 2001 Deck 250.00 4 Pipe
3 Major Grind Desk 350.00 1 Multi-purpose
Figure 1
“ What if you could
access these different
systems in a semi-trans-
parent way?”
element are built from the
$$sseerrvveerrss
array, which allows
quick identification of the correct server.
A little more functionality, please.
Now that we have observed how the adapter file is
used more extensively, and how the
Database_Universal’s
qquueerryy(())
function helps us with the
addition of fill-in variables, we are ready to build an

example application which will adjust the inventory
quantities in both stores and update the store’s quanti-
ty content instantly. Our shrewd owner wishes more
functionality.
Listing 7 shows our final integration script. Notice
that in order to update/add/edit information in the eas-
iest way, we must keep track of the server ID for each
product using a hidden input field in the HTML form.
This will allow our script to easily select the proper serv-
er on which to perform the query.
Where to go from here
The tools used in this article have many other uses. For
instance, I have used them for a caching system, per-
taining to the status of data in over 60 databases. It
could also be used for linking any type of correlating
data into one interface as we did in the examples
above. It could be used to clone records from one data-
base to another (like replication).
Data mining is becoming an important aspect of the
Internet. With XML-based protocols such as RSS, SOAP,
and XML-RPC, the need to provide information in a
unified form grows. Database_Universal could be used
for the creation of RSS or XML files that contain infor-
mation from many databases. XML compliments
Database_Universal very nicely, and it also allows others
to use the data in future scripts by simply querying your
October 2004

PHP Architect


www.phparch.com
21
FFEEAATTUURREE
Driving multiple databases anywhere
1 <?
2
3 include “Database_Universal.php”;
4 include “ServerList.php”;
5 include “Adapter.php”;
6
7 // Create DB object
8 $database = new Database_Universal();
9
10 // Check for actions
11 switch ($_REQUEST[‘action’]) {
12
13 // If non, then default action, add product GUI
14 default:
15 echo “<BR>Add New Product”;
16 echo “<FORM METHOD=’POST’>”;
17 echo “<INPUT TYPE=’hidden’ NAME=’action’ VALUE=’add’>”;
18
19 echo “<SELECT NAME=’serverid’>”;
20 foreach ($servers as $serverid=>$server) {
21 echo “<OPTION VALUE=’” . $serverid . “‘>” . $server[‘universal_name’] . “</OPTION>”;
22 }
23 echo “</SELECT><BR>”;
24
25 echo “<BR>Product Name: <BR><INPUT TYPE=’text’ NAME=’name’>”;
26 echo “<BR>Product Ammount: <BR><INPUT TYPE=’text’ NAME=’price’>”;

27 echo “<BR>Product Quantity: <BR><INPUT TYPE=’text’ NAME=’quantity’>”;
28 echo “<BR>Product Description: <BR><TEXTAREA NAME=’description’></TEXTAREA>”;
29 echo “<BR><INPUT TYPE=’submit’ VALUE=’Add Product’>”;
30 echo “</FORM>”;
31
32 break;
33
34 // Action add. Adds product to seleted store
35 case “add”:
36 $server = $servers[$_REQUEST[‘serverid’]];
37 $database->setConnection($server[‘connection’],$server[‘options’]);
38 if ($database->connect()) {
39 $item[] = addslashes($_REQUEST[‘name’]);
40 $item[] = addslashes($_REQUEST[‘price’]);
41 $item[] = addslashes($_REQUEST[‘quantity’]);
42 $item[] = addslashes($_REQUEST[‘description’]);
43
44 $results = $database->query($sql[$server[‘universal_type’]][‘add_product’],$item);
45
46 if ($results == false) {
47 echo “Bad SQL statement or data inserted”;
48 }
49
50 echo “<BR>Item : “ . $_REQUEST[‘name’] . “ was added to “ . $server[‘connection’][‘hostname’];
51 echo “<BR><BR><A HREF=’add_product.php’>Back</A>”;
52 // Free record set and disconnect
53 $database->disconnect();
54 } else {
55 echo “<br>Could not connect to database : “ . $database->lasterror;
56 }

57 }
58
59 ?>
Listing 6
script to bring all the data together. Without these
complimentary links this data might require a spidering
script, which can require a lot of upkeep.
You can even use this method for populating several
databases with records you may have received from a
spider or RSS feed reader. This is incredibly important if
you are in a position of obtaining content to populate
several types of databases.
Another possible use of these tools is to create graphs
of live data from content-related databases. This could
be useful if you want to track supply/demand,
highs/lows or averages through many different data-
bases and provide the data in a graphical format that
people can understand and read.
Also, if you wish to tie in non-database connections
to Database_Universal (spider tools for instance), you
just have to create a new type name (along the lines of
pgsql or mysql) and check for it in the
ccoonnnneecctt(())
and
qquueerryy(())
methods. Your custom code can then connect
and query as desired and return the record set back.
Help with SQL
There are so many different database standards: ODBC,
MSSQL, MySQL, Postgres, SQLite, and so on.

October 2004

PHP Architect

www.phparch.com
22
FFEEAATTUURREE
Driving multiple databases anywhere
1 <?
2
3 include “Database_Universal.php”;
4 include “ServerList.php”;
5 include “Adapter.php”;
6
7 $database = new Database_Universal();
8
9 switch ($_REQUEST[‘action’]) {
10
11 default:
12 echo “<BR>Change Product Quanity”;
13 echo “<FORM METHOD=’POST’>”;
14 echo “<INPUT TYPE=’hidden’ NAME=’action’ VALUE=’change’>”;
15
16 // Cycle through servers in Server List
17 foreach ($servers as $serverid=>$server) {
18 $database->setConnection($server[‘connection’],$server[‘options’]);
19 // If connected perform query
20 if ($database->connect()) {
21 $results = $database->query($sql[$server[‘universal_type’]][‘get_products’]);
22 // Retrieve record sets and output them

23 while ($record =& $results->fetchRow()) {
24 echo “<FORM METHOD=’POST’>”;
25 echo “<INPUT NAME=’action’ TYPE=’hidden’ VALUE=’change’>”;
26 echo “<BR>” . $record[$sql[$server[‘universal_type’]][‘vars’][‘product_name’]];
27 echo “<BR>” . $record[$sql[$server[‘universal_type’]][‘vars’][‘product_description’]];
28 echo “<BR>Price : “ . $record[$sql[$server[‘universal_type’]][‘vars’][‘product_price’]];
29 echo “<BR>Current Quantity :” . $record[$sql[$server[‘universal_type’]][‘vars’][‘product_quantity’]];
30 echo “<BR>New Quantity”;
31 echo “<INPUT TYPE=’hidden’ NAME=’serverid’ VALUE=’$serverid’>”;
32 echo “<INPUT TYPE=’hidden’ NAME=’productname’ VALUE=’” . $record[$sql[$server[‘universal_type’]][‘vars’][‘prod-
uct_name’]] . “‘>”;
33 echo “<INPUT TYPE=’hidden’ NAME=’productid’ VALUE=’” . $record[$sql[$server[‘universal_type’]][‘vars’][‘prod-
uct_id’]] . “‘>”;
34 echo “<INPUT TYPE=’text’ SIZE=3 NAME=’new_quantity’>”;
35 echo “<BR><INPUT TYPE=’submit’ VALUE=’Update quantity’>”;
36 echo “</FORM>”;
37 }
38 } else {
39 echo “<br>Could not connect to database : “ . $database->last_error;
40 }
41 }
42
43 break;
44
45 case “change”:
46 // Switch to correct server
47 $server = $servers[$_REQUEST[‘serverid’]];
48 // Connect
49 $database->setConnection($server[‘connection’],$server[‘options’]);
50 if ($database->connect()) {

51 // Form data array to be used in PEAR::DB query
52 $data[] = $_REQUEST[‘new_quantity’];
53 $data[] = $_REQUEST[‘productid’];
54
55 $results =& $database->query($sql[$server[‘universal_type’]][‘change_product_quantity’],$data);
56
57 echo “<BR>Item : “ . $_REQUEST[‘productname’] . “ quantity was updated on “ . $server[‘connection’][‘hostspec’];
58 echo “<BR><BR><A HREF=’change_product_quantity.php’>Back</A>”;
59 $database->disconnect();
60
61 } else {
62 echo “<br>Could not connect to database : “ . $database->lasterror;
63 }
64 }
65
66 ?>
Listing 7
Unfortunately, this means that each one has a slightly
different way of connecting or querying. Fortunately,
PEAR::DB takes some of that pain away by abstracting
out the connection and querying operations. You just
need to be able to form the proper SQL statement to
achieve your goals. Once you have that, you can place
it in the adapter file, assign it a friendly name in the
array, and use it without worrying about it again.
Here are some websites to assist you in the creation
of your own multi-database system:
Conclusion
Data communication and collaboration is important. If
you are ever working on an project with a lot of ele-

ments to talk to, molding the data to your needs is
essential. Not all programmers like the structure of a
particular database table or its column names, but with
the tools described in this article you can map them to
whatever you want and use these new names in your
scripting. The adapter takes the aspects that are ugly or
that you don’t like, and transforms them into some-
thing that is easy for you to work with. It is extremely
flexible and allows you to use database operations that
may differ in syntax structure, while still achieving the
same effect.
I hope the information provided here was entertain-
ing and informative. The task of collecting information
from many databases is an important aspect of my cur-
rent job, and I have no doubt that this need is shared
among many. I have used the techniques presented
here with over 10 different database structures and it
has done me well and saved me much time.
October 2004

PHP Architect

www.phparch.com
23
FFEEAATTUURREE
Driving multiple databases anywhere
About the Author ?>
To Discuss this article:
/>With over eight years of programming and project experience, Geoffrey
Mondoux is a Project Manager and Developer at Hostworks Incorporated

(hostworks.ca) as well the owner and operator of SacredCore (sacred-
core.net).
• SQL Database Reference Manual
hhttttpp::////wwwwww..ssqqll..oorrgg//ssqqll--ddaattaabbaassee//mmyyssqqll//
• Comparison of Oracle, MySQL, and
Postgres DBMS
hhttttpp::////ddeett--ddbbaalliiccee..iiff..ppww..eedduu..ppll//ddeett--
ddbbaalliiccee//ttttrraacczzyykk//ddbb__ccoommppaarree//ddbb__ccoommppaarree..hhttmmll
• Gentle Introduction to SQL
hhttttpp::////ssqqllzzoooo..nneett//
Can’t stop thinking about PHP?
Write for us!
Visit us at
/>

Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Tải bản đầy đủ ngay
×