Elefant PHP Content Management System

Meet Sitellite's successor: Elefant CMS

A modern PHP framework and content management system based on the improved features of PHP 5.3+. Elefant is an extremely fast and easy to use CMS that inherits all the best of Sitellite, without the fat. Learn more »


The Class Loader

What is Sitellite?

Sitellite is a website content management system (CMS), which provides features for creating, publishing and managing content on a website.  A CMS provides the system for the various roles involved in the website lifecycle to come together, including the programmers, designers, administrators and content creators. 

Main Components

Sitellite Application Framework

At the core of Sitellite is a standard library of over 100 PHP classes that provide generic, reusable components for building any type of web application.  This includes a database abstraction layer, template libraries, a class loader, form handling, and more.

The Content Server

The content server is the publishing component of Sitellite, which controls access to content such as web pages or news stories, as well as to applications (apps) written on top of the Sitellite framework, and their components (boxes, forms, libraries, collections).  The content server is in charge of parsing a visitor request, determining what they are requesting, and how to display it (design or output templates).

The Content Manager

The content manager is the GUI used b editors and administrators to update the web site content, as well as to access additional apps written on top of the framework.

Model-View-Controller, Sitellite style

Sitellite was designed as a platform for deploying multiple web-based applications with a high degree of compatibility and integration between each of them. We realized that you can look at any website as a collection of applications, from its news section, to user comments, to a discussion forum, search, blog, contact forms, or anything else. But each of these has to come together to make up a consistent visitor experience.

This is where many pure development frameworks fall short, and provides the central distinction of the Sitellite development model, or what we call extended MVC (MVC2). At its core, a CMS is an implementation of the MVC usage pattern. The content server is the controller, the output templates are the view, and the apps and content are the model.

Where we extend this is that each application also implements the MVC pattern on top of this, allowing the same models, views, and controller to be shared among multiple apps, while still allowing for maintainable, organized code within each app.

MVC inside a Sitellite application

There are several different components that make up a complete application. In terms of the actual source code, Sitellite splits this into 3 different areas:

  • Libraries – These go in the 'lib' folder. They make up the abstraction layer between the database and the controller code. In MVC terms, these are the Model.
  • Boxes – These go in the 'boxes' folder. A box is essentially just a PHP script that takes the visitor request parameters, calls the appropriate methods from the Model, and compiles the results using a template. While Sitellite's content server acts as the central Controller, each box acts as a mini Controller itself.
  • Forms – Forms go in the 'forms' folder. They act very much like boxes, but are limited to form handling (display, validation, results processing). Sitellite automatically includes the necessary form classes from SAF to make coding them a bit faster. Forms can be considered nother form of a Controller, like the boxes.

After the source code itself, an app also consists of a few additional components:

  • Templates – These go in the 'html' folder. In Sitellite there are global templates for the site design, and application-specific templates for rendering the generic output of the application (independent of display style/CSS). The app-specific templates use the .spt file extension and use a template language called SimpleTemplate which is better suited to app development. Global templates use an XML-based template format called XT, which are better suited to design output.
  • Configurations – These go in the 'conf' folder. Configuration info includes instructions for how Sitellite should handle the app, as well as settings that customize the app for a particular site.
  • Data – These go in the 'data' folder. Data would include file uploads associated with usage of the app. Each app may maintain its own storage folder, or can integrate with Sitellite's built-in Web Files document repository or the Image Manager feature in Sitellite. This depends on the needs of each particular app.
  • Translations – These go in the 'lang' folder. Sitellite stores translations of content separately, but individual text strings in an app also need translating and Sitellite provides a means of doing that as well, and these are stored in the 'lang' folder. It's easy to make applications multilingual in Sitellite.
  • Install data – These go in the 'install' folder. This would include any custom content types defined in the app, the database schema for the app, scheduled tasks to be installed in the scheduler, etc.

As you can see, there's a lot more than just a model, view and controller to a real-world application. But Sitellite takes care of each of these in a standardized way across each app, which makes it easier for everything to work together nicely.

But enough with the theory, let's get into some practical examples. 

Exercise: Install the Sitellite CMS on your workstation so you can work through the examples in this lesson. Describe any challenges you ran into during the installation process.

Sitellite Installation Guides

Here are a few guides to help you get Sitellite installed on your desktop.

Sitellite on Windows Setup Guide

Sitellite on Mac OS X Setup Guide

Sitellite Installation Guide

Directory structure

boxes
conf
data
docs
forms
html
install
lang
lib
pix

Exercise: Create a blank application directory in the correct folder of your Sitellite installation, and describe the use of at least two of these folders. Name the app folder itself 'myapp'.

Creating a box

<?php

echo 'Hello ' . $_GET['name'];

?>
sitellite_access = public
sitellite_status = approved
sitellite_action = on

You should now be able to call your box from the browser like this:

http://www.example.com/myapp-helloworld-action?name=Me

For the sake of not repeating information in too many places, here's a tutorials that covers Sitellite's URL structure and what each component of it means.

Our first template

<p>Hello {name}</p>
<?php

echo template_simple ('helloworld.spt', $_GET);

?>

Taking stock

boxes
    helloworld
        access.php
        index.php
conf
data
docs
forms
html
    helloworld.spt
install
lang
lib
pix

SimpleTemplate basics

You can pass either an array or an object in the second parameter of the template_simple() function call.  The items or properties of this parameter are then made available to the template by name using curly braces to denote insertion points.  For example, {name} in the above example refers to $_GET['name'].

SimpleTemplate files have several more powerful features which we'll explore later, including looping and conditions (if statements). 

Global objects

Sitellite makes a number of global objects available to you to make certain programming tasks easier.  These can be called in your boxes, libraries, or even in your templates.  The main objects include:

$cgi

Contains the GET and POST variables sent by the current box or form.  Also allows you to do some input validation on user-submitted data and other routines related to request handling.

$db

A database connection object.  This allows you to send and receive data from the underlying MySQL database, and to connect to additional databases as needed as well.

$intl

A language translation object.  This allows you to add multilingual features to your apps.

$loader

A class and box loading object.  This allows you to import new class libraries from SAF or from any app, and also allows you to call boxes and forms from  one another.

$menu

Contains a tree structure of the pages of the website.  Can be used to build dynamic navigational menus.

$page

Contains the current web page data.  Allows you to view and set new page properties, including the global templates used for the current request.

$session

A session authentication object.  This contains the current user's profile and access rights.  You can also set temporary session data here, such as a list of shopping cart items.

$site

Contains the directory and URL information for the website and the current web page request.

Some of these objects also provide functions to access them, such as $db, $page, and $session.  Others have to be imported into the box's namespaces before they can be used, since each box is executed in its own namespace separate from the global one.  This helps eliminate conflicts and side-effects between boxes.

Accessing global objects

<?php

// get the names list from the session data
$names = session_get ('names');

// if there's no list yet, create a blank one
if (! is_array ($names)) {
    $names = array ();
}

// add the new name and save it to the session data
$names[] = $_GET['name'];
session_set ('names', $names);

// now we'll create a data array to pass to the template,
// since we're passing it the $names list as well as the
// current name
$data = array (
    'name' => $_GET['name'],
    'list' => $names,
);

// call the template and pass it $data this time
echo template_simple ('helloworld.spt', $data);

?>
<p>Hello {name}</p>

<p>I've said hello to:</p>

<ul>
{loop obj[list]}
    <li>{loop/_value}</li>
{end loop}
</ul>
If you refresh the browser window and change the "?name=Tom" parameter each time, you should see a history of all the names sent from this point on.

Setting page properties

<?php

// get the names list from the session data
$names = session_get ('names');

// if there's no list yet, create a blank one
if (! is_array ($names)) {
    $names = array ();
}

// add the new name and save it to the session data
$names[] = $_GET['name'];
session_set ('names', $names);

// now we'll create a data array to pass to the template,
// since we're passing it the $names list as well as the
// current name
$data = array (
    'name' => $_GET['name'],
    'list' => $names,
);

// set the page title
page_title ('Hello World Examples');

// call the template and pass it $data this time
echo template_simple ('helloworld.spt', $data);

?>

The other properties of the page that can be altered include:

The output of the box technically becomes the $page->body value used in the global design templates.

You can find the complete list of functions, properties and methods for each object above in Sitellite's API reference here: 

Sitellite API Reference

Exercise: Use one of the other global objects mentioned above to add a new feature to the 'helloworld' box. Upload the modified PHP code. Feel free to add the modified helloworld.spt file as a comment to the journal entry as well.

The MailForm library

Sitellite's form handling is provided by the MailForm libraries in SAF. MailForm handles generating forms based on simple INI-formatted files that define the form "widgets" (MailForm's name for fields) and a PHP script that does the handling.

MailForm has several built-in features for preventing spambot abuse, and also takes care of the input validation and translation issues automatically, including many built-in validation rules as well as the ability to create custom validation rules as PHP functions.

A simple contact form

[Form]

message = Please use this form to contact us.

[name]

type = text
alt = Your Name

[email]

type = text
alt = Email Address
rule 1 = "not empty, You must enter your email address."
rule 2 = "email, Your email address does not appear to be valid."

[message]

type = textarea
alt = Comments/Questions
labelPosition = left

[submit_button]

type = submit
setValues = Send

You'll also want to copy the 'access.php' file from the 'helloworld' box into your 'contact' folder as well. Forms need the same access settings as boxes.

Breaking down the above file, you'll see that it's divided into blocks using the square braces to denote block names.  It always starts with the [Form] block, which defines properties of the form itself, followed by subsequent blocks, each of which represents a single form field or widget.

Types 

Each widget must be given a type.  The types correspond to the list of available widgets in the Sitellite API references, and you can also make custom types and refer to them in individual apps.  For example, Sitellite's built-in WYSIWYG editor, called "xed" exposes a widget which can be called like this:

type = xed.Widget.Xeditor

This tells Sitellite to look in the 'xed' app's libraries for a Widget/Xeditor.php file where it should find a widget of the type 'xeditor'.  MailForm handles all the class loading behind-the-scenes, so you never have to worry about that in your form code.

Alt 

The alt setting defines the text to be displayed next to the field itself.  If not set, this will default to the widget name capitalized.  For example "widget_name" would become "Widget Name" if no alt setting is specified.

Rules

You can also see in the email field that two rules are defined for it.  The first tells MailForm that the field can't be empty, and the second puts it through some tests to try to make sure it's a valid email address.

You can have as many rules for each widget, and we'll look at some more of the rule types a bit later.  For now, it's enough to know that they take the form "rule # = rule, Error message."

<label for="name">Your Name</label>
<input type="text" name="name" />

The index.php file

<?php

class MyappContactForm extends MailForm {
    function MyappContactForm () {
        parent::MailForm (__FILE__);
    }

    function onSubmit ($vals) {
        // let's output the form data to the user
        echo 'You entered:<br /><br />';

        foreach ($vals as $key => $value) {
            echo $key . ' = ' . $value . '<br />';
        }
    }
}

?>

Sitellite knows to automatically import the MailForm library when a form is called, so you don't have to explicitly import it yourself.  In a box, you would need to first say:

loader_import ('saf.MailForm');

So the first thing to do is define a new class for our form.  Form classes are also automatically instantiated and called by Sitellite, but this means you need to name your form classes in a way that Sitellite expects.   The format of a class name is as follows:

You class extends the MailForm class so that it can inherit all the functionality it needs to draw your form.

Next, you need to create a constructor method, which is a function named the same thing as your class name.  In more complex forms, you might put some additional processing here, but for now all this needs to call is:

parent::MailForm (__FILE__);

This tells MailForm where your class is, so it can find and parse your settings.php file for you automatically.

The last thing your class needs is an onSubmit() method, which will be given an associative array of the form data and does something with the results.  In this case, it simply displays them back for the visitor. 

Viewing the results

You're now ready to view the form you just created.  To load the form from your browser, go to:

http://www.example.com/myapp-contact-form

You should see your form appear there (changing 'www.example.com' to your own site address).  Feel free to try it out and see the results. 

Exercise: Modify the onSubmit() form handler to send you an email using PHP's mail() function when the form is submitted. You can upload your finished changes here.

A closer look at validation rules

Let's take a closer look at the input validation capabilities of MailForm.  As we saw before, validation rules take the following format:

rule # = "rule, Error message."

The rule itself has its own format as well, and there are many different types of rules you can use out-of-the-box.  These are:

You can also specify 'not' in front of any rule to change the rule to its opposite.  For example:

rule 1 = not empty, This field cannot be empty.

rule 2 = not numeric, This field must contain letters AND numbers.

rule 3 = "not equals 'other_field', This field must not be the same as other_field."

Custom validation rules

<?php

function myapp_rule_username_lowercase ($vals) {
    if ($vals['username'] != strtolower ($vals['username'])) {
        // the username must be lower case
        return false;
    }
    // the username is correct
    return true;
}

?>

To call this function as a rule, you would say:

rule 1 = "func 'myapp_rule_username_lowercase', Your chosen username must be lowercase."

Not necessarily the most useful example, but you could easily extend this for credit card format validation, ensuring dates are correct (ie. they didn't enter 1492/25/64), and so on.

Exercise: Add a custom validation rule to your form and call it via your settings.php file.

SimpleTemplate is one of two template engines in Sitellite. The other, XT, is an XML-based template language used to render the design templates for the web site. SimpleTemplate, on the other hand, is used for smaller output, such as the formatting of search results or news stories. The reason for the two, instead of the usual one-size-fits-all template language which is what most frameworks offer, is that we were able to better tailor each for its intended use.

SimpleTemplate allows you to create small templates fast. Its syntax is not overly complex, and it has none of the XML verbosity that XT does. This makes it ideal for programmers to use to format the output of database queries and other user interface needs.

The basics

As we've seen from the above examples, SimpleTemplate templates are saved to '.spt' files in the 'html' folder of each app.

The basic tags that can be used in the template correspond to the key names or property names of the associative array or object passed to the template_simple() function. So if the array contains the keys 'id', 'name', and 'description', then the main tags are {id}, {name}, and {description}.

Loops

<ul>
{loop obj[items]}
    <li>{loop/_value}</li>
{end loop}
</ul>

The obj[items] means "loop through the array stored in the 'items' key of the array passed to the template". 'obj' represents the array or object passed to the template. Similarly, if we passed an object to the template, obj[items] would become obj.items. These are both a shorthand way of saying $obj['items'] and $obj->items respectively, which makes it easier to read within a template.

We refer to the current loop item via {loop/property} where 'property' is some property of the current loop item. {loop/_value} is one of several properties available in any SimpleTemplate loop, because they are created by SimpleTemplate itself. These include:

Conditions

{if obj[some_value]}
<p>{some_value}</p>
{end if}
{if obj[some_value]}
<p>{some_value}</p>
{end if}
{if else}
<p>No value.</p>
{end if}
{if obj.property eq 'foo'}
<p>{property}</p>
{end if}

{if obj.property gt 5}
<p>{property}</p>
{end if}

{if not empty (obj.property)}
<p>{property}</p>
{end if}

Multiple loops

{loop obj[people]}
<h2>{loop/name}</h2>

<ul>
    {loop loop.cities}
        <li>{loop/_value}
            {if loop._value eq parent.city}- Current{end if}
        </li>
    {end loop}
</ul>
{end loop}

Output filters

Default - passed to htmlentities_compat():
{some_value}

Using an alternate function:
{filter strtoupper}{some_value}{end filter}

Using multiple filters - evaluated outward, so nl2br is first:
{filter strtoupper/nl2br}{some_value}{end filter}

Using an alternate filter on multiple tags:
{filter strtoupper}
<h1>{name}</h1>
<p>{description}</p>
{end filter}

Disabling all filters:
{filter none}{some_value}{end filter}

Filter shorthand for a single item:
{some_value|strtoupper}

Filter shorthand with multiple filters - evaluated outward again:
{some_value|nl2br|strtoupper}

Additional tags

Beyond looping, conditions and output filtering, SimpleTemplate has a number of additional features for template developers, listed with examples below.

{exec} and {php}

{php obj[some_value]}

{exec obj[some_value] += 1}

{intl}

{intl This text can be translated.}

This text cannot.

{alt}

{alt odd even}

<table>
{loop obj[items]}
    <tr class="{alt/next}">
        <td>{loop/_key}</td>
        <td>{loop/_value}</td>
    </td>
{end loop}
</table>

Boxes

Output a dynamic breadcrumb menu from the 'sitellite' app:
{box sitellite/nav/breadcrumb}

Output the helloworld box from our previous examples:
{box myapp/helloworld?name=Joe}

Output the standard Sitellite editing buttons for the web view:
{box cms/buttons?collection=myapp_listing&object=[obj]}

Registering multiple objects

<?php

loader_import ('myapp.Objects');

$product = new Product ($_GET['id']);

$category =& $product->getCategory ();

template_simple_register ('category', $category->makeObj ());

template_simple ('register_test.spt', $product->makeObj ());

?>
<p>Product name: {name}</p>
<p>Category name: {category/name}</p>
When writing applications in any language, it's easy to amass a large number of scripts very quickly that often have duplicate bits of code that would be better placed in a single, shared location. To solve this, we use PHP classes to abstract repetitious tasks so that we only write them once, then use the class in each place that the task is needed.

Loading classes from SAF

<?php

loader_import ('saf.HTML');

echo html::p (
    html::strong ('Some text'),
    array ('style' => 'text-align: center')
);

?>

Save this to inc/app/myapp/boxes/classtest/index.php.  Also make sure to copy the access.php file into your new box, or move it into the main 'boxes' folder so it is inherited by all your app's boxes.

To load this box in your browser, go to:

Calling your own classes

<?php

class MyList {
    var $list = array ();

    function get ($name) {
        return $this->list[$name];
    }

    function set ($name, $value) {
        $this->list[$name] = $value;
    }
}

?>

Loading your custom class

<?php

loader_import ('myapp.MyList');

$list = new MyList ();

$list->set ('foo', 'bar');

echo $list->get ('foo');

?>

A little cross-app integration

<?php

// import the library itself
loader_import ('news.Story');

// create a new object
$story = new NewsStory;

// set a few properties for performing a story search
$story->limit (10);
$story->orderBy ('date desc, rank desc');

// we can get the latest 10 stories with a blank search
$list = $story->find (array ());

// let's output the stories with a template
echo template_simple ('latest_stories.spt', $list);

?>
<ul>
{loop obj}
    <li><a href="{site/prefix}/news-app/id.{loop/id}">{loop/title}</a></li>
{end loop}
</ul>

Exercise: As you can see, Sitellite makes it easy to integrate components of one app with another. Look up the loader_box() function and describe the various ways of calling boxes from other boxes, from .spt templates, and in global templates as well.

The Generic class

The Generic class is part of the saf.Database package.  It provides a set of generic accessor methods for reading and writing to database tables, which can drastically reduce the amount of effort required to write database-bound code, which is a frequent endeavour in most applications.

Generic also provides features that allow objects to be automatically generated in a similar way as forms are with INI descriptors, and provides automatic integration with Sitellite's access control and multilingual features with no extra coding.  But first, let's look at a simple example and build up from there.

Creating a database table

create table myapp_listing (
    id int not null auto_increment,
    name char(48) not null,
    description text,
    primary key (id),
    index (name)
);

Using Generic with our new table

<?php

loader_import ('saf.Database.Generic');

class MyappListing extends Generic {
    function MyappListing () {
        parent::Generic ('myapp_listing', 'id');
    }
}

?>
This simply imports the Generic package and creates a blank class extending it.  The parent::Generic() call is passed the database table name and the primary key field name.  That's all Generic needs to work with our new table.
<?php

loader_import ('myapp.Listing');

$listing = new MyappListing ();

// add a few listings...

if (! $listing->add (array (
    'name' => 'Listing One',
    'description' => 'Description of the first listing.',
))) {
    die ($listing->error);
}

$id = $listing->add (array (
    'name' => 'Listing Tw',
    'description' => 'Description of the second listing.',
));

// update listing two (to correct our spelling!)

$listing->modify ($id, array ('name' => 'Listing Two'));

// display all of the listings

echo template_simple (
    'listings.spt',
    $listing->find (array ())
);

// delete them all

$listing->remove (array ('1=1'));

?>
{loop obj}
<p>
    <strong>{loop/name}</strong><br />
    {loop/description}
</p>
{end loop}

Exercise: What Generic method retrieves a single object from the database if you give it the primary key value for that object?

Defining multiple tables and their relations

create table myapp_products (
    id int not null auto_increment,
    name char(72) not null,
    price decimal(7,2) not null,
    category int not null,
    description text not null,
    primary key (id),
    index (category, price)
);

create table myapp_categories (
    id int not null auto_increment,
    name char(72) not null,
    primary key (id),
    index (name)
);

The Objects.ini.php file

; <?php /*

[Product]

table = myapp_products
pkey = id

[Category]

table = myapp_categories
pkey = id

[rel:Category:Product]

type = 1x ; one-to-many
Product field = category
cascade = on ; delete products when a category is deleted

; */ ?>
create table myapp_product_category (
    product_id int not null,
    category_id int not null,
    primary key (product_id, category_id)
);
[rel:Category:Product]

type = xx ; many-to-many
join_table = myapp_product_category
Product field = product_id
Category field = category_id
<?php

loader_import ('myapp.Objects');

$category = new Category;

$cat_id = $category->add (array (
    'name' => 'Hosting Packages',
));

$product = new Product;

$product->add (array (
    'name' => 'Basic Hosting',
    'price' => 9.95,
    'category' => $cat_id,
    'description' => '250MB storage, 5GB bandwidth, etc.',
));

?>

Table relations

Product::setCategory (&$category_object)
Product::unsetCategory (&$category_object)
Product:getCategories ()
Category::setProduct (&$product_object)
Category::unsetProduct (&$product_object)
Category::getProducts ()
<?php

loader_import ('myapp.Objects');

$product = new Product (array (
    'name' => 'Basic Hosting',
    'price' => 9.95,
    'description' => '250MB storage, 5GB bandwidth, etc.',
));

$category =& $product->setCategory (
    new Category (array (
        'name' => 'Hosting Packages',
    ))
);

?>
<?php

loader_import ('myapp.Objects');

$category = new Category (array (
    'name' => 'Hosting Packages',
));

$product =& $category->setProduct (
    new Product (array (
        'name' => 'Basic Hosting',
        'price' => 9.95,
        'description' => '250MB storage, 5GB bandwidth, etc.',
    ))
);

?>
Also note that the set*() methods return a reference to the Generic object being passed to them, allowing you to pass them a new object, but to still capture it and manipulate it further afterwards.

Updating a single item

<?php

loader_import ('myapp.Objects');

// get the specified product

$product = new Product ($_GET['id']);

// modify the product in place

$product->set ('name', $_GET['name']);
$product->set ('price', $_GET['price']);
$product->set ('description', $_GET['description']);

// save the changes to the database

$product->save ();

?>

Single product display

<?php

loader_import ('myapp.Objects');

$product = new Product ($_GET['id']);

echo template_simple ('product.spt', $product->makeObj ());

?>

Permissions/access control

alter table myapp_products add column sitellite_status varchar(48) not null default '';
alter table myapp_products add column sitellite_access varchar(48) not null default '';
alter table myapp_products add column sitellite_team varchar(48) not null default '';
alter table myapp_products add index (sitellite_status, sitellite_access, sitellite_team);
permissions = on
I'm afraid that is actually all there is to it.  Requests made to find() items from the product list will automatically be limited by Sitellite's access controls.  Generic ties into Sitellite's saf.Session package for that.

Multilingual objects

multilingual = on

Custom class methods

import = myapp.CustomProduct
extends = CustomProduct
<?php

loader_import ('saf.Database.Generic');

class CustomProduct extends Generic {
    // let's add a calculateTaxes() method here
    function calculateTaxes ($percent) {
        return $this->val ('price') * $percent;
    }
}

?>

The one difference here is that we don't have to worry about a constructor or telling Generic what table information to use, since we do that in the lib/Objects.ini.php file already. We simply create the shell of a class and add methods to it.

Generic leads to short, simple, and highly readable box code that is obvious in its function and easy to maintain. It also encourages a more disciplined programming technique, since accessing your data through the Generic objects helps enforce the Model part of MVC.

And as you can see, it's quick due to the minimal amount of code that offers fairly deep integration with Sitellite's various components.

Performance Tip: If you chmod your 'lib' folder to be writeable by Apache (usually 0777), then the auto-generated code will be saved to lib/_Objects.php so it's only generated dynamically on the first request.  If you make changes to the lib/Objects.ini.php file afterwards, Sitellite will automatically update the generated code as well. 

Lazy database programming

<?php

// insert a product into the database
db_execute (
    'insert into myapp_products values (null, ?, ?, ?, ?)',
    $_GET['name'],
    $_GET['price'],
    $_GET['category_id'],
    $_GET['description']
);

// get the last inserted id value
$id = db_lastid ();

// fetch all of the items in a certain category
$list = db_fetch_array (
    'select * from myapp_products where category_id = ?',
    $_GET['category_id']
);

// get a single product object
$product = db_single (
    'select * from myapp_products where id = ?',
    $_GET['id']
);

// get just the price of a single product
$price = db_shift (
    'select price from myapp_products where id = ?',
    $_GET['id']
);

// get a list of key/value pairs for the products (id and name)
$list = db_pairs (
    'select id, name from myapp_products order by name asc'
);

?>