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

MySQL /PHP Database Applications Second Edition phần 6 docx

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 (553.05 KB, 81 trang )

;
drop table if exists story_versions;
create table story_versions
(
story_id integer not null
, modify_dt timestamp
, modify_by varchar(20) not null
, stage_id integer not null
, publish_dt date null
, headline varchar(255) null
, subtitle varchar(255) null
, byline_prefix varchar(20) null
, summary text null
, body text null
, primary key (story_id, modify_dt)
, foreign key (story_id) references stories (story_id) on delete
cascade
)
type=InnoDB
;
drop table if exists user_seq;
create table user_seq
(
id int not null auto_increment
, primary key (id)
)
type=InnoDB
;
drop table if exists user_stage_map;
create table user_stage_map
(


user_id integer not null
, stage_id integer not null
, primary key (user_id,stage_id)
, index (stage_id,user_id)
, foreign key (user_id) references users (user_id) on delete cascade
, foreign key (stage_id) references stages (stage_id) on delete
cascade
)
type=InnoDB
;
drop table if exists users;
create table users
360 Part IV: Not So Simple Applications
(
user_id integer not null auto_increment
, username varchar(20) not null
, password varchar(16) not null
, name varchar(50) not null
, email varchar(255) null
, primary key (user_id)
, unique (username)
)
type=InnoDB
;
Code Overview
At this point, we assume that you are getting comfortable with the way the appli-
cations in this book have been constructed. Even with the simple safe_
mysql_query()
function in the guestbook example, you saw the usefulness of hav-
ing a standard way of working with PHP’s native MySQL routines. The built-in rou-

tines will let you do what you need to do, no question. But in the course of using
them, you may find that you’re writing the same kind of code multiple times, a sure
signal that some higher-level functions are called for. Also, should you ever want
to port your code to a different DBMS for some crazy reason, like because you’re
being paid to, going through your code and converting those MySQL-specific func-
tions to some other system can be a big pain.
If you’ve ever done any work with Perl, you may be familiar with the DBI
library. It provides a standard interface to multiple database systems. You may have
also used Comprehensive Perl Archive Network (CPAN), the big code library where
you can find all sorts of previously invented wheels. The same kinds of benefits are
available with PHP, thanks to the good people who have built— and are building
even now — PEAR.
To quote from the PEAR Manifest ( />introduction.php
): “PEAR is short for ‘PHP Extension and Application
Repository’ and is pronounced just like the fruit.” PEAR has several facets. It’s a
library of PHP code that solves many common problems encountered by Web
developers. It’s also a means of packaging and distributing code, to make it simpler
to install code from that library, and to encourage people to share their own code.
The best place to find out more is at the Web site: . Here
you’ll find the code, the main PEAR documentation, mailing lists, and other useful
information.
PEAR is very much a moving target, undergoing constant improvement and
extension, and it has the rough edges that brings. So by way of introduction, we’ll
focus on one of the most widely used — and most completely documented — classes,
the DB class. It’s one of the core PEAR classes that are automatically distributed
and installed as part of PHP (at least, as of this writing). Like Perl’s DBI class, DB
Chapter 11: Content-Management System 361
provides a standard interface to multiple database systems. It makes it easy to do
the kinds of things you’ll want to do to get data out of a database (like building an
associative array from the results of a query) and to put data into a database (like

handling those pesky quote marks).
As you work through, less and less of the code should require explanation.
Thus, our descriptions of the code will deal only with those parts that are really
new or tricky.
Here, most of the newer looking code will come from assigning the privileges
discussed in the previous section. The application sends queries that you haven’t
used before.
Code Breakdown
Once again, the code in this application will make heavy use of the functions in the
/functions folder. A lot of the code presented here will make calls to those functions.
The great thing about functions is that they become part of your library of code
that you can re-use for other purposes.
Functions from /dsn
The PEAR DB library takes a connection string that will look somewhat familiar if
you’ve used Perl’s DBI class, and that is easy to figure out in any case. It typically
looks something like this:
phptype://username:password@hostspec/database
where hostspec might be replaced with the port number and name of the local-
host. The routine that accepts connections also accepts an associative array with all
the parts spelled out as key/value pairs, so that’s what we’ll use.
Rather than store usernames and passwords in the code of the example, as we
have done up until now, we’ve moved the connection information for the database
to a function in a separate directory, outside the document root of the Apache
server. This provides a small amount of extra security — though if you’re on a
shared server, this information is still vulnerable. But at least moving it out of the
Web-server document root means that no one can download the file as a Web page.
In our setup, the /dsn directory is parallel to the /htdocs directory. In there is one
file, db_dsnlist.php, defining one function, db_dsnlist():
function db_dsnlist()
{

static $_defaults = array(
‘application’ => ‘default’
);
static $_simple = array(
362 Part IV: Not So Simple Applications
‘application’, ‘username’, ‘password’, ‘database’
);
$p = func_get_args();
$p = parse_arguments($p, $_simple, $_defaults);
static $dsnlist = array(
‘default’ => array(
‘phptype’ => ‘mysql’
, ‘dbsyntax’ => NULL
, ‘username’ => ‘nobody’
, ‘password’ => ‘ydobon’
, ‘protocol’ => ‘tcp’
, ‘hostspec’ => ‘localhost’
, ‘port’ => NULL
, ‘socket’ => NULL
, ‘database’ => ‘test’
)
, ‘oldcatalog’ => array(
‘phptype’ => ‘mysql’
, ‘dbsyntax’ => NULL
, ‘username’ => ‘nobody’
, ‘password’ => ‘ydobon’
, ‘protocol’ => ‘tcp’
, ‘hostspec’ => ‘localhost’
, ‘port’ => NULL
, ‘socket’ => NULL

, ‘database’ => ‘oldcatalog’
)
, ‘catalog’ => array(
‘phptype’ => ‘mysql’
, ‘dbsyntax’ => NULL
, ‘username’ => ‘nobody’
, ‘password’ => ‘ydobon’
, ‘protocol’ => ‘tcp’
, ‘hostspec’ => ‘localhost’
, ‘port’ => NULL
, ‘socket’ => NULL
, ‘database’ => ‘catalog’
)
, ‘discussion’ => array(
‘phptype’ => ‘mysql’
, ‘dbsyntax’ => NULL
, ‘username’ => ‘nobody’
, ‘password’ => ‘ydobon’
, ‘protocol’ => ‘tcp’
Chapter 11: Content-Management System 363
, ‘hostspec’ => ‘localhost’
, ‘port’ => NULL
, ‘socket’ => NULL
, ‘database’ => ‘discussion’
)
, ‘netsloth’ => array(
‘phptype’ => ‘mysql’
, ‘dbsyntax’ => NULL
, ‘username’ => ‘nobody’
, ‘password’ => ‘ydobon’

, ‘protocol’ => ‘tcp’
, ‘hostspec’ => ‘localhost’
, ‘port’ => NULL
, ‘socket’ => NULL
, ‘database’ => ‘netsloth’
)
, ‘content’ => array(
‘phptype’ => ‘mysql’
, ‘dbsyntax’ => NULL
, ‘username’ => NULL
, ‘password’ => NULL
, ‘protocol’ => ‘tcp’
, ‘hostspec’ => ‘localhost’
, ‘port’ => NULL
, ‘socket’ => NULL
, ‘database’ => ‘netsloth’
)
, ‘admin’ => array(
‘phptype’ => ‘mysql’
, ‘dbsyntax’ => NULL
, ‘username’ => ‘admin’
, ‘password’ => ‘supersecret’
, ‘protocol’ => ‘tcp’
, ‘hostspec’ => ‘localhost’
, ‘port’ => NULL
, ‘socket’ => NULL
, ‘database’ => ‘netsloth’
)
, ‘tracking’ => array(
‘phptype’ => ‘mysql’

, ‘dbsyntax’ => NULL
, ‘username’ => ‘nobody’
, ‘password’ => ‘ydobon’
, ‘protocol’ => ‘tcp’
, ‘hostspec’ => ‘localhost’
364 Part IV: Not So Simple Applications
, ‘port’ => NULL
, ‘socket’ => NULL
, ‘database’ => ‘tracking’
)
);
// remove NULL values to not override entries from dsn
$p = array_diff($p, array_filter($p,’is_null’));
if (isset($dsnlist[$p[‘application’]]))
{
$dsn = array_merge($dsnlist[$p[‘application’]],$p);
}
else
{
$dsn = array_merge($dsnlist[‘default’],$p);
}
return $dsn;
}
Typically, this function is called with just the application name as a parameter,
and will return the entry for that application from the static array of connection
parameters. But we can pass in other values as well, which are merged into the
returned array.
Functions from /book/functions/database
The functions of the PEAR DB library are powerful enough that in most circum-
stances you can use them either directly in the code of the Web pages or in functions

specific to an example. In a few instances you do the same work in all the examples,
though, and these general functions are stored in the /databases directory of the
general /functions directory.
db_connect()
The db_connect() function is similar to the mysql_connect() function we used in
previous examples. It creates a persistent connection to the MySQL server, getting
connection parameters from the
db_dsnlist() function described earlier.
function db_connect()
{
static $_connections = array();
static $_defaults = array(
‘application’ => NULL
, ‘database’ => NULL
, ‘username’ => NULL
, ‘db_error_level’ => E_USER_ERROR
, ‘db_error_handler’ => ‘db_error_handler’
Chapter 11: Content-Management System 365
, ‘options’ => array(
‘debug’ => 4
, ‘persistent’ => TRUE
, ‘autofree’ => TRUE
)
);
static $_simple = array(‘application’,’username’,’password’);
$dc = count($_connections);
$p = func_get_args();
if (empty($p))
{
if ($dc)

{
$dbh = array_pop(array_values($_connections));
if ($dbh === NULL) { user_error(‘Last connection is
NULL.’, E_USER_ERROR); exit; }
return $dbh;
}
user_error(‘No existing database connection found.’,
E_USER_ERROR);
exit;
}
$p = parse_arguments($p, $_simple, $_defaults);
if (empty($p[‘application’]))
{
$p[‘application’] = $p[‘database’];
if (!empty($p[‘username’]))
{
$p[‘application’] .= ‘:’.$p[‘username’];
}
}
$dbh = array_key_value($_connections,$p[‘application’],NULL);
if ($dbh !== NULL)
{
return $dbh;
}
$dsn = db_dsnlist($p);
$dbh = DB::connect($dsn, $p[‘options’]);
if (DB::isError($dbh))
{
$private_error = ‘dsn:’.var_export($dsn,TRUE).”\n”
366 Part IV: Not So Simple Applications

.’ error:’.var_export($dbh,TRUE).”\n”
;
user_error(
‘Could not connect to database: ‘.$dbh->getMessage()
, $p[‘db_error_level’]
);
return FALSE;
}
if (is_string($p[‘db_error_handler’])
&& function_exists($p[‘db_error_handler’])
)
{
// it’s a function name - OK
}
elseif (is_array($p[‘db_error_handler’])
&& count($p[‘db_error_handler’]) == 2
&& method_exists($p[‘db_error_handler’][0],
$p[‘db_error_handler’][1])
)
{
// it’s an object method - OK
}
else
{
$p[‘db_error_handler’] = NULL;
}
if (!empty($p[‘db_error_handler’]))
{
$dbh->setErrorHandling(PEAR_ERROR_CALLBACK,
$p[‘db_error_handler’]);

}
else
{
$dbh-
>setErrorHandling(PEAR_ERROR_TRIGGER,$p[‘db_error_level’]);
}
$_connections[$p[‘application’]] = $dbh;
if ($dbh === NULL)
{
$private_error = var_export($_connection, TRUE);
user_error(‘connection is NULL.’, $p[‘db_error_level’]);
exit;
}
return $dbh;
}
Chapter 11: Content-Management System 367
If db_connect() is called with no parameters, it hands back the handle of the
last DB object that was created. You’ll notice the use of this function throughout
this example and the examples that follow; we can call db_connect() from any
point in the application — in a Web page, inside a function, and so on— and get
access to the database, without having to set up a global variable, and without
making multiple connections. The more advanced object-oriented features of PHP
4.3 even let us do away with storing the object handle in a variable, and just use
the function in its place. Prior to PHP 4.3 we would have to do something like this:
$dbh = db_connect();
$dbh->query(‘delete * from mysql.users’);
But the new PHP object handling lets us just write
db_connect()->query(‘delete * from mysql.users’);
The db_connect() function also sets up how DB errors are handled. They can
either be passed on directly to a function or class method, or processed when they

trigger a PHP error of a given error level and thus go through whatever error
handling we’ve set up for general PHP errors. For the examples in this book, we
normally use the former method, passing DB errors on to a function of our own,
db_error_handler().
db_error_handler()
We use a special error-handling function for DB errors rather than only relying on
our regular error_handler() function. We do this so that we can roll back any
open transaction (if we still have an active database connection) and then trigger a
fatal error that will exit the page and stop any other queries from running. This is
key to the concept of atomic transactions, which are multi-stage procedures in
which, by rule, either all of the steps must occur, or none of them. This prevents
such problems as, in the case of a bank, money being credited to one account with-
out being subtracted from another one.
function db_error_handler($db_error)
{
$timestamp = time();
// this should be unnecessary but can’t hurt
$dbh = db_connect();
if (is_a($dbh,’DB’))
{
$last_query = $dbh->last_query;
$dbh->query(‘rollback’);
}
$skip_past_function = ‘mysqlraiseerror’;
$private_error = “DB error ($timestamp): “.$db_error->userinfo;
368 Part IV: Not So Simple Applications
$error_level = E_USER_ERROR;
user_error(
“Database error - please contact the system
administrator.($timestamp)”

,$error_level
);
}
db_fetch_record()
This function provides a convenient way to get a record or set of records from a
table. It makes use of DB’s system for token replacement, which is a fancy way of
saying “placeholders.” As a simple example, you can run a query with DB like this:
$result = $dbh->query(‘select * from mytable where mykey = 1’);
But you can also pass in two arguments to DB::query(), the query string itself,
and an array of values to replace into the string:
$result = $dbh->query(
‘select * from mytable where mykey = ?’
, array($mykey)
);
The token character ? in the query string tells DB that it should replace it with
the content of a value from the array of arguments. If you have two ? characters in
your query string, it looks for two values in the array, and so on. The very nice
aspect of this — beyond freeing you from having to build a new query string for
every new set of values you want to include in your query, which is no small pota-
toes — is that DB takes care of quoting and escaping characters for you. A statement
like this:
$mykey = 1;
$myname = “O’Reilly”;
$result = $dbh->query(
‘select * from mytable where mykey = ? and myname = ?’
, array($mykey, $myname)
);
results in this query being run by MySQL:
select * from mytable where mykey = 1 and myname = ‘O\’Reilly’
and although this book is about PHP and MySQL, it’s worth noting here that DB

can be used with a wide variety of databases, handling the proper quotation and
escape syntax for each one. If you’ve ever had to port code from, say, Sybase or
PostgreSQL to MySQL, you can appreciate how valuable a feature that is.
Chapter 11: Content-Management System 369
You can also make substitutions for literal parts of the query, using the ! token
character, like this:
$mykey = 1;
$myname = “O’Reilly”;
$result = $dbh->query(
‘select * from mytable where mykey = ? and ! = ?’
, array($mykey, ‘myname’, $myname)
);
DB interprets the ! character to indicate that it should put the corresponding
value from the argument list as-is, without quoting it, so that you can change the
name of the table of the column you query dynamically. You might be thinking,
looking at this example, what is the point of putting the literal string ‘myname’ in
the argument list, when you could have just written it into the query in the first
place? It’s only to show that you are not limited to using variables in your argu-
ment array.
DB even grabs the contents of an entire file for you, using the & token character,
like this:
$dbh->query(
‘insert into myfiles (filename, filecontents) values (?, &)’
, array(‘my file’, ‘myfile.txt’)
);
So now that we’ve seen a little more of what DB can do for us, look at an exam-
ple of using it, in the db_fetch_record() function:
function db_fetch_record()
{
static $_defaults = array(

‘table’ => NULL
, ‘key’ => NULL
, ‘value’ => NULL
, ‘columns’ => ‘*’
, ‘extra’ => NULL
, ‘key_op’ => ‘=’
, ‘key_join’ => ‘ and ‘
, ‘order_by’ => NULL
);
static $_simple = array(‘table’, ‘key’, ‘value’);
$args = func_get_args();
extract($_defaults);
$p = parse_arguments($args, $_simple, $_defaults);
370 Part IV: Not So Simple Applications
extract($p, EXTR_IF_EXISTS);
$query = ‘select ! from !’;
$bind = array($columns,$table);
$where = NULL;
if (!empty($key) && !empty($value))
{
$where .= implode($key_join, array_fill(0, count($key), “!
$key_op ?”));
if (is_array($key) && is_array($value))
{
foreach ($key as $i => $k)
{
$bind[] = $k;
$bind[] = $value[$i];
}
}

else
{
$bind[] = $key;
$bind[] = $value;
}
}
if ($extra)
{
if ($where)
{
$where = “ ($where) and “;
}
$where .= “ ($extra) “;
}
if ($where)
{
$query .= ‘ where ‘.$where;
}
$order_by = (array)$order_by;
if (count($order_by) > 0)
{
$query .= ‘ order by ‘.implode(‘,’,$order_by);
}
$result = db_connect()->getAll($query, $bind,
DB_FETCHMODE_ASSOC);
if (!$result)
{
Chapter 11: Content-Management System 371
$private_error = ‘could not fetch record: ‘
.’ query=’.$query

.’ bind=’.$bind
.’ result=’.$result
;
user_error(“Could not fetch $table record”, E_USER_ERROR);
exit;
}
if (count($result) == 1)
{
$result = array_shift($result);
}
return $result;
}
If the resulting data set has only one row, that row is returned directly. Otherwise,
the entire data set is returned. In either case, the constant DB_FETCHMODE_ASSOC
(defined by the DB library) tells the DB::getAll() method to return each row of
data as an associative array, with the column names from the query as keys.
db_values_array()
The db_values_array() function is similar to db_fetch_record() in that it’s a
shorthand for writing out a whole query. In this case, though, a list of values is
always returned and a particular table structure is assumed: that the name of the
table is the plural of the name of the label column, and that the name of the key
column is the name of the label column plus _id. You can pass in corrections to
these assumptions as arguments to the function (a common example from our code
here: the name of a status lookup table is usually ‘status’, not ‘statuss’).
function db_values_array ()
{
static $_defaults = array(
‘label’ => NULL
, ‘table’ => NULL
, ‘value’ => NULL

, ‘sort’ => NULL
, ‘where’ => NULL
);
static $_simple = array(‘label’,’table’);
$p = func_get_args();
extract($_defaults);
$p = parse_arguments($p, $_simple, $_defaults);
extract($p, EXTR_IF_EXISTS);
if (empty($label))
372 Part IV: Not So Simple Applications
{
$label = str_replace(‘_id’,’’,$value);
}
elseif (empty($value))
{
$value = $label.’_id’;
}
if (empty($table))
{
$table = $label.’s’;
}
if (empty($sort))
{
$sort = $label;
}
if (empty($where))
{
$where = ‘1=1’;
}
$output = db_connect()->getAssoc(

‘select !, ! from ! where ! order by !’
, FALSE
, array($value,$label,$table,$where,$sort)
);
return $output;
}
The most common use of db_values_array() is to generate a list of values
from a database table for use in a SELECT field or group of option fields (radio but-
tons or checkboxes).
nullop()
The nullop() function returns either is or is not if the value being checked is
equal to NULL, and either = or <> otherwise. We use <> rather than != because the !
character has special meaning to the DB code (see the db_fetch_record() func-
tion, described previously in the chapter):
function nullop($value=NULL,$op=’=’)
{
if ($value === NULL)
{
if (strstr($op,’!=’))
{
$op = ‘is not’;
Chapter 11: Content-Management System 373
}
else
{
$op = ‘is’;
}
}
else
{

if (strstr($op, ‘!=’))
{
$op = ‘<>’;
}
}
return $op;
}
Functions from /content/functions
These functions will be used throughout the application. This section will contain
many references to Chapter 9 because in that chapter we first used many of the
functions we’ll call upon here.
connect_validate_login()
In this example we are using MySQL’s own user and password tables to set up
accounts that can be used with this application. The success or failure of the
attempted connection to a MySQL server tells us if a username is valid or not. We
do this by splitting the authenticate() function used in previous examples into
several pieces, so that we can drop in our own validation code — in this case,
connect_validate_login().
function connect_validate_login()
{
// if we haven’t started a session yet, the references to
$_SESSION
// will come up invalid, so start one if we need to.
check_session();
if (!isset($_SESSION))
{
global $_SESSION;
}
static $_defaults = array(
‘application’ => ‘content’

, ‘username’ => NULL
, ‘password’ => NULL
374 Part IV: Not So Simple Applications
, ‘database’ => ‘netsloth’
);
if ($_defaults[‘username’] === NULL)
{
if (isset($_SESSION[‘PHP_AUTH_USER’]))
{
$_defaults[‘username’] = $_SESSION[‘PHP_AUTH_USER’];
}
if (isset($_SESSION[‘PHP_AUTH_PW’]))
{
$_defaults[‘password’] = $_SESSION[‘PHP_AUTH_PW’];
}
}
static $_simple = array(‘username’,’password’);
$args = func_get_args();
$p = parse_arguments($args, $_simple, $_defaults);
$ok = FALSE;
$p[‘db_error_level’] = E_USER_NOTICE;
ob_start();
$dbh = @db_connect($p);
ob_end_clean();
if ($dbh)
{
$ok = TRUE;
}
else
{

$p[‘error_message’] = “Could not connect\n”;
}
return $ok;
}
fetch_story()
This function enables us to get the record for a story.
function fetch_story ($args=NULL)
{
$story_id=NULL;
if (isset($_SESSION) && isset($_SESSION[‘PHP_AUTH_USER’]))
{
$this_username=$_SESSION[‘PHP_AUTH_USER’];
}
Chapter 11: Content-Management System 375
else
{
$this_username = NULL;
}
if (is_assoc($args))
{
extract($args, EXTR_IF_EXISTS);
}
elseif (is_numeric($args))
{
$story_id = $args;
}
$query = <<<EOQ
select m.user_id as is_ok
, s.*
, date_format(s.publish_dt, ‘%Y’) as publish_yr

, date_format(s.publish_dt, ‘%m’) as publish_mn
, date_format(s.publish_dt, ‘%d’) as publish_dy
, t.stage, t.stage_table
from stories s
left join stages t on s.stage_id = t.stage_id
left join users u on u.username = ifnull(?, user())
left join user_stage_map m on s.stage_id = m.stage_id
and m.user_id = u.user_id
EOQ;
$bind = array($this_username);
if ($story_id)
{
$query .= ‘ where s.story_id = ? ‘;
$bind[] = $story_id;
}
$result = db_connect()->getRow($query, $bind,
DB_FETCHMODE_ASSOC);
return $result;
}
fetch_story_version()
This function works like fetch_story(), except that it allows us to specify a value
for the modify_dt value.
<?php
function fetch_story_version ($args=NULL)
376 Part IV: Not So Simple Applications
{
$story_id = NULL;
$modify_dt = NULL;
$this_username = $_SESSION[‘PHP_AUTH_USER’];
if (is_assoc($args))

{
extract($args, EXTR_IF_EXISTS);
}
elseif (is_array($args))
{
$story_id = array_shift($args);
$modify_dt = array_shift($args);
$this_username = array_shift($args);
}
elseif ($args !== NULL)
{
$story_id = $args;
}
$query = <<<EOQ
select m.user_id as is_ok
, s.*
, date_format(s.publish_dt, ‘%Y’) as publish_yr
, date_format(s.publish_dt, ‘%m’) as publish_mn
, date_format(s.publish_dt, ‘%d’) as publish_dy
, t.stage, t.stage_table
from story_versions s
left join stages t on s.stage_id = t.stage_id
left join users u on u.username = ifnull(?, user())
left join user_stage_map m on s.stage_id = m.stage_id
and m.user_id = u.user_id
EOQ;
$bind = array($this_username);
$wheres = array();
if ($story_id)
{

$wheres[] = ‘s.story_id = ?’;
$bind[] = $story_id;
}
if ($modify_dt)
{
$wheres[] = ‘s.modify_dt = ?’;
$bind[] = $modify_dt;
Chapter 11: Content-Management System 377
}
if (count($wheres) > 0)
{
$query .= ‘ where ‘.implode(‘ and ‘, $wheres);
}
$result = db_connect()->getRow($query, $bind,
DB_FETCHMODE_ASSOC);
return $result;
}
?>
function fetch_author()
This function works similarly to the fetch_story() function, except that it operates
on the authors table to find all the stories by a specified author.
function fetch_author ($args=array())
{
$author_id = NULL;
$other = NULL;
if (is_assoc($args))
{
extract($args, EXTR_IF_EXISTS);
}
else

{
$author_id = $args;
}
$args = array(‘table’=>’authors’);
if ($author_id)
{
$args[‘key’] = ‘author_id’;
$args[‘value’] = $author_id;
}
if (is_assoc($other))
{
$args = array_merge($args, $other);
}
return db_fetch_record($args);
}
fetch_user()
This function also works similarly to the fetch_story function, except it looks for
postings by a given user.
378 Part IV: Not So Simple Applications
function fetch_user ()
{
$params = array(‘table’=>’users’);
$args = func_get_args();
foreach ($args as $arg)
{
if (is_assoc($arg))
{
$params = array_merge($params, $arg);
}
elseif (is_numeric($arg))

{
$params[‘key’][] = ‘user_id’;
$params[‘value’][] = $arg;
}
elseif (is_string($arg))
{
$params[‘key’][] = ‘username’;
$params[‘value’][] = $arg;
}
}
return db_fetch_record($params);
}
stage(), stage_id(), stages()
The stage() and stage_id() functions are front ends to the main stages() func-
tion. The first time stages() is called, the contents of the stages table from the
database are loaded into a static array. This enables us to make subsequent calls to
look up a stage name by its ID value, or vice versa, without querying the database.
<?php
function stage($stage_id=NULL)
{
return stages(‘stage_id’,$stage_id);
}
function stage_id($stage=NULL)
{
return stages(‘stage’,$stage);
}
function stage_table($stage_id=NULL)
{
return stages(‘stage_table’,$stage_id);
}

function stages($key=NULL,$value=NULL)
{
Chapter 11: Content-Management System 379
static $stages = NULL;
if ($stages === NULL)
{
$result = db_connect()->query(
‘select * from stages order by stage_id’
);
while ($row = $result->fetchRow(DB_FETCHMODE_ASSOC))
{
extract($row);
$stages[‘stage’][$stage] = $stage_id;
$stages[‘stage’][‘’][] = $stage_id;
$stages[‘stage_id’][$stage_id] = $stage;
$stages[‘stage_id’][‘’][] = $stage;
$stages[‘stage_table’][$stage_id] = $stage_table;
$stages[‘’][] = $row;
}
$result->free();
}
if (empty($key))
{
return $stages[‘’];
}
elseif (array_key_exists($key,$stages))
{
if (empty($value))
{
return $stages[$key][‘’];

}
elseif (array_key_exists($value,$stages[$key]))
{
return $stages[$key][$value];
}
}
return NULL;
}
?>
stage_table_name()
We build a separate table for each stage of the workflow through which a story
passes. MySQL permissions are assigned, allowing individual users of the applica-
tion to access these tables. This function converts a stage name into the name of the
corresponding stage table.
function stage_table_name($stage)
{
380 Part IV: Not So Simple Applications
return preg_replace(‘/ /’, ‘_’,
strtolower(trim($stage)).’_stories’);
}
write_author()
The write_author() function creates or updates a record in the authors table in
the database.
function write_author($args=array())
{
$author_id = NULL;
$author = NULL;
$email = NULL;
$bio = NULL;
if (is_assoc($args))

{
extract($args, EXTR_IF_EXISTS);
}
else
{
$private_error = ‘bad arguments to write_author:’
. var_export($args, TRUE)
;
user_error(
‘Invalid arguments - could not write author’
, E_USER_WARNING
);
return FALSE;
}
if (empty($author_id))
{
// if we don’t have an ID value, no record exists
// for this author - create one.
$author_id = db_connect()->nextId(‘author’);
$stmt = db_connect()->prepare(
‘insert into authors (author,email,bio,author_id) values
(?,?,?,?)’
);
}
else
{
// if we have an ID value, a record currently exists
// for this author - update it.
Chapter 11: Content-Management System 381
$stmt = db_connect()->prepare(

‘update authors set author=?, email=?, bio=? where
author_id = ?’
);
}
$result = db_connect()->execute($stmt,
array($author,$email,$bio,$author_id));
if (!$result)
{
user_error(‘Could not update author record’,
E_USER_WARNING);
return FALSE;
}
return TRUE;
}
write_story()
The write_story() function creates or updates a record in the stories table in the
database. It also moves a story from one stage to another. Because a user may
attempt to modify a story that is in a stage to which the user does not have access,
or send a story forward or backward in the workflow to a restricted stage, we may
end up getting MySQL permission errors from a query. We don’t want the applica-
tion to simply roll back the transaction and stop when this happens, so we use the
DB class pushErrorHandling() and popErrorHandling() methods (actually,
these are methods inherited from the general PEAR Error class) to temporarily
change the way database errors are handled.
<?php
function start_dbhandler()
{
db_connect()->pushErrorHandling(PEAR_ERROR_TRIGGER,
E_USER_NOTICE);
}

function end_dbhandler($result=TRUE)
{
db_connect()->popErrorHandling();
return $result;
}
function write_story($args=array())
{
$stage_id = NULL;
$publish_yr = NULL;
$publish_mn = NULL;
382 Part IV: Not So Simple Applications
$publish_dy = NULL;
$publish_dt = NULL;
$headline = NULL;
$subtitle = NULL;
$byline_prefix = NULL;
$summary = NULL;
$body = NULL;
$story_id = NULL;
$submit = NULL;
$author = NULL;
$author_id = NULL;
if (is_assoc($args))
{
extract($args, EXTR_IF_EXISTS);
}
else
{
$private_error = ‘write_story: error: bad arguments: ‘
. var_export($args, TRUE)

;
user_error(‘Could not update story’, E_USER_WARNING);
return FALSE;
}
start_dbhandler();
// begin transaction
db_connect()->query(‘begin’);
if (empty($story_id))
{
// if we have no ID value, this is a new story.
// get the ID value of a new record from story sequence
$story_id = db_connect()->nextId(‘story’);
$result = db_connect()->query(
‘insert into stories (story_id,headline) values (?,?)’
, array($story_id,’Not Yet Updated’)
);
if (!$result)
{
db_connect()->query(‘rollback’);
user_error(
‘Could not insert new record into stories table’
Chapter 11: Content-Management System 383
, E_USER_ERROR
);
return end_dbhandler(FALSE);
}
}
else
{
// if we have an ID value, this is an existing story.

// get the name of its current stage table.
// (see admin/stage.php for notes about the purpose and
// use of the stage access tables.)
$oldstage_table = db_connect()->getOne(
‘select s.stage_table from stages s, stories t
where t.story_id = ? and t.stage_id = s.stage_id’
, array($story_id)
);
if (!$oldstage_table)
{
db_connect()->query(‘rollback’);
user_error(
‘Could not access current stage table for story
‘.$story_id
, E_USER_WARNING
);
return end_dbhandler(FALSE);
}
// remove the story from the old stage access table
$result = db_connect()->query(
‘delete from ! where story_id = ?’
, array($oldstage_table,$story_id)
);
if (!$result or DB::isError($result))
{
db_connect()->query(‘rollback’);
user_error(
‘Could not delete from current stage table for story
‘.$story_id
, E_USER_WARNING

);
return end_dbhandler(FALSE);
}
}
// get the assigned stage, or the first stage by default
384 Part IV: Not So Simple Applications

×