Wednesday, August 10, 2016

Near Real-Time Updates

For a browser-based application that we produced last year, we found that our end-users were spending a lot of time on the screen - or getting called away before hitting "submit" - and coming back to timed-out sessions.  As users of Google Apps for Government, our folks have gotten used to never having to hit a "save" button and, although we extended the time-out periods, it became something that I wanted to solve before the next release.

The obvious choice was AJAX, but none of us had ever mixed AJAX and CakePHP.  Here's what I worked out to address the issue.  The example that I'll be using is a simple employees table, defined as follows:

CREATE TABLE employees (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    first_name VARCHAR(40) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    street_address VARCHAR(60),
    city VARCHAR(40),
    state VARCHAR(2),
    zip_code INT,
    department_id INT NOT NULL,
    user_id INT NOT NULL,
    created DATETIME DEFAULT NULL,
    modified DATETIME DEFAULT NULL
);

Note that I have foreign keys to a departments and a users table; the users table is drawn from the CakePHP Authentication and Authorization tutorial, and the departments table is defined as follows (if you wish to include it; it won't be used for this post):

CREATE TABLE departments (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    shortcode varchar(4),
    name varchar(50) NOT NULL,
    created DATETIME DEFAULT NULL,
    modified DATETIME DEFAULT NULL
);

After baking these tables and setting up authentication (the latter is not necessary for this exercise), my first changes were at the template level.  I decided to do the employees update first, as the add requires extra logic and I wanted to put that part off while I worked out the main functionality.

In src/Template/employees/edit.ctp, I added the following code to every keyable form field:

<?= $this->Form->create($employee) ?>
<fieldset>
    <legend><?= __('Edit Employee') ?></legend>
    <?php
        echo $this->Form->input('first_name', array(
            "onblur" => "ajaxUpdate(this.id, this.value, $employee->id)"));
        echo $this->Form->input('last_name', array(
            "onblur" => "ajaxUpdate(this.id, this.value, $employee->id)"));
        echo $this->Form->input('street_address', array(
            "onblur" => "ajaxUpdate(this.id, this.value, $employee->id)"));
...

The JavaScript onblur action fires whenever a form field loses focus, so when the user tabs out of a field or mouse-clicks into another field, the JavaScript function ajaxUpdate will be called.  We're sending three parameters to ajaxUpdate:

  • this.id - the HTML "id" of whatever field we're in; e.g. first-name, last-name, etc.
  • this.value - the contents of the field.
  • $employee->id - the primary key of the row we're editing; CakePHP automatically sets this value in the controller for reference by the views (templates).
Also, at the end of my edit.ctp, I added a status field for my own debugging and feedback purposes:

    <?= $this->Form->end() ?>
    <p>Status: <span id="txtStatus"></span></p>
</div>

In webroot/js, I created a file called ajax.js (I have never been the most imaginative with naming!) which contains the following code function:

function ajaxUpdate(fieldName, fieldValue, rowId) {
     var xmlhttp = new XMLHttpRequest();
     if (fieldValue.length == 0) {
         var param2 = fieldName + "=NULL";
     } else {
         var param2 = fieldName + '=' + fieldValue;
     }
     var param1 = rowId;
     xmlhttp.onreadystatechange = function() {
         if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
             document.getElementById("txtStatus").innerHTML = xmlhttp.responseText;
         }
     };
     var postURL = "/ajaxcake/employees/ajax/" + param1 + "/" + param2;
     xmlhttp.open("POST", postURL, true);
     xmlhttp.send();
 }

Here we're doing a bit of validation (I am using the word "NULL" when a field is empty) and constructing a CakePHP-usable URL.  My application is storied in the folder "ajaxcake," we're invoking the employees controller, and my method is called "ajax".

On a normal form submit, we'd get the row ID in the URL, and we'd get a request/response object containing the entire row: field names, and all contents whether changed or not.  Here, though, the server is only going to get what we send it, and - because HTTP is stateless - the server won't know what row we're editing or which column needs changing unless we tell it.  That's why we're sending RowID as the first URL parameter and the field we're updating along with its new value (e.g. "first-name=Joe" or "street-address=1234 Main St.") as the second URL parameter.

Our new XMLHttpRequest (xmlhttp) needs a function that will execute on response; that is what is being prepared by xmlhttp.onreadystatechange.  Mostly for debugging purposes, I'm sending status information back to the textStatus to confirm the updates.

If param2 has spaces, that's okay; the browser will fill it in with space codes (%20) and CakePHP will translate them back.

(Credit to W3 Schools for the basis of my AJAX code!)

Okay, so we're almost there.  Now we just need the method in our employees controller that will accept the URL that we've created and called in our Javascript function.  Here is is:

/**
     * Ajax method
     *
     * @param string|null $id Employee ID.
     * @param string|null $field Field Value.
     * @return string|null.
     */
    public function ajax($id = null, $field = null)
    {
        $this->autoRender = false;
        $fieldSplit = explode('=', $field, 2);
        $fieldToUpdate = str_replace('-', '_', $fieldSplit[0]);
$employee = $this->Employees->get($id, [ 'contain' => [] ]); if ($this->request->is(['post', 'get'])) { $employee->$fieldToUpdate = $fieldSplit[1]; if ($this->Employees->save($employee)) { echo 'Updated ' . $id . '.' . $fieldToUpdate . ' is now ' . $fieldSplit[1]; } else { echo 'Failed to update ' . $id . '.' . $field; } } else { echo 'Update code bypassed.'; } }

Stepping through, we first turn off autoRender, as CakePHP by default will try to find and render an ajax.ctp at the end of this method, and we don't want to render anything new.  We explode the second parameter so that instead of, for example, "street-address=1234 Main St." we instead get the $fieldSplit array with "street-address" and "1234 Main St.".  Next we replace any dashes that came from the HTML "id" with underscores, to match Cake's database naming conventions.

It's pretty standard CakePHP from here on out; we get the employee by it's id and then we replace the contents of the employee row with the new contents of the field we're updating.  PHP allows for the use of variable variables and that's what we're doing with:

$employee->$fieldToUpdate = $fieldSplit[1];

$fieldToUpdate contains the underscored field name that we're updating, and by expressing it in this context with its dollar-sign, the new data ($fieldSplit[1]) ends up in the $employee object in the appropriate column.

All that's left is the persist the data and then echo a message that will go back to our temporary debugging/status field on the screen (textStatus).  What we set out to achieve has taken place without  the user having pressed a button and with no concentration-disrupting refresh of the screen.

With thousands or tens of thousands of users, I have no idea how this would scale; my organization deals with relatively small numbers.  But in our tests, users tab happily from field-to-field at a normal pace, their data always saved up-to-the-minute... even if they never hit the submit button at the bottom of the form.

Sunday, January 31, 2016

AFK

I've been lax in posting lately because I've been - among other things - learning, discovering, and playing with CakePHP too much to take time to stop and write about it.

Back soon with lotsa new, cool stuff.

Friday, January 1, 2016

A CakePHP Contact Form

Contact forms... simple, right?  In CakePHP, it's not hard, but it requires a couple of techniques that seemed worth a post.  Because a contact form normally collects data that we're not going to store in a database, a modelless form is a good choice.  And since we'll be emailing the contact info, we have to configure our CakePHP app to send email.

I'll be using the "CakeHRMS Tutorial" application for my contact form; the app code (with the addition of the contact form) is available via my GitHub repository.

Configuring CakePHP for Email


In this example, I configured my CakePHP app to use SMTP via my web host.  Chances are, for most production applications, you'd want something similar.

Edit config/app.php and find the "EmailTransport" section:

    'EmailTransport' => [
        'default' => [
            'className' => 'Smtp',
            // The following keys are used in SMTP transports
            'host' => 'mail.my.domain',
            'port' => 26,
            'timeout' => 30,
            'username' => 'myaddress@my.domain',
            'password' => 'myPassword',
            'client' => null,
            'tls' => null,
        ],
    ],

You'll need to look at your web host's email configuration information in order to fill this in.  The "host" field is your web hosting account's SMTP server address, the "port" as required for connecting to is, and the username and password are for an email address that you've configured through your web host.

The next block in config/app.php is "Email":

    'Email' => [
        'default' => [
            'transport' => 'default',
            'from' => 'mySenderAddress@my.domain',
            //'charset' => 'utf-8',
            //'headerCharset' => 'utf-8',
        ],
    ],

This sets the "from" address for "default," the transport that we configured in the previous block.  You can configure multiples of both "EmailTransport" and "Email" which allows you the flexibility in your application to use different addresses, web hosts, etc.

Our app is ready to send emails; now we need to give the user a way to interface with it.

Adding a Modelless Form


A modelless form is a form that isn't associated with a database table.  Most forms are used to manipulate data that we store in a database; that's what we get out of the "bin/cake bake" process.  In this case, however, there's no "contact" table, hence no model (i.e. no table or entity).

My contact form is very similar to the one found in the CakePHP documentation on modelless forms.  The code for the form lives in src/Form, a directory that doesn't exist until you create it.  Let's take a look at the code:

<?php
// in src/Form/ContactForm.php
namespace App\Form;

use Cake\Form\Form;
use Cake\Form\Schema;
use Cake\Validation\Validator;
use Cake\Mailer\Email;

class ContactForm extends Form
{

    protected function _buildSchema(Schema $schema)
    {
        return $schema->addField('name', 'string')
            ->addField('email', ['type' => 'string'])
            ->addField('body', ['type' => 'text']);
    }

    protected function _buildValidator(Validator $validator)
    {
        return $validator->add('name', 'length', [
                'rule' => ['minLength', 10],
                'message' => 'Please enter your name'
            ])->add('email', 'format', [
                'rule' => 'email',
                'message' => 'Please enter a valid email address',
            ])->add('body', 'length', [
                'rule' => ['minLength', 25],
                'message' => 'Please enter your message text',
            ]);
    }

    protected function _execute(array $data)
    {
        $email = new Email();
        $email->profile('default');
        
        $email->from([$data['email']])
            ->to('my.address@my.domain')
            ->subject('Web Site Contact Form')
            ->send([$data['body']]);
  
        return true;
    }
}

In function _buildSchema, we define the fields that we want to use for our form; we're going to collect the sender's name ("name"), his or her email address ("email"), and then the text of their message ("body").

In _buildValidator we set validation rules for our fields and create messages to explain errors to our users.

Finally, _execute will send the email for us.  Our email object ($email) is an instance of the Email class, and we're telling it to use the "default" profile that we set up in config/app.php.  We'll send the email using the "from" address and body text submitted via the form.

The Controller


We'll need a controller for our contact form.  Remember "convention over configuration" -- since our form is "ContactForm," our controller should be ContactController:

<?php
// In a controller
namespace App\Controller;

use App\Controller\AppController;
use App\Form\ContactForm;

class ContactController extends AppController
{
    public function index()
    {
        $contact = new ContactForm();
        if ($this->request->is('post')) {
            if ($contact->execute($this->request->data)) {
                $this->Flash->success('Your message has been sent; we\'ll get back to you soon!');
                $this->request->data['name'] = null;
                $this->request->data['email'] = null;
                $this->request->data['body'] = null;
            } else {
                $this->Flash->error('There was a problem submitting your form.');
            }
        }
        $this->set('contact', $contact);
    }
}

We've created an index method that creates a contact object ($contact) from the ContactForm class, and if our request is a POST we call the execute method of $contact with the data from the HTTP request.  This sends our email so we flash a "success" message and then clear out the form fields (as we're not redirecting to another screen).

We still need one other piece: the template for the form.

The Template


In src/Template, create a "Contact" folder and create an index.ctp like this:

<nav class="large-3 medium-4 columns" id="actions-sidebar">
...
(your sidebar navigation goes here)
...
</nav>
</div>
<div class="employees view large-9 medium-8 columns content">
    <?= $this->Form->create($contact); ?>
    <?= $this->Form->input('name'); ?>
    <?= $this->Form->input('email'); ?>
    <?= $this->Form->input('body'); ?>
    <?= $this->Form->button('Submit'); ?>
    <?= $this->Form->end(); ?>
</div>

Nothing unusual here; this is pretty much just a normal form template.  When we invoke create($contact) we're referencing the variable that we "set" in our controller.  The template's "Submit" button invokes the index method of the ContactController, just like we'd expect.  Mostly, the big difference in "modelless" is that you don't have a model or entity, so you have to put that code elsewhere; that's the role of the code in src/Form/ContactForm.php.



If all went well - and assuming you have an email server upon which to test - you should now have a fully-operational "Contact" form.