Database Essentials
Tutorials / Programming
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:
- page_add_header() – Adds an HTTP header to send with the page.
- page_add_link() – Adds an HTML link tag to the page header. Links are used to include related data such as stylesheets or RSS feeds.
- page_add_meta() – Adds an HTML meta tag to the page.
- page_add_script() – Adds a Javascript script to the page.
- page_add_style() – Adds a CSS stylesheet to the page.
- page_below() – Sets the below_page property of the page. This positions the box as a child of a particular page in the site hierarchy.
- page_description() – Sets the description metadata field.
- page_id() – Sets the page ID. Handy for boxes with "fake" pages that alias them.
- page_keywords() – Sets the keywords metadata field.
- page_onblur() – Adds an onblur handler to the page.
- page_onclick() – Adds an onclick handler to the page.
- page_onfocus() – Adds an onfocus handler to the page.
- page_onload() – Adds an onload handler to the page.
- page_onunload() – Adds an onunload handler to the page.
- page_template() – Sets the global template used to render the page.
- page_template_set() – Changes the global template set used to render the page.
- page_title() – Sets the title of the page.
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:
- App name (e.g., Myapp if your app folder is named 'myapp') with the first letter capitalized.
- Folder name (e.g., Contact if your form is in 'forms/contact') also with the first letter capitalized. Please note that if your form is in a sub-folder such as 'forms/contact/techsupport' you would name it MyappContactTechsupportForm, specifying each sub-folder in turn.
- The word 'Form'.
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:
- is 'some value' – The value must be equal to 'some value'
- contains 'some value' – The value entered must contain 'some value'
- regex 'pattern' – The value must match the regular expression defined in the 'pattern'
- equals 'fieldname' – The value must match the value from another form field, useful for verifying email addresses and passwords.
- empty – The field must be empty. This is more often used with the prefix 'not' so you can verify that the field is 'not empty'.
- length 'value' – The value must be the specified length. Some valid length examples are: 6, 6+, 6-12, 12- meaning 6 exactly, 6 or more, between 6 and 12, and 12 or less.
- gt 'value' – The value must be greater than 'value'
- ge 'value' – The value must be greater than or equal to 'value'
- lt 'value' – The value must be less than 'value'
- le 'value' – The value must be less than or equal to 'value'
- func 'function_name' – The value must return true when passed to the specified function. This enables you to easily define custom validation rules.
- unique 'table/column' – The value must not already exist in the specified database table in the specified column. Useful for ensuring usernames are unique, for example, or that an email address isn't used to register for multiple accounts.
- exists 'path/to/directory' – The value must not exist as a file name in the specified folder. This is useful for file uploads to ensure they don't overwrite an existing file.
- numeric – The value must be numeric, no letters.
- email – The value must be a valid email address.
- header – The value must validate for use in an HTTP header. Useful for securing against header injection attacks.
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:
- {loop/_key} – The array key of the current loop item. This could be a number or a string.
- {loop/_index} – The numeric index of the current loop item. This is always a number and is not the same as the key.
- {loop/_total} – The total number of items in the loop array.
- {loop/_properties} – An array of properties of the current loop item.
- {loop/_value} – The value of the current loop item.
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:
- http://www.example.com/myapp-classtest-action
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'
);
?>
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:
- http://www.example.com/myapp-classtest-action
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'
);
?>