Tutorial > let's code !

1 2 3

First, we want to separate the logic of the application from the controller and put it in a model.

Let's create our model "LiveComments" file :

Go in "models" folder and create a file called "class.LiveComments.php".

Files present in "models" folder are automatically included by the module.

Let's start with this :

models/class.LiveComments.php :
<?
class LiveComments
{
 public $database = "module_livecomments.db";
 private $conn;
 
 function __construct()
 {
  $this->conn = new SQLiteDatabase(AH_SITES_DB.$this->database)
  //Of course, this is not a recommended way to proceed in production.
  //This is just to make our example plug&play !
  $this->createDatabaseIfDoesNotExist();
 }
 
 function getComments()
 {
  $result = $this->conn->query("SELECT id, author, message, date FROM comments");
  return $result->fetchAll(SQLITE_ASSOC);
 }
 
 function createDatabaseIfDoesNotExist()
 {
  $result = $this->conn->query("SELECT COUNT(*) FROM sqlite_master");
  if ( $result->fetchSingle() == 0 )
  {
   $schema = $this->getSchema();
   $this->conn->queryExec("BEGIN; $schema COMMIT;");
  } 
 }
 
 function getSchema()
 {
  $schema = <<<SQL
  CREATE TABLE comments (
  message text NULL
  author string NULL,
  date string NULL,
  id INTEGER PRIMARY KEY
  );
  INSERT INTO comments (author, message, date) VALUES ('John Doe', 'Hello, world !', datetime('now'));
SQL;
  return $schema;
 }
}
?>

Change main_controller.php to this :

controllers/main_controller.php :
<?
class MainController_LivecommentsModule extends Controller_Module
{
 public $default_action = 'index';
 // by convention, private variables are prepended with an underscore
 private $_liveComments;
 
 function _init()
 {
  // instantiate the model
  $this->_liveComments = new LiveComments;
 }
 
 function index()
 {
  // ask the model to fetch comments
  $this->comments = $this->_liveComments->getComments();
 }
} 
?>

Change views/main/index.tpl to this :

views/main/index.tpl :
{foreach from=$comments item="comment"}
<hr/>
On {$comment.date|date_format}, {$comment.author|escape} said :<br/>
<pre>
{$comment.message|escape}
</pre>
{/foreach}

Now go back to the draft site and refresh.

Ok, let's add a new action to post a message.

First, create a link to this new action.

In views/main/index.tpl, add this :

{link_to class="st0">"new" text="Post a comment"}

Try to post a new message.

The module complains about not finding the action 'new' nor its corresponding view, so let's add a "new()" method to the controller class and create a new.tpl view in views/main/.

But wait... we have a small problem here. Due to a (sad) limitation of PHP, one cannot name a method "new", nor "list"... because such words are PHP reserved words :-/

Try and you'll get a syntax error from PHP.

Hopefully, alahup! provides a simple workaround. We will call it "new_()" (notice the underscore) and it will just work !

But let's first create a "new.tpl" view in views/main/ :

views/main/new.tpl :
<h3>New comment</h3>
{form}
Author :<br/>
<input name="comment[author]" type="text" value="{$smarty.post.comment.author|escape}"><br/>
Message :<br/>
<textarea name="comment[message]" rows="10" cols="50">{$smarty.post.comment.message|escape}</textarea><br/>
<input type="submit" name="submit">
{/form}

First notice the use of {form}{/form}.

It will insert the expected <form ...></form> tag. You don't have to worry about where to post the form : it will just post the form to the current action.

You'll understand why it's a smart convention when you see the action.

Just another remark before we carry on : maybe you're wondering why we use "comment[author]" instead of just "author" in the <input> tag.

This is just a convention which turns out to be quite handy as you will see.

Here is the new_() method to add to main_controller.php :

function new_() // underscore required because 'new' is a reserved word in PHP
{
 if ( count($_POST) )
 {
 if ( $this->_liveComments->newComment($_POST['comment']) )
 {
 $this->flash['message'] = "Created item.";
 $this->redirect_to('index')
 }
 else
 {
 $this->flash['message'] = join('<br/>', $this->_liveComments->errors);
 } 
 }
}

It is quite self explanatory, provided you add the newComment() method to the LiveComment class.

This is the final models/class.LiveComment.php file :

models/class.LiveComment.php :
<?
class LiveComments
{
 public $database = "module_livecomments2.db"
 public $errors = array();
 private $conn;
 private $thread_id;
 
 function __construct()
 { 
  $this->conn = new SQLiteDatabase(AH_SITES_DB.$this->database)
  // Of course, this is not a recommended way to proceed in production.
  // This is just to make our example plug&play !
  $this->createDatabaseIfDoesNotExist();
 }
 
 function getComments()
 {
  $result = $this->conn->query("SELECT id, author, message, date FROM comments");
  return $result->fetchAll(SQLITE_ASSOC);
 }
 
 function newComment($comment_hash)
 {
  $author = $comment_hash['author'];
  $message = $comment_hash['message'];
  if ( trim($author) == '' )
  {
   $this->errors[] = "Author can't be left blank";
  }
  if ( trim($message) == '' )
  {
   $this->errors[] = "Message can't be left blank";
  }
  if ( count($this->errors) ) return false;
  try
  {
   $this->conn->query("INSERT INTO comments (author, message, date) VALUES ('".sqlite_escape_string($author)."', '".sqlite_escape_string($message)."', datetime('now'))");
  }
  catch (Exception $e)
  {
   $this->errors[] = $e;
   return false;
  }
  return true;
 }
 
 function createDatabaseIfDoesNotExist()
 {
  $result = $this->conn->query("SELECT COUNT(*) FROM sqlite_master");
  if ( $result->fetchSingle() == 0 )
  {
   $schema = $this->getSchema();
   $this->conn->queryExec("BEGIN; $schema COMMIT;");
  } 
 }
 function getSchema()
 {
  $schema = <<<SQL
  CREATE TABLE comments (
  message text NULL
  author string NULL,
  date string NULL,
  id INTEGER PRIMARY KEY
  );
  INSERT INTO comments (author, message, date) VALUES ('John Doe', 'Hello, world !', datetime('now'));
SQL;
  return $schema;
 }
}
?>

Note that we added :

public $errors = array();

at the beginning.

See how the errors are handled :

If newComment() fails, it returns false after having saved the appropriate messages in $this->errors array.

The new_() method in the controller then saves the error messages in the flash and the same view is redisplayed.

If newComment() is successful, we are redirected to the 'index' action.

Notice also that both the author and the message were passed to the model with the 'comment' array() incoming from the POST because of the comment[] trick in the view.

If we added more options in the form, we wouldn't have to change this behavior.

So we have a basic, but functional module.

However, it has a limitation : if we insert it at different places in the site, it will share the same comments.

We don't want that. We want the author to be able to define some sort of thread when it puts the module in the page.

Let's go back to elements/definitions/mod.xml

Add a new property :

objects/mod.xml :
<prop id="thread_id" type="textInput" show="false">
 <caption>Thread ID : </caption>
</prop>

and a new rule inside <rules></rules> :

objects/mod.xml :
<ifequal prop="type" value="livecomments">
 <include prop="thread_id" />
</ifequal>

Click on the little hand to edit the block.

We now have the ability to define a thread ID. Set it to something. "General" for instance and "Apply" the change.

shadow
shadowshadow

Remember, custom properties (such as this new thread_id) can be read template side in $this.custom.

If you add {$this.custom.thread_id} in the mod.tpl template, the value of thread_id will be displayed.

But what we actually want here, is to pass it to the module.

That's easy.

Go to elements/sets/default/mod.tpl and simply change:

templates/_objects/mod.tpl :
{module id="livecomments"}

to

templates/_objects/mod.tpl :
{module id="livecomments" thread_id=$this.custom.thread_id}

Parameters passed to the module that way are available in the module's "param" property.

From the module's class (livecomments_module.php file), you can read it with :

From the module's class : :
$this->params['thread_id'];

From any controller, you can read it with

From a controller's class : :
$this->_module->params['thread_id'];

Let's check this :

In the controllers/main_controller.php, change :

controllers/main_controller.php :
function _init()
{
 // instantiate the model
 $this->_liveComments = new LiveComments;
}

to

controllers/main_controller.php, :
function _init()
{
 $this->thread_id = $this->_module->params['thread_id'];
 // instantiate the model
 $this->_liveComments = new LiveComments;
}

And change the main layout (views/_layout/main.tpl) to

views/_layout/main.tpl :
<h2>Thread ID = {$thread_id}</h2>
{if $flash.message}
 <div style="color: red;">{$flash.message}</div>
{/if}
{$content_for_layout}

Refresh...

shadow
shadowshadow

Now that we are sure that the thread_id parameter can be set by the author and read from the module, it's easy to use it in the controller and the model.

In main_controller.php, change the _init() method to :

main_controller.php :
function _init()
{
 // instantiate the model
 $this->_liveComments = new LiveComments($this->_module->params['thread_id']);
}

The final version of models/class.LiveComments.php is :

models/class.LiveComments.php :
<?
class LiveComments
{
 public $database = "module_livecomments2.db"
 public $errors = array();
 private $conn;
 private $thread_id;
 
 function __construct($thread_id)
 { 
  $this->thread_id = sqlite_escape_string($thread_id)
  $this->conn = new SQLiteDatabase(AH_SITES_DB.$this->database);
  // Of course, this is not a recommended way to proceed in production.
  // This is just to make our example plug&play !
  $this->createDatabaseIfDoesNotExist();
 }
 
 function getComments()
 {
  $result = $this->conn->query("SELECT id, author, message, date FROM comments WHERE thread_id = '$this->thread_id'");
  return $result->fetchAll(SQLITE_ASSOC);
 }
 
 function newComment($comment_hash)
 {
  $author = $comment_hash['author'];
  $message = $comment_hash['message'];
  if ( trim($author) == '' )
  {
   $this->errors[] = "Author can't be left blank";
  }
  if ( trim($message) == '' )
  {
   $this->errors[] = "Message can't be left blank";
  }
  if ( count($this->errors) ) return false;
  try
  {
   $this->conn->query("INSERT INTO comments (thread_id, author, message, date) VALUES ('$this->thread_id', '".sqlite_escape_string($author)."', '".sqlite_escape_string($message)."', datetime('now'))");
  }
  catch (Exception $e)
  {
   $this->errors[] = $e;
   return false;
  }
  return true;
 }
 
 function createDatabaseIfDoesNotExist()
 {
  $result = $this->conn->query("SELECT COUNT(*) FROM sqlite_master");
  if ( $result->fetchSingle() == 0 )
  {
   $schema = $this->getSchema();
   $this->conn->queryExec("BEGIN; $schema COMMIT;");
  } 
 }
 
 function getSchema()
 {
  $schema = <<<SQL
  CREATE TABLE comments (
  message text NULL
  author string NULL,
  date string NULL,
  thread_id string NULL,
  id INTEGER PRIMARY KEY
  );
  CREATE INDEX comments_thread_id on comments (thread_id);
  INSERT INTO comments (thread_id, author, message, date) VALUES ('general', 'John Doe', 'Hello, world !', datetime('now'));
SQL;
 return $schema;
 }
}
?>