A Beginners’ Guide
Contents
- Introduction
- Preparing the Environment
- Defining a Project Domain
- Routing the requests
- Handling requests with actions
- Applying a business logic
- Processing a resulting content
- Conclusion
- More information
Introduction
First of all, welcome to Phoebius framework. We see Phoebius as something that developers needed to produce complex web-services natively featured everything that well-known gurus of software development advised to all of us: principles of cohesion and decoupling by Steve McConnell, patterns by Martin Fowler, and so on and so forth. All this makes a resulting product more stable and less obscured, thus minimizing costs of development, test and maintenance and delivering the pleasure to developers, project managers and customers.
Phoebius framework is the best choice for the basis of long-run web applications that meant to be scalable enough to meet the requirements of extremely-changing real world.
In this tutorial we will develop a classic weblog, introducing you almost every package of the framework and how it makes the life of the developer easier.
Preparing the Environment
After downloading and application stub and
extracting the Phoebius framework source to the corresponding directory, consider
that /www is mapped as web-server root and /tmp has enough
permissions for writing.
Inside this tutorial we consider that an absolute path to our application is
$app (and it is mapped to the root of an application stub), and a path
to the framework core is $base (that points to the
/phoebius directory). Of course, you can locate them in any relation:
$app can be inside $base and $base can be
inside $app; the only requirement is the correct absolute path to the
init script (that is located at $base/etc/app.init.php).
A typical application has the following structure:
/ (aka $app) /cfg /default /config.php /etc /config.php /lib /phoebuis (aka $base) /tmp /var /views /www /index.php
The bootstrapping process of such application is the following:
- the incoming request is routed by a web-server to a file called
front-controller (it is
$app/www/index.phpby default, but this script can be located anywhere you like - just assume that you have specified the correct absolute paths to an init-script); - front-controller loads the framework's init-script
$base/etc/app.init.php; - front-controller loads the application config
$app/etc/config.php. An application config prepares everything that does not depend on an environment where the application runs, e.g. a custom path to a library classes (by default it is$app/lib:$app/var/lib); - front-controller loads the host config (located at
$app/cfg/<slot name>/config.php). A host config can differ over different environments (e.g. test, production, development, etc), therefore it defines a host preset (the verbosity of errors, and more), stores a database connection parameters, tunes a web-server and does much more. The host configs are aggregated into the application slots. The name of an application slot is determined from an environment variable calledPHOEBIUS_APP_SLOT. If not defined, the "default" slot is used.
For our simple blog application, all we need is a single database connection, so our
application config can be left untouched, and the default host config
($app/cfg/default/config.php) can look similar to this:
<?
define ('APP_SLOT_CONFIGURATION', SLOT_PRESET_DEVELOPMENT);
DBPool::add(
'default',
PgSqlDB::create()
->setHost('localhost')
->setUser('postgres')
->setPassword('postgres')
->setName('blog_db')
);
?> Here we have specified a development preset of the host (which maximizes the
application feedback by an error verbosity an detailed logging) and have defined a
default PostgreSQL database (you can specify MySQL swimmingly because the Phoebius
database abstraction layer uses the ANSI-SQL-unified query builder, so you don't
need to write SQL queries manually).
Defining a Project Domain
A development of an application starts with describing the domain with an XML schema, which is then translated to an internal representation (i.e. to a graph of internal objects), called "ORM domain". ORM domain holds the information regarding the application entities, mappings between entities and database schemes, and much more.
So let's define the essences of our application. Our simple weblog is represented by
posts and comments. A Post contains the title, the text, the date, and the set of
Comments left to the post. A Comment contains the author, the text and the post this
comment is assigned to, and the time when the comment was published. In other words,
we have a one-to-many relation. Now let our framework know about our domain.
Firstly, write down the definition in a simple XML schema located at
$app/var/schema.xml:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE domain SYSTEM "../phoebius/share/Orm/Domain/Meta/Xml/abstract.dtd"> <domain db-schema="default"> <entities> <entity name="Post"> <properties> <identifier /> <property name="title" type="String" /> <property name="text" type="String" /> <property name="date" type="Date" /> <container name="comments" type="Comment" /> </properties> </entity> <entity name="Comment"> <properties> <identifier /> <property name="author" type="String" /> <property name="text" type="String" /> <property name="post" type="Post" /> <property name="time" type="Timestamp" /> </properties> </entity> </entities> </domain>For more information, refer to /share/Orm/Domain/Meta/Xml/abstract.dtd.
Then we tell the framework to produce the code according to the definition, by calling the framework generator via the console and passing the absolute path to the directory where the application resides. The real-world example:
$ php /www/phoebius.org/phoebius/bin/make.php --code --schema --db=default --config-file=default var/schema.xml
Now you have a fresh set of autogenerated classes and files, that represent a high-level abstraction over database schemas and framework internals. The most important are:
$app/lib/Domain/Post.class.php, the class representing the Post entity;$app/lib/Domain/Comment.class.php, the class representing the Comment entity;$app/var/db/default.sql, a SQL schema produced for the RDBMS driver you have chosen in a host config and already queried against it. Push it to the database you use.
Routing the requests
Phoebius has a powerful routing package, which can route requests even using the complex conditions and mapping variables to types. Consider, that our blog requires to handle the following types of requests:
/-- the front page/?skip=<skip_count>-- the list of previous posts starting withskip_count, whereskip_countis unsigned integer/post/<post_id>-- a separate page containing post (identified by thepost_id) with associated comments
To route these requests we need to create a router. The simplest way to do that is to create a class that implements a framework's ChainedRouter. Let's name it BlogRouter and locate it at our application library:
$app/lib/BlogRouter.class.php
class BlogRouter extends ChainedRouter
{
function __construct()
{
$this->setDefaultDispatcher(new MvcDispatcher());
$this->fillRoutes();
}
protected function fillRoutes()
{
$this->route(
'skip-posts',
'/?skip',
array('controller' => 'Blog', 'action' => 'skipPost')
);
$this->route(
'show-post',
'/post/:id',
array('controller' => 'Blog', 'action' => 'showPost')
);
$this->route(
'front-page',
'/',
array('controller' => 'Blog', 'action' => 'frontPage')
);
}
}
As we are planning to use Mvc to handle the requests, we set an MvcDispatcher as the default route dispatcher. Dispatcher is an application layer that is responsible for passing the request to the corresponding handler.
ChainerRouter uses natural prioritizing strategy: the firstly-added rule has higher priority than the last one. That's why the route for front page is defined at the end of the table as it uses less greedy set of rules.
Handling requests with actions
In Mvc context, an action is a procedure that is responsible for handling the request and producing the result. Usually, action is a method of the controller class. Our routing policy defined below specifies requests that should be dispatched by MvcDispatcher and handled by the "Blog" controller. Therefore we define a class BlogController with the following actions (consider the naming notation defined by the base ActionBasedController class):
$app/lib/Mvc/Controllers/BlogController.class.php
class BlogController extends ActionBasedController
{
/**
* Request: /
* @return IActionResult
*/
function action_frontPage()
{
}
/**
* Request: /?skip=N
* @return IActionResult
*/
function action_skipPosts(Integer $skip)
{
}
/**
* Request: /post/:id
* @return IActionResult
*/
function action_showPost(Integer $id)
{
}
}
Each action handles the corresponding request, if matched. Noteworthy that
end-developers should not care about incoming parameter types: the framework
internals checks the compatibility of the request by itself. For example, the
request /?skip=20 will be passed to
BlogController::action_skipPosts() but /?skip=abc won't.
Integer here is the defined type.
You can also use types defined in a project domain: Phoebius will use incoming
variable value as the identifier to fetch the object.
Applying a business logic
Now it is time to fill our actions with business logic. We will do this for every action that handles the request.
The front page is simply a page with the latest posts. Therefore we can just make an internal redirection to an action that handles skip-posts request skipping zero posts:
function action_frontPage()
{
return $this->action_skipPosts(new Integer(0));
}
Action, that fetches the list of Post object, uses simple criteria over the entity
query language wrapper -- an EntityQuery class, and puts this list and the number of skipped posts to
a model (a transport between action and view) and return the name of the view, that
is automatically resolved by the action invoker. Notice, that we do not mix action's
business logic with any presentation logic: we pass the result of business logic
away to a view, which itself is responsible for presentation:
function action_skipPosts(Integer $skip)
{
$list =
Post::query()
->setOffset($skip->getValue())
->setLimit(25)
->getList();
);
return $this->view(
'posts',
array(
'posts' => $list,
'skip' => (string) $skip
)
);
}
An action for the page with a separate Post is simple too. We access the Post object by the specified ID and then obtain all the comments for the Post using the EntityQuery criteria:
function action_showPost(Integer $id)
{
$post = Post::dao()->getEntityById((string) $id);
$comments =
Comment::query()
->where(Expression::eq('post', (string) $id))
->getList();
return $this->view(
'post',
array(
'post' => $post,
'comments' => $comments
)
);
}
Note that we reference to the name of the Comment object property ("post"), no
matter how the database column is named.
Processing a resulting content
We have created actions that only apply a business logic, but don't care about the presentation, because in modern three-tier applications we should divide the business and the presentation logic. The result of an action (a model, that itself represents a produced data) is passed to a view. A view is a plain script that produces the markup and injects the passed data within this markup.
A Phoebius UI package is a view engine that offers a wide range of features for easy management of a huge number of different views. It defines three different types of views:
- Master page view
- Content page view
- Control view
For our simple weblog we need to create a common master page, three content pages (the first is for the list of posts; the second is for the select post with comments left to it, and the third one is 404 page) and one control page for composing a menu.
$app/views/master.view.php
...static header...
<?
$this->renderPartial('menu');
$this->getDefaultContent();
?>
...static footer...
$app/views/posts.view.php
<?
$this->setMaster('master');
?>
<h1>Blog posts</h1>
<?
foreach ($this->posts as $post) { ?>
<h1><?=$post->getTitle()?> <small><?=$post->getDate()->toFormattedString()?></small></h1>
<br />
<?=$post->getText()?>
<hr />
<? }
?>
$app/views/post.view.php
<?
$this->setMaster('master');
?>
<h1><?=$this->post->getTitle()</h1>
<?=$this->post->getText()?>
Standard Phoebius distribution requires views to be located inside $app/views
directory. The naming pattern is <view_name>.view.php.
Conclusion
Phoebius framework is modern solution intended to quick building of applications that require self-documenting stratified code of high quality.
More information
Download the AJAX application example or read the manual pages.
