Table of contents
The Wildfire Framework is a set of PHP class files packed with features to ease your development pains. To use the framework in a PHP script, you only need to require one file.
<?php
require "/path/to/wildfire/common/initFramework.php";
?>
That's it! If you're curious as to what this file actually does, we've detailed it right here, but you might just want to skip it and read on about the different packages of classes available to you now that you've turned on the framework.
This is the root package to the Wildfire framework. It contains a few classes you'll want to remember.
wfObject is responsible for loading and finding classes. It has only static methods. In addition to its capabilities regarding loading classes, it has three assert methods that class authors are encouraged to use: _assertTraversable, _assertType, and _assertClassExists.
wfError is a simple error handler. The framework initialization script calls a method on this class that registers it as the default error handler. Any errors that come through this class are turned into Exceptions and thrown.
wfI18N is the gateway to internationalization strings. The i18n folder has (read: will have) a folder for all translated languages. In each is one or more files that contain associative arrays of strings. wfI18N has two important methods:
This class provides a means to get and store application settings. There is an "options.ini.php" file in the "common" folder, containing these settings. There are three methods to this static class.
This static class provides a simple page timer, which is started by the framework initialization script. There are only two methods, to get the current execution time, and to restart the timer.
Another static class, wfString was a place to gather all the useful string manipulation methods that piled up as the framework grew. As a note for future development, similar string methods should be put here.
PHP lacks a native enumerated type, so this was our best attempt. The rest of this description is copied from the class doc block.
Enum classes are used to represent set types of things, for instance, if we created a class to represent different operating systems, it might provide enum methods like this:
$unix = OperatingSystem::Unix();
$win = OperatingSystem::Windows();
$mac = OperatingSystem::Mac();
echo $unix->getName(); // prints Unix
The collections package has several interfaces and implementing classes that have done your dirty work for you. Call it a tip of the hat and an improvement upon Java's collections package. As such, we use some of the same names you might have heard when referring to collections in other programming languages.
Taking these concepts and running, we devised the following classes and interfaces for your use.
Why do all this? PHP's array capabilities are quite impressive indeed, you might say. Just as the SPL allows you to have Iterable classes, so too did we want to have an object that can do many of the things one can do with arrays with intelligent exception throwing.
The data package provides for two kinds of objects: data adapters and a record set wrapper.
The data adapters build on top of PDO, adding some needed or convenient capabilities. Currently, adapters exist for the following databases:
The data adapters add the following methods onto of the PDO class.
There is a convenient factory class to instantiate these adapters for you.
$info = array( "hostname"=>"localhost", "username"=>"foo", "password"=>"bar", "dbname"=>"test" );
$options = array(PDO::ATTR_PERSISTENT => true);
$mysql = wfDb::get( 'mysql', $info, $options );
$info = array( "uri"=>"/var/www/sqlite/wildfire.sqlite" );
$sqlite = wfDb::get( 'sqlite', $info );
The record set is an editable result set with collections of its columns and rows. There are five classes involved with the record set.
These classes aren't tied to the adapters at all, but a wfRecordSet can be created from a PDOStatement. We needed these classes because the PDOStatement object is immutable, so the runtime object query mechanism (outlined in the domain model section) needed another avenue to create its own record sets. Also, PDOStatements, and all record sets for that matter lack manipulation capabilities. With our record set, you can remove or add columns, sort the rows, or change row values.
We're aware there are ORM (object-relational mapper) systems out and about for PHP. Most of them have adopted the design pattern popular with the Ruby on Rails folks, the Active Record pattern. While there is nothing wrong with that pattern, we felt it necessary to introduce other patterns as well, because you're entitled to our opinions.
For those of you who study software design patterns, we have taken the good ideas behind Data Mappers, the Identity Map, and the Unit of Work. The domain model is made up of these several parts that all work together to achieve data object persistence.
For example's sake, let's say your domain model has to do with a message board system, comprised of Users, then Boards which contain Threads, which contain Posts. For simplicity and example's sake, all Users have permission to all boards, and each board has only one moderator.
The data entity is the holder for information out of the data store. Essentially, each logical "thing" you can come up with should be an entity. In our example case, we would create the following entity classes (we strongly recommend your objects be named in singular):
The wfDataEntity has the logic to load and contain relations between itself and other entities. It doesn't know about its own collection, and it only knows about the object retrieval methods of the wfWorkUnit. (That's a lie - when the entity is changed in any way, it alerts the wfWorkUnit that it is dirty and needs to be updated in the data store).
This is an extension of wfSortableSet, and is designed to hold entities of one type. In our case, we would create a UserCollection, BoardCollection, ThreadCollection, and PostCollection.
Why use strongly-typed collections instead of arrays? This allows class authors to typehint a collection of one type of entity and be sure they're only getting the one type, instead of having to test each entry in a collection to make sure it's the entity they expect.
The data mapper is responsible for performing CRUD operations with the data store (create, retrieve, update, delete). It turns raw data into entities. There is one mapper per entity class. Thus, for the example, we would create a UserMapper, BoardMapper, ThreadMapper, and PostMapper. wfDataMapper is an abstract class, and currently, the only subclass to it is wfSQLMapper. We wanted to design it that way in case there were some other data store used besides a database (xml files, for instance). The mapper is responsible for a few things aside from CRUD.
Each mapper has a method for returning the class name of its supported entity and a method that returns an instantiated collection appropriate to that entity.
The mapper is not responsible for caching, however it is responsible for defining the duration the entity should be cached. Each mapper has a method that returns an EntityCacheType stating this duration. There are three durations:
The mapper defines the relationships between the entity it owns and other entities. For information on the relationship definition, see the API documentation for the getRelationships method in the wfDataMapper class.
In our example, the relationships are as follows
The mappers are also responsible for saving changes to linked entity collections. For example, if we have a Board object and delete from its ThreadCollection a thread, the BoardMapper should make sure to tell the ThreadMapper to delete that thread (if we set the relationship to do that - which we would).
Data mappers should have no knowledge whatsoever of the wfWorkUnit.
The identity map is a place to store the entity once it has been retrieved by the data mapper. The work unit takes care of this, so don't worry about interacting with the identity map directly.
Entities are stored here so that if they are requested again within their cache time (see above), they can simply be pulled out of the identity map instead of having to take an expensive trip back to the data store.
The wfIdentityMap keeps track of those entities that should be persisted only for the current request, for a certain time, or forever. When the identity map is serialized to the session as a property of the work unit, all request-only entities are removed, and any time-out entities are checked for expiration (the identity map has a wfTimedBag collection with entities in it).
Again, it's all very seamless and it's all handled by the wfWorkUnit.
The work unit is a session-persistant object that is the gateway between userland and the ORM model. The work unit allows you to CRUD objects without any knowledge of their storage methods. You can do queries against these objects and even execute reports with grouping and aggregate functions (see wf.domainModel.query). The work unit has a few responsibilities and uses.
The work unit receives requests for entities in one of four ways.
When get or getAll is called, first, the work unit checks the identity map to see if the requested entity or entities are stored there. If not, it uses the entity's data mapper to get the requested object(s), puts it or them into the identity map, then returns the entity or collection to the user. The methods find and findAll do not check the identity map but go straight to the mapper.
Secondly, if an entity is new and you wish to put it in the data store, you simply register the object as new, and on the next commit, it will be inserted.
Thirdly, if an entity is changed (its own values or the members of a linked collection), it contacts the wfWorkUnit itself and says "Hey, I'm different." On the next commit, any pending transactions will be taken care of.
If you want to remove an entity, the work unit can do your dirty work and bump it, off it, whack it, etc.
The work unit has a commit and rollback function. Any changes you perform are considered volatile data until you commit them. Keep in mind that since the work unit persists across sessions, if you have entities that are cached for longer that just a request, any uncommitted changes you make will still be there until they're rolled back or committed. If you've had it with the current session, you can destroy it and unload all data.
The query package of the domain model allows you to construct with SQL-like syntax, or via the programmatic API, query statements that can be executed to return collections of entities or wfRecordSet objects with aggregated data. There are several facets to this package.
The parts of the query statement are arranged into tokens. There are three kinds of tokens: Criteria, Clauses, and Clause Items.
There is an abstract class that extends wfqlToken called Criterion (singular for Criteria, grammar nerds!). It has two implementing classes: Expression and Junction. When forming an if statement in PHP or a where clause in SQL, an Expression can be likened to a boolean statement (a > b), likewise a Junction is two or more (a > b AND b < c ).
There are three types of wfqlClause objects: SelectClause, GroupClause, and SortClause. Similarly, they each have a corresponding wfqlClauseItem: Select, Group, and Sort.
This is an enum representing the different functions that can be performed on grouped data.
The WFQL engine translates the string representation of columns and method calls into references. To put it simply, they tell the engine how to get the value out of the reference, if it has any aliases in the rest of the query, if it has an aggregate function attached, and soforth. The reference manager keeps tabs on all of these references for a single query. These classes are used by the engine and don't need your attention. Move along! :)
There are two more classes, wfqlParser and Tuple, but you'll never need them, so they shall remain a mystery to the world of documentation.
wfQuery allows you to specify criteria and sorting to get a collection. You can add Criterion objects and Sort objects, or parse a statement from a string consisting of "WHERE [criteria] ORDER BY [sorts]". Once executed, it returns a collection of entities.
Take wfQuery and add the ability to specify which fields you want and how to group them (if you want to group them), and some having criteria for the groups (if you've grouped them). Once executed, it returns a wfRecordSet with the data you specified.
What you've been waiting for, right? Our first example will demonstrate programmatically building a query to get Post objects authored by User #24.
<?php
include "common/initFramework.php";
$unit = wfWorkUnit::getInstance();
$query = $unit->query("Post");
$query
->addWhere( Expression::eq('userId',24) )
->addSort( Sort::descending('postedOn') );
$posts = $query->execute();
?>
Here's the same but as a parsed statement.
<?php
include "common/initFramework.php";
$unit = wfWorkUnit::getInstance();
$query = $unit->query("Post",'where userId = 24 order by postedOn desc');
$posts = $query->execute();
?>
You can even use field names that deal with linked entities. Let's say the post has a relationship with its author.
<?php
include "common/initFramework.php";
$unit = wfWorkUnit::getInstance();
$query = $unit->query("Post",'where author->lastName = "Smith" order by postedOn desc');
$posts = $query->execute();
?>
Getting excited? Now let's try a report query for the total number of posts each user has made.
<?php
include "common/initFramework.php";
$unit = wfWorkUnit::getInstance();
$query = $unit->reportQuery("Post");
$query
->addSelect( Select::column('userId') )
->addSelect( Select::aggregate(Aggregate::Count(),'id','posts') )
->addGroup( Group::parse('userId') )
->addHaving( Expression::gte('posts',10) )
->addSort( Sort::descending('posts') );
$recordSet = $query->execute();
// the recordset will have two columns, userId and posts, sorted by posts descending,
// and only have the records for users who have posted at least ten messages
?>
The same thing parsed:
<?php
include "common/initFramework.php";
$unit = wfWorkUnit::getInstance();
$statement = "SELECT userId, count(id) as posts " .
" GrOuP By userId HAViNG posts > 10 " .
" ORDER BY posts desc";
$query = $unit->reportQuery("Post",$statement);
$recordSet = $query->execute();
// the recordset will have two columns, userId and posts, sorted by posts descending,
// and only have the records for users who have posted at least ten messages
?>
You can use methods instead of property names, even methods that don't correspond to database fields. You can even give them arguments.
Literals like strings must be quoted with double-quotes.
Case for tokens doesn't matter - but property and method case does!
The new classes added to the PHP SPL for file and directory access are nice, they're just not what we like to use. We seperated the functionality of different streams out into different classes. Local files have a lot more methods than remote ones, for instance. Directories have some different features than regular files. The interfaces and classes below support the seperation of capability in these different types of resources.
Objects can implement both interfaces for streams, but should implement at least one.
All classes in wf.io inherit from the abstract class wfStream. wfStreamFactory takes a file/directory URI and creates the correct object for it.
Any read-capable stream can be copied to a write-capable stream. (Files to directories, directories to other directories, etc.) The raw stream resources can be passed to PHP stream functions if desired.
The observer pattern is nothing new to you savvy software engineers. It allows for popular objects (read: Subjects), to notify other suck-up objects (read: Observers), of their changes via messages (read: Messages). The whole thing works very much like LiveJournal.
The good thing about this pattern means that an object can publicize its events without any knowledge of the objects it's notifying. Observers opt-in to receive these notifications. Let's say there's an extension to Wildfire called Extension X. There's another called Extension Z. Extension X has a very nifty object that changes values over time. Extension Z developers want to use all that nice code, but not to change it. If Extension X's nifty object implements ISubject, and Extension Z's interested objects implement IObserver and opt-in to messages from the ISubject, it can be tied into events without altering any of Extension X's code.
An object can be both an IObserver and an ISubject. Hell, it can even notify itself of changes.
The web package borrowed ideas from ASP.NET and from JSP in the handling of responses and requests (and our own spin on things). Basically, there are three classes that will be your new best friends: wfHttpRequest, wfHttpResponse, and wfSession.
This object should be available globally as $request. (It's not a superglobal like $_GET or $_POST, but it's good enough.)
There are three things available on wfHttpRequest of which you should take note.
This is a wfQueryString object. wfQueryString is an IMap containing the $_GET data. You can reference it any of the following ways:
wfQueryString and wfFormData both inherit from the abstract class wfRequestValueData, which provides scrubbing methods.
This is a wfFormData object. wfFormData functions in every way like wfQueryString, only containing the $_POST data, not $_GET. You can reference it any of the following ways:
This method returns an array containing all of the user-submitted request headers.
This object should be available globally as $response. This class lets you control all response-related output to the client browser including:
This object should be available globally as $session. It is a special object that serves two purposes. First off, it is a MutableMap that is directly referenced to the global session array. Secondly, it has two notable methods that you will use profusely.
This is an instance of wfHttpClient, an object that contains information about a client's IP address, browser software, operating system, and more.
This method will return to you a wfCookieCollection containing the cookies for the user. Each wfCookie object can have anything as its value. It is serialized and gzipped when sent to the client for storage. See the wfCookie and wfCookieCollection API for further details.
Copyright © 2005-2006 Team Wildfire. All Rights Reserved.