|
Criteria
OSQL
DAOs
Form
Cache
Application
Metaconfiguration
|
|
sample Forum application
This example is relevant only for legacy 0.2.x series.
define user's table:
create sequence person_id;
create table person(
id bigint default nextval('person_id') primary key,
nickname varchar(255) not null,
password varchar(40) not null,
mail varchar(128) not null,
created timestamp not null,
last_login timestamp not null,
modified timestamp not null
);
create unique index person_mail_uniq on person(lower(mail));
and it's business class:
final class Person extends IdentifiableObject implements DAOConnected
{
private $nickname = null;
private $password = null;
private $mail = null;
private $created = null;
private $modified = null;
private $lastLogin = null;
public static function create()
{
return new Person();
}
public static function dao()
{
return DAO::person();
}
/* quite boring {get,set}ters goes here */
}
forum's table:
create sequence forum_id;
create table forum(
id integer default nextval('forum_id') primary key,
name varchar(255) not null,
description text null,
created timestamp not null,
modified timestamp not null,
message_count bigint not null
);
forum's class:
final class Forum extends NamedObject implements DAOConnected
{
private $description = null;
private $messageCount = null;
private $created = null;
private $modified = null;
public static function create()
{
return new Forum();
}
public static function dao()
{
return DAO::forum();
}
/* even more boring methods */
}
forum's message table finally:
create sequence message_id;
create table message(
id bigint default nextval('message_id') primary key,
forum_id integer not null references forum(id)
on delete restrict on update cascade,
person_id bigint not null references person(id)
on delete restrict on update cascade,
-- parent is null when top level
parent_id bigint null references message(id)
on delete cascade on update cascade,
-- thread == id, when top level
thread_id bigint not null references message(id)
on delete cascade on update cascade,
name varchar(255) not null,
content text not null,
created timestamp not null
);
and it's business class:
final class Message extends NamedObject implements DAOConnected
{
// since we don't want to pollute our cache
private $forumId = null;
private $parentId = null;
private $threadId = null;
// not reflected at DB in any way
private $count = null;
private $person = null;
private $content = null;
private $created = null;
public static function create()
{
return new Message();
}
public static function dao()
{
return DAO::message();
}
/* ok, this time you'll see all that boring stuff below */
public function getForumId()
{
return $this->forumId;
}
public function setForumId($id)
{
$this->forumId = $id;
return $this;
}
public function getParentId()
{
return $this->parentId;
}
public function setParentId($id)
{
Assert::isTrue(
$this->id === null || ($this->id != $id)
);
$this->parentId = $id;
return $this;
}
public function getThreadId()
{
return $this->threadId;
}
public function setThreadId($id)
{
$this->threadId = $id;
return $this;
}
public function getPerson()
{
return $this->person;
}
public function setPerson(Person $person)
{
$this->person = $person;
return $this;
}
public function getCreated()
{
return $this->created;
}
public function setCreated(Timestamp $created)
{
$this->created = $created;
return $this;
}
public function getContent()
{
return $this->content;
}
public function setContent($blaBlaBla)
{
$this->content = $blaBlaBla;
return $this;
}
public function getChildsCount()
{
return $this->count;
}
public function setChildsCount($count)
{
$this->count = $count;
return $this;
}
}
let's simplify our life by DAO-wrapper:
final class DAO
{
public static function forum()
{
return Singleton::getInstance('ForumDAO');
}
public static function message()
{
return Singleton::getInstance('MessageDAO');
}
public static function person()
{
return Singleton::getInstance('PersonDAO');
}
}
ok, 1 of 3 DAOs:
final class PersonDAO extends MappedStorableDAO
{
// quite obvious mapping
protected $mapping = array(
'id' => null,
'nickname' => null,
'password' => null,
'mail' => null,
'created' => null,
'modified' => null,
'lastLogin' => 'last_login'
);
public function getTable()
{
return 'person';
}
public function getSequence()
{
return 'person_id';
}
public function getObjectName()
{
return 'Person';
}
public function logIn($userName, $passWord)
{
$person =
$this->getByLogic(
Expression::expAnd(
Expression::eq('nickname', $userName),
Expression::eq('password', $passWord)
)
);
return
// sync last login time
$this->save(
$person->setLastLogin(
new Timestamp(time())
)
);
}
public function makeObject(&$array, $prefix = null)
{
return
Person::create()->
setId($array[$prefix.'id'])->
setNickname($array[$prefix.'nickname'])->
setPassword($array[$prefix.'password'])->
setMail($array[$prefix.'mail'])->
setCreated(new Timestamp($array[$prefix.'created']))->
setModified(new Timestamp($array[$prefix.'modified']))->
setLastLogin(new Timestamp($array[$prefix.'last_login']));
}
public function setQueryFields(
InsertOrUpdateQuery $query, Person $person
)
{
$now = new Timestamp(time());
// don't forget about time upon creation
if ($query instanceof InsertQuery)
$query->set(
'created',
$person->setCreated(
$now
)->
getCreated()->toString()
);
return
$query->
// don't forget about time upon any modification too
set(
'modified',
$person->setModified($now)->
getModified()->toString()
)->
set('id', $person->getId())->
set('nickname', $person->getNickname())->
set('password', $person->getPassword())->
set('mail', $person->getMail());
}
}
2 of 3:
final class ForumDAO extends MappedStorableDAO
{
protected $mapping = array(
'id' => null,
'name' => null,
'description' => null,
'created' => null,
'modified' => null,
'messageCount' => 'message_count'
);
public function getTable()
{
return 'forum';
}
public function getSequence()
{
return 'forum_id';
}
public function getObjectName()
{
return 'Forum';
}
// allow remote uncaching
public function uncacheById($id)
{
return parent::uncacheById($id);
}
public function makeObject(&$array, $prefix = null)
{
return
Forum::create()->
setId($array[$prefix.'id'])->
setName($array[$prefix.'name'])->
setCreated(new Timestamp($array[$prefix.'created']))->
setModified(new Timestamp($array[$prefix.'modified']))->
setMessageCount($array[$prefix.'message_count'])->
// optional
setDescription(
isset($array[$prefix.'description'])
? $array[$prefix.'description']
: null
);
}
public function setQueryFields(InsertOrUpdateQuery $query, Forum $forum)
{
$now = new Timestamp(time());
if ($query instanceof InsertQuery) {
$query->set(
'created',
$forum->setCreated($now)->
getCreated()->toString()
);
$forum->setMessageCount(0);
}
return
$query->
set('id', $forum->getId())->
set('name', $forum->getName())->
set('description', $forum->getDescription())->
set('message_count', $forum->getMessageCount())->
set(
'modified',
$forum->setModified($now)->
getModified()->toString()
);
}
}
final and most interesting:
final class MessageDAO extends MappedStorableDAO
{
protected $mapping = array(
'id' => null,
'name' => null,
'forumId' => 'forum_id',
'personId' => 'person_id',
'parentId' => 'parent_id',
'threadId' => 'thread_id',
'created' => null
// 'content' hided from default fields/mapping
);
public function getTable()
{
return 'message';
}
public function getBodyTable()
{
return 'message_body';
}
public function getObjectName()
{
return 'Message';
}
// wrappers
public function getById($id, $full = false)
{
$query =
$this->makeSelectHead()->
where(
Expression::eq(
DBField::create('id', $this->getTable()),
$id
)
);
if ($full)
$query->get('content');
return $this->getByQuery($query);
}
public function getList(ObjectQuery $oq, $full = false)
{
$query = $oq->toSelectQuery($this);
if ($full)
$query->get('content');
return $this->getListByQuery($query);
}
public function makeObject(&$array, $prefix = null)
{
return
Message::create()->
setId($array[$prefix.'id'])->
setName($array[$prefix.'name'])->
setCreated(new Timestamp($array[$prefix.'created']))->
setForumId($array[$prefix.'forum_id'])->
setPerson(
DAO::person()->getById(
$array[$prefix.'person_id']
)
)->
setContent(
isset($array[$prefix.'content'])
? $array[$prefix.'content']
: null
)->
// null when top level
setParentId(
isset($array[$prefix.'parent_id'])
? $array[$prefix.'parent_id']
: null
)->
setThreadId(
isset($array[$prefix.'thread_id'])
? $array[$prefix.'thread_id']
: null
)->
setChildsCount(
isset($array[$prefix.'count'])
? $array[$prefix.'count']
: null
);
}
public function setQueryFields(
InsertOrUpdateQuery $query, Message $message
)
{
if ($query instanceof InsertQuery)
$query->set(
'created',
$message->setCreated(new Timestamp(time()))->
getCreated()->toString()
);
return
$query->
set('id', $message->getId())->
set('name', $message->getName())->
set('content', $message->getContent())->
set('forum_id', $message->getForumId())->
set('person_id', $message->getPerson()->getId())->
set('parent_id', $message->getParentId())->
set('thread_id', $message->getThreadId())->
set('created', $message->getCreated()->toString());
}
public function getThreadList(ObjectQuery $oq)
{
return
$this->getQueryResult(
$oq->toSelectQuery($this)->
get(
OSQL::select()->from($this->getTable(), 'sub')->
get(
Expression::sub(
SQLFunction::create('count', 'id'),
new DBValue(1)
)
)->
where(
Expression::eq(
new DBField('thread_id', $this->getTable()),
new DBField('thread_id', 'sub')
)
)->
setName('count')
)->
andWhere(
Expression::isNull('parent_id')
)
);
}
protected function inject(
InsertOrUpdateQuery $query, Identifiable $message
)
{
Assert::isTrue($message instanceof Message); // paranoia
// we should update 'modified' and 'message_count' there
$forumQuery = OSQL::update(DAO::forum()->getTable());
if ($query instanceof InsertQuery) {
if ($message->getThreadId() === null)
$message->setThreadId(
$message->getId()
);
// increase message counter upon message addition
$forumQuery->
set(
'message_count',
Expression::add(
new DBField('message_count'),
new DBValue(1)
)
);
}
// modification time should be set
// on every message-realted action
$forumQuery->
set('modified', SQLFunction::create('now'));
// go
Transaction::immediate(DBFactory::getDefaultInstance())->
add(
// main query
$this->setQueryFields(
$query->setTable($this->getTable()),
$message
)
)->
add(
// forum' update query
$forumQuery
)->
flush();
// drop all message-related lists from cache
$this->dropLists();
// uncache message's forum object
DAO::forum()->uncacheById($message->getForumId());
// uncache message, if any
$this->uncacheById($message->getId());
return $message;
}
}
so here goes our business logic (all view-templates are omitted), list of available forums:
final class forumList extends BaseModule
{
protected $plainList = array();
public function process()
{
try {
$this->forumList = DAO::forum()->getPlainList();
} catch (ObjectNotFoundException $e) {
// no forums created yet
}
return $this;
}
/* don't forget to include your favorite template at dump() */
}
current forum threads list with ability to add new threads:
final class threadList extends FormedModule
{
const PER_PAGE = 50;
protected $forum = null;
protected $threadList = null;
public function init()
{
$this->form->
add(
Primitive::boolean('post')
)->
add(
Primitive::integer('forumId')->required()
)->
add(
Primitive::integer('page')->setDefault(1)
)->
import($_GET);
// forum's id is missing or invalid
if ($this->form->getErrors())
return $this->blowOut();
// in case we're awaiting new message arrival
$this->form->
add(
Primitive::string('name')->setMax(255)->required()->
addImportFilter(Filter::textImport())->
addDisplayFilter(Filter::htmlSpecialChars())
)->
add(
Primitive::string('content')->setMax(40960)->required()-> // ~40Kb
addImportFilter(Filter::textImport())->
addDisplayFilter(Filter::htmlSpecialChars())
)->
importMore($_POST);
return $this;
}
public function process()
{
$form = $this->form;
try {
$this->forum = DAO::forum()->getById(
$form->getValue('forumId')
);
} catch (ObjectNotFoundException $e) {
// forum with such id does not exist
return $this->blowOut('forumList');
}
// check POST-trigger
if ($form->getValue('post')) {
// imaginary class to check user's permissions to post
if (!SecurityManager::isLoggedIn())
return $this->blowOut();
// if there are any errors,
// user should correct them first
if ($form->getErrors())
return $this;
// ok, let's build new object
$message =
Message::create()->
setPerson(
SecurityManager::getUser()
)->
setForumId(
$this->forum->getId()
)->
// parent should be null
// thread will be set at DAO
setName(
$form->getValue('name')
)->
setContent(
$form->getValue('content')
);
DAO::message()->add($message);
// go back to thread's list
HeaderUtils::redirect(
$this->setParameters(
array(
'forumId' => $this->forum->getId()
)
)
);
return $this;
} else
// there was no POST-trigger,
// so assume, there were no input
$form->dropAllErrors();
try {
// let's get this forum's threads list
// sorted by creation time
$this->threadList =
DAO::message()->getThreadList(
ObjectQuery::create()->
addLogic(
Expression::eqId('forumId', $this->forum)
)->
sort('created')->desc()
);
} catch (ObjectNotFoundException $e) {
// newly created, probably
}
return $this;
}
/* don't forget to include your favorite template at dump() */
}
single thread view with ability to {un,}fold messages and to post replies:
final class threadView extends FormedModule
{
// current forum
protected $forum = null;
// current viewed message
protected $message = null;
// all messages list
protected $messageList = null;
public function init()
{
$this->form->
add(
Primitive::integer('id')->required()
)->
add(
Primitive::boolean('post')
)->
add(
Primitive::boolean('unfold')->setDefault(false)
)->
import($_GET);
// same logic as in threadList module
if ($this->form->getErrors())
return $this->blowOut('forumList');
$this->form->
add(
Primitive::string('name')->setMax(255)->required()->
addImportFilter(Filter::textImport())->
addDisplayFilter(Filter::htmlSpecialChars())
)->
add(
Primitive::string('content')->setMax(40960)->required()-> // ~40Kb
addImportFilter(Filter::textImport())->
addDisplayFilter(Filter::htmlSpecialChars())
)->
importMore($_POST);
return $this;
}
public function process()
{
$form = $this->form;
try {
$this->message = DAO::message()->getById(
$form->getValue('id'),
// get content, if user requested so
$form->getActualValue('unfold')
);
} catch (ObjectNotFoundException $e) {
// there is no such message
return $this->blowOut('forumList');
}
// POST-trigger
if ($form->getValue('post')) {
// imaginary class again
if (!SecurityManager::isLoggedIn())
return $this->blowOut();
if (!$form->getErrors()) {
// build full message object
$message =
Message::create()->
setPerson(
SecurityManager::getUser()
)->
setForumId(
$this->message->getForumId()
)->
setParentId(
$this->message->getId()
)->
setThreadId(
$this->message->getThreadId()
)->
setName(
$form->getValue('name')
)->
setContent(
$form->getValue('content')
);
// save it
$message = DAO::message()->add($message);
// finally redirect to it's view
HeaderUtils::redirect(
$this->setParameters(
array(
'id' => $message->getId()
)
)
);
return $this;
}
} else
// there was no user input
$form->dropAllErrors();
try {
// get current thread message's list
$list = DAO::message()->getList(
ObjectQuery::create()->
addLogic(
Expression::eq(
'threadId',
$this->message->getThreadId()
)
),
$form->getActualValue('unfold')
);
// build simple tree to simplify it's displaying
foreach ($list as &$message) {
if (($parentId = $message->getParentId()) === null)
$this->messageList[0][] = $message;
else
$this->messageList[$parentId][] = $message;
}
} catch (ObjectNotFoundException $e) {
// such thread does not even exist
return $this->blowOut('forumList');
}
return $this;
}
/* don't forget to include your favorite template at dump() */
}
that's really so simple. you're almost onPHP's guru now, congratulations.
|