Miniblog project

From CMC for PHP
Jump to: navigation, search

Installing the miniblog fileset

Here is the miniblog repository: github.com/Calmarsoft/cmcphp-demoblog

The current fileset is the following:

  • cache/, a writable directory for holding session and caches files
  • js/cmc.js, the javascript part of the framework,
  • js/javascript-xpath.min.js, a javascript dependency, which allows xpath DOM selection in javascript
  • php/cmc.phar, the compressed version of the framework
  • material/ a directory with the HTML materials of the application's views
  • program/ a directory with "main" files: myApp.php (application), and config.php (configuration)
  • program/control/ a directory with the implemented frames: register.php, login.php, newpost.php, postList.php, postdetail.php
  • program/backend/ a directory with ui independent php sources: userAuth.php
  • index.php, the application/website entry point
  • cmc-blog.sql, a script for creating the database schema (in MariaDB/MySQL)

As we can see the files are organized a different way compared to the skeleton; this was done here for illustrating that the framework let everyone organize the project's items as they wish.

This miniblog application as the following features:

  • register and login form with validity control
  • post list view with insert link
  • post detail with delete/update buttons
  • handle the views depending of the 'login' state

It can be tested by just uncompressing in a folder of the web server, installing the MySQL database utins cmc-blog.sql script, and updating program/config.php file.

Or tested directly here: cmc.calmarsoft.com/cmcphp/test/cmc-blog

Implementation step by step

Use the skeleton as source

We uncompress the skeleton and reorganize the files:

  • we put all material, including the default page in a dedicated folder 'material/'
  • we put all php sources, expect the entry point in a 'program/' folder.
  • then we update config.php and index.php file to reflex those changes:

for index.php (it must find the application sources, and cmc.phar file):

ini_set('include_path', 'program:php:/usr/lib/pear');

(we did also change the namespace to 'cmcblog')

for 'program/config.php', the 'index', and 'site' constant values, the database connection parameters, and the config::MAT_path constant:

const site = 'cmcblog'; // our application name
const index = 'postlist';  // the main view name
[...]
            'database' => site,       // in local, base is project name
            'login' => site,
[...]
    const my_dft_Path = index;
    // 'material' location i.e. the HTML part
    const MAT_path = 'material/';

(here we don't change the namespace, that must remain 'cmc')

The register and login views

create html views

Those views are at first just standard HTML forms, with only addition of a 'data-cmc-id' attribute used to make a link with the PHP code.

For the 'register' view extract:

        <div data-cmc-id="register">            
            <form action="" method="POST"> 
                <fieldset> 
                    <h3>Please enter registering information</h3>
                    <span> <label>Username:</label> </span><input type="text" id="r_name"/><br/> 
                    <span> <label>Email:</label> </span><input type="text" id="r_email"/><br/> 
                    <span> <label>Password:</label> <input type="password" id="r_pass_1"/> </span><br/> 
                    <span> <label>Confirm password:</label> <input type="password" id="r_pass_2" /> </span><br/> 
                    <label>Yes, I have read and agree the terms of use</label><input type="checkbox" id="r_conditions"/> <br />
                    <input id="register" type="submit" value="Register" /><br /><br /> <span id="errortext">Sorry, this service is currently unavailable; please try again later</span> 
                </fieldset> 
            </form>
       </div>

Files are material/register.html and material/login.html

This is the minimal, and can be improved to allow correct display and login even without javascript enabled.

implement the 'frame' for the views

code prepare

There are two control classes: one for login the other for register. Copy skelFrame.php into the frame of your choice and change:

  • the namespace
  • the className (of course)
  • the values returned by getId() and getName(). Remember: getId() must match data-cmc-id attribute of the view piece that will be bound to the class.
widget binding

Then replace the components in the '$_widgetdef' array using appropriate factories and id:

    protected $_widgetdef = array(
        'bt_register' => array(button::factory, 'register'),
        'name' => array(input::factory, 'r_name'),
        'email' => array(input::factory, 'r_email'),
        'pass1' => array(input::factory, 'r_pass_1'),
        'pass2' => array(input::factory, 'r_pass_2'),
        'cond' => array(checkbox::factory, 'r_conditions'),
        'errortext' => array(label::factory, 'errortext'),
    );

Finally define the validity control settings through the 'intialUpdate' method:

    public function viewInitialUpdate($view) {
        $this->w('name')->addValidation('nonEmpty');
        $this->w('pass1')->addValidation('numChars', 6,24);      
        $this->w('pass2')->addValidation('equals', 'pass1');
        $this->w('email')->addValidation('email');
        $this->w('cond')->addValidation('True');
    }

Above is the code for the register frame. You can see the use of several prepared validity functions (valid email, equality to other field, ...). Those rules will be automatically tested on client (and planned also on server, but this is not implemented yet)

add event

Now we need to add event binding and implement; here this is button 'click':

    public function viewInitialUpdate($view) {
        [...]
        $this->AddClickEvent('bt_register', array($this, 'btRegister'));
    }

    public function btRegister($view) {
        // auth object
        $auth = \cmc\sess()->getUserAuth();
        // get input fields
        $user = $this->w('name')->getValue();
        $pass = $this->w('pass1')->getValue();
        $email = $this->w('email')->getValue();

        $auth->do_register($user, $pass, $email);
        if (!$auth->is_logged())
            $text = "Registration failed";
        else {
            $text = 'Welcome, ' . $auth->userName();
            // redirect to main view
            $view->setRedirect('/postlist', false);
        }
        // set result text
        $this->w('errortext')->setCaption($text);
        $view->setRedirectBack('/postlist');
    }

Above is the implementation for the registration. It uses a separate class for checking the registration process, and then updates the label and set redirections.

add client validity control

Here we demonstrate a 'real-time validity' control scheme: it checks the validity while the data is entered. If the global validity is changing it can call a user-defined function in order to change the display (in our case: ungray a button).

    public function viewInitialUpdate($view) {
        $this->clientSetValidationCB('register.validChange', 300);
        [...]

It means that it will call 'register.validChange', within a granularity of 300ms. Then the view must be updated like follows:

            var register = function() {
                return {
                    // called by framework when validation state is changing
                    validChange: function(state, event, f) {
                        if (state) {
                            f.w('bt_register').enable();
                        } else {
                            f.w('bt_register').disable();
                        }
                    }
                };
            }();

Note the 'f.w('bt_register')': it means widget 'bt_register' of current frame (the one which is related to the event). Also note that the widget is identified by its 'appliation' id, and not the HTML id.

In addition, the 'cmc-valid' and 'cmc-invalid' HTML classes are add/removed for each component that becomes valid or invalid. This allows to have the display changing while an entry becomes valid.

The postlist view

There will be some differences compared to the 'login' and 'register' views:

  • it must change display depending on the 'login' status
  • there is a list component

update datasource info

For the list we will need a query that returns the posts. We can add it in the application query referential:

class MyDataEnv extends dataenv {

    // the direct queries
    private $_myQueries = array(
        // this one is the post complete list
        'postlist' => 'SELECT p.id as id, u.id as uid, title, body, name AS author, modified FROM `posts` p 
                                                          INNER JOIN `user` u WHERE p.author = u.id order by created desc',

Then the query will be simply referenced later by its name.


setup the html view

The postlist view interesting part is as follows:

        <div id="cnBox">
            <div id="cn-on" class="user-auth">
                Welcome, <span id="user"></span>
                <a href="postlist?logout">logout</a>
            </div>
            <div id="cn-off" class="user-ano">                      
                <a href="login">sign-in</a>
                <a href="register">register</a>
            </div>
        </div>
        <div>
            <h2>Latest posts:</h2>
            <div>
                <p class="user-auth"><a href="addpost">New post</a>

                <div id="postlist">
                    <div><h3><a href="detail/n">a post</a></h3>
                        Modified on <span> </span> by <span> </span>
                        <div>Text...</div>            
                    </div>
                </div>
            </div>

        </div>

If we display this HTML source directly, we will see the layout of ONE post, and the display that we plan to do with login 'on' and 'off'. We plan to 'remove' the items with 'user-auth' class, and hide/show items with id='cn-off' and id='cn-on' depending on logon status.

the frame widgets

We will use "area" widgets for hide/show items, and also a "compositelist" widget for the post list itelf.

        'cn-on' => array(area::factory, 'cn-on'),
        'cn-off' => array(area::factory, 'cn-off'),
        'user' => array(label::factory, 'user'),        
        'posts' => array(compositelist::factory, 'postlist'),
the composite list

We need to specify additional information to tell how the data is filled in the post list:

    private $postElems = array(
        'h3/a' => 'title',
        'h3/a/@href' => 'detail/${id}',
        'span[2]' => 'author',
        'div' => '!body',
        'span[1]' => 'modified',
    );
    public function viewInitialUpdate($view) {
        $this->w('posts')->setCompositeMap($this->postElems, array('id'));  
    }

The setCompositeList takes an associative array keyed by the 'local' xpath of the item linked to data items.

The xpath expression is relative to the first child element of the 'model', where the component is linked to (here id='postList'). This means that the id of the component must be placed as parent of a 'line' model.

By the way we have a '
' list here, but we can handle a table list in the same way. The id of the component will have to be placed on the top of the lines (so in the tbody tag).

If you encounter difficulties in finding the correct xpath expression, you can use the 'dump_LineElems' developer function like:

$this->w('posts')->dump_lineElems($view);

The 'value' is the fieldname by default, except if there is a ${} expression in the string. The '!' prefix allows HTML content to be NOT escaped (by default html code will safely appear as HTML source)

Here we have a REST compatible link defined by the expression 'detail/${id}' (will link to the 'detail' view with 'id' as REST parameter)

Then the data contents itself can be specified as follows:

public function viewUpdate($view, $sess) {
    $this->w('posts')->setDataQuery($sess, 'postlist');
}

'postlist' is the query name that we have defined in the application.

the components visibility

Here we used two different methods:

  • item hide/show:
$log = $auth->is_logged();
        $this->w('cn-on')->setVisible($log);
        $this->w('cn-off')->setVisible(!$log);
  • item removal:
if (!$log)
            $this->viewRemoveElements($view, 'user-auth');

item hide/show can apply on any widget; here we used the simplest one: 'area'. the 'removeElements' method takes a class name so can be used to remove any item with that class.

The newpost view

Here we will see how to use the 'table' component.

  • it must change display depending on the 'login' status
  • there is a list component

update datasource info

Here we will use a 'table' for adding data.

We can add it in the application table referential:

class MyDataEnv extends dataenv {
[...]
    private $_myTables = array(
        'user' => array(), // table of users
        'posts' => array(), // table of posts
    );

Then the table will be simply referenced later by its name.


the frame widgets

Nothing special here: just input, label and button components

        'bt_update' => array(button::factory, 'update'),
        'title' => array(input::factory, 'title'),
        'body' => array(input::factory, 'body'),
        'errortext' => array(label::factory, 'errortext'),
inserting data on click

Here is the code that is used:

    public function btUpdate($view) {   
        $auth = \cmc\sess()->getUserAuth();
        
        $db = \cmc\sess()->getDataEnv()->getConnectedDB();
        if ($db) {
            $table = $db->gettable('posts');
            if ($table) {
                try {
                    $table->insertData(array(
                        'author' => $auth->userId(),
                        'title' => $this->w('title')->getValue(),
                        'body' => $this->w('body')->getValue(),
                        'modified' => date("Y-m-d H:i:s")
                            )
                    );
                } catch (\cmc\db\DatabaseException $e) {
                    return false;
                }
            }
        }           
        $view->setRedirect('/postlist', false);
    }

We see here that we use our 'auth' class to have the user ID, and the component to have other values. Then we just call the 'insertData' method, with the field and values in an array.

The postdetail view

This is the longer code of this project, because it handles 'preview' and update/delete actions on the same view. However, the code itself is quite simple.

The HTML source has two parts:

  • the 'view' mode, done by html text
  • the 'update' mode done by input tags

This is why there is for example are body and in_body items.

items visiblity

The items visibility is handled by a 'UpdateMode' function that shows or hides item depending on:

  • if user is logged
  • if an update is in motion (user clicked on update, and we wait for 'valid' or cancel' click)


REST handling

The REST handling is quite simple: just call the sess()->getParms() method. It will handle standard parameters (like ?param=value) and REST entries. The REST entries will appear as numerical entries while other will be string associated.

    public function currentId() {
        $params = sess()->getParams();
        $id = $params[0];
        return $id;
    }

So if URL is http://myhost/mypath/detail/12, $id will contain '12'.

update handling

The update button is implemented has follows:

    public function btUpdate($view) {
        $r = $this->getCurrentPost($view);
        if (!$r || !$this->postOwner)
            return;
        $this->w('in_title')->setValue($r['title']);
        $this->w('in_body')->setValue($r['body']);
        $this->UpdateMode(true);
    }

We call a local method to seek the current post, and then we fill the input items with actual value. Finally we update the visibility of items.

update 'post' handling

When validating, we need to perform the update and refresh the values back in 'view' mode:

    public function btSave($view) {
        $this->getCurrentPost();
        $id = $this->currentId();
        // post valid and owned by user
        if ($id && $this->postOwner) {
            $table = dataenv()->getQueryDS('posts');
            $table->updateData(array('id' => $id,
                'title' => $this->w('in_title')->getValue(),
                'body' => $this->w('in_body')->getValue(),
                'modified' => date("Y-m-d H:i:s")
            ));
            // reads data back...
            $this->readPost($view);
        }
    }

the 'delete' button: local dialog

The 'delete' button is special: we want a client side confirmation before having the event sent back to the server. To allow this we use two event binding methods (one on client the other on the server):

        $this->clientAddClickCB('bt_del', 'app.confirmdel');
        $this->AddEventListener('click', 'bt_del', array($this, 'btDelete'));

Note that we don't use 'AddClickEvent' but 'AddEventListener'. This is because we don't want to add an automatic POST event on the client; we want to be notified on client, but the event will manually be trigered by the custom Javascript code:

var app = function() {
                return {
                    // client side deletion confirm
                    confirmdel: function(event) {
                         $("#dialog-confirm").dialog({ resizable: false, height: 160, modal: true,
                            buttons: {
                                "Delete this post": function() {
                                    // explicit event propagation -> continue process
                                    cmc.eventpost(event);
                                    $( this ).dialog( "close" );
                                },
                                Cancel: function() {
                                    $( this ).dialog( "close" );                               
                                }
                            }
                        });
                    }
                };
            }();
We see that if the user clicks on "Delete this post", we trigger an 'eventpost' to propagate the event on the server an perform the deletion.