Generating jQuery code from your CakePHP Controller/View

An adventure in PHP’s include, eval, and JavaScript!

I am looking at using jQuery and AJAX as a means of making some of my forms in my StudentTracker© web app more streamlined.  Currently, I’m dynamically creating forms so that all the students’ assignments are listed on a single page, no matter how many assignments there are.  By putting each item into a separate form element (i.e., one text field for the student-visible name, another for the number of points it’s worth, etc, etc), I can allow the user (me! :)   ) to edit each one directly and in-place, without having to visit a separate page for each assignment.  This approach works well, even in spite of the ‘reload the entire page after each edit’ workflow that StudentTracker’s non-AJAX pages use.  The problem is that if I wanted to use, say, jQuery to create a rich user experience, it seems like I’d need a way to generate some JavaScript code per element, in order to do things like send AJAX messages back to the server when a form element gets changed. 

After thinking about it for a while, I’m not so sure that’s true.  I think that for my situation, not only will it be possible to use jQuery to handle a bunch of different elements individually, but it will actually be better to go with that approach.  I say this with the confidence of one who hasn’t actually tried it yet :)

That said, I was playing around with both CakePHP and some simple jQuery, and thinking that if I could use (Cake)PHP to write jQuery code directly into my JavaScript file then I could dynamically generate some JavaScript to go with my dynamically generated form.  Specifically, I wanted the PHP page that generated the JavaScript to have access to all the PHP variables that I have defined in my Cake view.  I also wanted to do so using an approach that could be reused within the Cake controller, if I chose.

After thinking about it, I’ve come up with a fun, clever, very very hack-y way to do it.  It’s so much fun, in fact, that I’m posting it here, even though I think I’ve got a better way to accomplish my original goal :) .

Side-note: If all you want to do is execute PHP code in your JavaScript file, then you can rename the webroot/js/…/foo.js file to be foo.js.php, and CakePHP will correctly interpret the code first as a normal PHP file.  You’ll need to link to foo.js.php (instead of foo.js), but except for that change, the browser will interpret that file as a JavaScript file.

The problem I have with this solution is that you can’t get to anything from the original context – any local variables in your view (or helpers that your controller defined, etc) will be UNAVAILABLE in the JavaScript-generating PHP file. 

This is because the .js.php file is separate from your controller/view files, which means that the browser needs to make a separate request to the web server to retrieve the JavaScript file – by the time the JS is being served up, the original controller/view methods have long since finished!

I started by asking myself "What about using PHP’s include function?"

include will open a file, pull it’s contents into the current .PHP page, and then evaluate the file’s contents, which is exactly what we want.  What’s also very handy is that the include code will be evaluated as if it really had been pasted into that line, including having access to all the local variables available at the time that include is called.  Incidentally, include with also drop back out of ‘PHP mode’, and into ‘HTML mode’, which we also want.  This mode switch will mean that include will basically ignore anything that’s not wrapped in <?php ?> tags, which is perfect.

At first blush, this seems like it should solve the problem easily and elegantly – we can pull the .js file into the view/controller, and evaluate all the (Cake)PHP elements in the file with full access to the view’s local variables.

But include this doesn’t quite work, because it will (effectively) paste the included (JavaScript) code directly into the controller/view method.  Instead, what we want is to have include do it’s work, and then have it hand us a string that contains results of that work (i.e., the evaluated contents of the included file).

Luckily for us, the PHP manual page spells out how to do exactly this.  We can use output buffering to include the file, then put that into a string.  My code below is basically a simple variation on http://us3.php.net/manual/en/function.include.php – specifically the "Example #6 Using output buffering to include a PHP file into a string"

I said to myself "Let’s put this logic into a function and call it!"

The code that I got from the PHP manual wasn’t the most tricky thing in the world, but it wasn’t really something I wanted to have littered throughout my CakePHP code.  So putting this into a function is a natural and normal way to place it into a well-understood and reusable package.

The only problem is that if we call include from a function, then all of that function’s local variables will be in scope, and so we won’t be able to access the controller/view’s variables.

I told myself that I’d finally found a fun reason to use PHP’s eval function!

The basic approach is to define a string that is valid PHP code for including the JavaScript file.  The output buffering goes into this block, too, but I’m going to leave most of the error-checking/input massaging out of this example to keep things simple.  When PHP’s eval is called on the block, then it will execute the ‘include’ statement, in the context of the eval line.

Because the code runs in the context of the eval line, we should should be clear any variables that we create (or stomp on) will still exist after the call to eval is done.  (By ’stomp on’, I mean ‘overwrite’.)  Because of this, we shouldn’t create local variables (or stomp on existing variables) in the script that we’ll be evaluating.   We can achieve this by creating a CakePHP helper, having the controller import that helper, and then using instance variables on that helper object instead of local variables.  In order to make that work, we’ll need to have a variable that contains the name of the helper. 

Thus, the final product: the JavascriptEval helper, with instance variables that’ll substitute for parameters and return values (and locals, if I had any).  The code for the helper (and example files to demonstrate the use of the helper) are listed below; if you want a .ZIP file that contains all of these files, you can find it at http://panitzco.com/Files/JavascriptEval_Demo.zip.  All the files in the .ZIP should be set up so that you can extract it into your /app/ directory, and everything will end up where it needs to go. 

javascript_eval.php
(this is the new, reusable helper – to be placed into /app/views/helpers)

<?php
/**
* JavaScript Helper class file.
*
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
/**
* JavascriptEx Helper class for easy use of (Cake)PHP to generate
* JavaScript, from within a view/controller file.
*
*/
class JavascriptEvalHelper extends AppHelper {

/**
* File to evaluate in the current context
* This needs to be a path (in the file system) to the JavaScript file -
*     it will be passed to the call to include
*
* You should set this before eval'ing evalScript
*
* @var string
* @access public
*/
    var $scriptToEvaluate = false;

/**
* Code that needs to be passed to PHP's eval function. This
*     is set in the helper's constructor.
*
* You shouldn't need to change this, normally.
*
* @var string
* @access public
*/
    var $evalScript = false;

/**
* The resulting, evaluated script
*
* This is roughly equivalent to a return value, if
*     this had been done using functions, instead of this
*     hacky eval thing :)
*
* @var string
* @access public
*/
    var $scriptResults = null;

    function __construct() {
        parent::__construct();
        $this->scriptToEvaluate = "";

$this->evalScript = <<<THESCRIPT
    if( !isset( \$name_of_javascript_eval_object ) ) {
        \$name_of_javascript_eval_object = "javascriptEval";
        // probably should do better error handling, here
    }

    if ( \$\$name_of_javascript_eval_object->scriptToEvaluate == "" ) {
        \$\$name_of_javascript_eval_object->scriptResults = "NO SCRIPT TO EVALUATE";
    }
    else
    {
        // pre-pend the /.../app/webroot/js/ onto the given filename
        \$\$name_of_javascript_eval_object->scriptToEvaluate = APP.'webroot'.DS.'js'.DS. \$\$name_of_javascript_eval_object->scriptToEvaluate ;
        if    (!is_file(\$\$name_of_javascript_eval_object->scriptToEvaluate )) {
            \$\$name_of_javascript_eval_object->scriptResults = 'FILENAME ('.
                    \$\$name_of_javascript_eval_object->scriptToEvaluate . ') IS NOT A VALID FILE!';
        }
        else {
            ob_start();
            include \$\$name_of_javascript_eval_object->scriptToEvaluate ;
            \$\$name_of_javascript_eval_object->scriptResults= ob_get_contents();
            ob_end_clean();
        }
    }
THESCRIPT;
    }
}
?>

You’ll notice that all the action is in the script that the constructor assigns to evalScript, using a heredoc.  Within the script, we check and see if $name_of_javascript_eval_object is defined, and if not, then we assign it a name that should typically work for view code (i.e,. in .CTP files).  Next we check if scriptToEvaluate has been set (it must be set to a file underneath the /app/webroot/js/ directory).  Following that, we assume the file’s name refers to something in the /app/webroot/js/ directory, and so we pre-pend /app/webroot/js/ to the provided script name.  If that file doesn’t exist, then we stop, otherwise we go ahead and include it.

Note the clever use of variable variables to allow someone to re-use this script in, say, a controller, by setting name_of_javascript_eval_object to be something other than the default javascriptEval.  This does mean that we’re creating a local variable in the scope that wants to do the include, but name_of_javascript_eval_object seems like a sufficiently unique name that I just go ahead & create it without checking.  if you wanted to, you could check that it doesn’t already exist and/or unset it before the script ends.

Here’s an example that uses this:

Controller file:

jse_controller.php
(this is the controller – to be placed into /app/controllers/jse_controller.php)

<?php
class JseController extends AppController {

    var $name = 'Jse';

    // For this simple demo, make sure that
    // the controller does not use any model(s)
    var $uses = array();

    function demoEvaluator() {
        $this->helpers[] = 'JavascriptEval';
        $this->helpers[] = 'JavaScript';
    }

}
?>

Other than adding the new JavascriptEval class and the core JavaScript class to the helpers array, this really doesn’t do much.

demo_evaluator.php
(this is the view – to be placed into /app/views/jse/demo_evaluator.php)

<?php
App::import('Sanitize');

    echo $javascript->link('jquery/jquery.js', false); // pull in jQuery

    // Test #1: Forgot to set the evalScript field
    eval( $javascriptEval->evalScript );
    echo 'This should read \'NO SCRIPT TO EVALUATE\'<br/><pre>'.Sanitize::html( $javascriptEval->scriptResults ).'</pre><br/><hr/>';

    // Test #2: Didn't provide a valid filename (into the local filesystem)
    $javascriptEval->scriptToEvaluate = 'NonexistentFile.js';
    eval( $javascriptEval->evalScript );
    echo 'This should read \'FILENAME (...) IS NOT A VALID FILE\'<br/><pre>'.Sanitize::html( $javascriptEval->scriptResults ).'</pre><br/><hr/>';

    // Test #3: This should include the JavaScript file, and evaluate it
    //     as if it was copy-and-pasted in, right here
    $javascriptEval->scriptToEvaluate = 'JavaScriptToEval.js';
    eval( $javascriptEval->evalScript );
    echo Sanitize::html( 'This should be the JavaScript file, with <?php ?> element evaluated').
        '<br/><pre>'. Sanitize::html( $javascriptEval->scriptResults ).'</pre>';

    // Normally, you'll want to include the resulting script in your page, like so:   
    $javascript->codeBlock( $javascriptEval->scriptResults, array('inline'=>false)  );
        // Because we're using the 'inline' => false setting, Cake will put the code into a
        // SCRIPT block in the head of the document.
        // Therefore we don't need to echo it to the document here....
?>
<br/>
<br/>
<hr/>
<br/>
<br/>
<h1>This is the jQuery/CakePHP demo for the 'JavascriptEval' helper</h1>
<p>These lines were generated using normal CakePHP routines</p>

<?php
    // Simple, jQuery link that we'll use in the JavaScript
    echo $html->link("Click here!", "#", array("id"=>'theLink'));
?>

This does a couple of quick unit tests on the provided helper.  It makes sure that the script will detect a missing filename, an incorrect filename, and that it will function correctly when given a correct filename.  Note that the core of this (the ‘typical usage’) boils down to three key steps:

  1. Set the scriptToEvaluate field to the relative path of the file within /app/webroot/js/.
  2. Use PHP’s eval function on the scriptToEvaluate field
  3. Retrieve the scriptResults field, and then use the core JavaScript helper to put the evaluated script into the head of the document.
Typical usage:

<?php 
    $javascriptEval->scriptToEvaluate = 'JavaScriptToEval.js';

    eval( $javascriptEval->evalScript );

    $javascript->codeBlock( $javascriptEval->scriptResults, array('inline'=>false)  );
?>

Note that because the helper assumes that we’ll be using it in a view, we do not need to set the name_of_javascript_eval_object variable. 

JavascriptToEval.php
(this is the JavaScript  – to be placed into /app/webroot/js/JavascriptToEval.js)

/**
* @author Panitz
*/

alert("Hello, from evaluated JavaScript!");

var outputString = <?php

echo '"hello, world!\n'; // note the " inside to start the string

for( $i = 0; $i < 4; $i++) {
    echo 'Iteration '. $i .'\n';
}
echo '"
'; // don't forget to close the string!

// The real goal: Note the use of the CakePHP things that we're
// using, just as if this code was executing inside the .CTP file
//    (which it is :)   )
$testHTML="<h1>BigTitle</h1>";
echo 'alert("sanitized html:\n'. Sanitize::html($testHTML) .'");';

echo $javascript->codeBlock("//Using the core JavaScript Helper!", array('inline'=>false))

?>

$(document).ready(function(){
    $("#theLink").click( function() {
        alert(outputString);
    } );

});

This basically contains a bunch of small tests, in order to demonstrate what you can do with this helper.  When you actually use this helper yourself, you’d want to replace the contents of this file with the JavaScript that you want to dynamically generate using (Cake)PHP. 

The first alert confirms that the JavaScript is loading correctly.  We then use PHP to generate a JavaScript string that we’ll use later on.

We then get into the really useful/interesting stuff – since the view imported the Sanitize class, we can use it here.  Similarly, we can use the helpers that the view has access to, such as the $javascript helper.

Following all that, we have an ultra-simple use of jQuery (which I talked about in my prior post)

A better way?

My original motivation for doing this was being able to create JavaScript code that could deal with dynamically created forms.  For example, I might want to create a single page that contains a list of all the assignments the students will do in a term, with a bit of JavaScript/jQuery attached to each item, so that each and every item can have it’s own rich interface (for example, having each item include a datepicker control).

In retrospect, I think that it’s possible (and better) to write the jQuery code so that it will attach event handlers to all of the UI elements (possibly by ‘tagging’ each one with a CSS class), and then just use that one, single, cache-able, normal JavaScript file instead of dynamically generating code for each client.

Still, the above hack is pretty darn cool, and was fun to try out :)

I’d love to hear if this post helped you out, if you’ve got further questions, or if you’ve got feedback on my blog.

July 06 2009 08:17 pm | CakePHP and Technology and jQuery

Trackback URI | Comments RSS

Leave a Reply