onPHP

Sample application.

 

en / ru

 

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.

powered by `tar xfj /dev/tty`
$Id: examples.Forum.en.html 3205 2007-04-30 20:14:42Z voxus $