<?php
/**
* Library for facilitating PHP and JavaScript interoperation
* @author Erik Elmore <hide@address.com>
* @version 0.1
* @copyright Copyright © 2006, Erik Elmore
* @license http://www.opensource.org/licenses/gpl-license.php GNU General Public License
* @package php2js
*/
/**
* Facilitates passing PHP data down to JavaScript
*
* This object will take nearly any kind of PHP variable and output a string
* that can be echoed into the body of a document as javascript.
*
* This is a static class that cannot be instantiated via 'new Php2js;'--an
* exception will be thrown if you try. This class is also declared using the
* 'final' keyword to prevent derivatives. The reason for this is the classing
* is really only being used for namespacing and hiding the methods that don't
* need to be accessible outside of the library.
*
* @package php2js
*/
final class Php2Js {
/**
* Private constructor
* This class is 100% static, no instances.
* @access private
*/
private function __construct () {}
/**
* Converts arbitrary PHP data into JavaScript code
*
* This method is a general use function that accepts almost any kind of
* PHP variable and returns a string that can be echoed into the body of
* a document--such as in the head tag of HTML documents or you can even
* (assuming your webserver is configured to make PHP process .js files)
* dynamically create the entire contents of JavaScript files.
*
* Generally, just supply $var and the translated string is returned. This
* function does not add any semicolons or newlines to the end of the
* generated string.
*
* Notes:
* - Exceptions will be thrown if it encounters a data type that it does not
* support or an object property/array index that can not be used as a
* JavaScript variable name.
* - Private class members cannot be retrieved from outside their own class
* gracefully from the outside. This class will simply ignore them. If
* you really need private members translated, you can add a method to
* your class that returns an array like the one returned from
* get_object_vars() and pass that array to this function.
* - Arrays with contiguous unsigned integer indices that start with 0
* will be represented as JavaScript arrays. Arrays with either negative,
* non-integer, non-contiguous, or non-numeric indices will be represented
* in JavaScript as objects.
*
* @param mixed var
* @return string
*/
public static function toJs( $var ) {
return self::doTranslation( $var );
}
/**
* Generate code to initialize JavaScript Image objects
*
* This function accepts a string or an array of strings and generates lines
* of Javascript to create new Image() objects and set their src property.
* This is useful if you have an array of images that you would like to
* have precached by the browser.
*
* You must specify the name of the variable in $info, otherwise an exception
* is thrown. If $var is an array, $info will be used as the resulting
* JavaScript array's name, and the array indices will be used as indices
* or property names where appropriate.
*
* Note: Arrays with contiguous unsigned integer indices that start with 0
* will be represented as JavaScript arrays. Arrays with either negative,
* non-integer, non-contiguous, or non-numeric indices will be represented
* in JavaScript as objects.
*
* @param mixed var
* @param string info
* @return string
*/
public static function toJsImages( $var, $info ) {
if( !self::validateJsVarName($info) ) {
throw new Php2JsError("\$info must be a valid JavaScript variable name");
}
if( is_string($var) ) {
$js = str_repeat(self::$nStr, self::$nLevel);
$js .= "var {$info} = new Image();\n";
$js .= "{$info}.src = " . self::doString($var). ";\n";
return $js;
}
if( !is_array($var) ) {
throw new Php2JsError("\$var must either be a string or an array of strings");
}
$simple = self::isSimpleArray($var);
ksort($var);
$i = 0;
$js = str_repeat(self::$nStr, self::$nLevel);
$js .= "var {$info} = new Array();\n";
foreach( $var as $k => $v ) {
if( $simple ) {
$key = $i;
$i++;
}
else {
if( !self::validateJsPropName($k . "") ) {
throw new Php2JsError("$k (array index) is not a valid JavaScript variable name");
}
$key = $k;
}
$js .= str_repeat(self::$nStr, self::$nLevel);
$js .= "{$info}['{$key}'] = new Image();\n";
$js .= str_repeat(self::$nStr, self::$nLevel);
$js .= "{$info}['{$key}'].src = " . self::doString($v) . ";\n";
}
return $js;
}
/**
* Set the initial indentation level
*
* This function allows you to set the initial level of indentation before
* translating a variable. An exception is thrown if $level is not an int.
*
* @param int level
*/
public static function setNestingLevel( $level ) {
if( is_numeric($level) ? (intval(0+$level) == $x) : FALSE ) {
throw new Php2JsError("\$level must be an integer value");
}
self::$nLevel = $level;
}
/**
* Set the indentation string
*
* This function allows you to set the indentation string. The specified
* string is printed out once for each nesting level on every line.
*
* @param string str
*/
public static function setNestingStr( $str ) {
if( !is_string($var) ) {
throw new Php2JsError("\$str must be a string");
}
self::$nStr = $str;
}
// Start DocBlock template area for internal library functions
/**#@+
* @internal
*/
/**
* Keeper of the current nesting level
*
* This member variable is used to track the current nesting level of the
* JavaScript output. It can be set externally via the setNestingLevel
* method to allow for nesting to start at a specific level of indentation.
*
* @var integer
*/
private static $nLevel = 0;
/**
* Used to create nesting effect
*
* This string is used to pad the JavaScript source code with white space
* while iterating through arrays and object properties. It is 4 spaces by
* default, but it can be changed via setNestingStr.
*
* @var string
*/
private static $nStr = " ";
/**
* List of JavaScript keywords
* @var array
*/
private static $jsKeywords = array (
'abstract' => 'abstract',
'boolean' => 'boolean',
'break' => 'break',
'byte' => 'byte',
'case' => 'case',
'catch' => 'catch',
'char' => 'char',
'class' => 'class',
'const' => 'const',
'continue' => 'continue',
'debugger' => 'debugger',
'default' => 'default',
'delete' => 'delete',
'do' => 'do',
'double' => 'double',
'else' => 'else',
'enum' => 'enum',
'export' => 'export',
'extends' => 'extends',
'false' => 'false',
'final' => 'final',
'finally' => 'finally',
'float' => 'float',
'for' => 'for',
'function' => 'function',
'goto' => 'goto',
'if' => 'if',
'implements' => 'implements',
'import' => 'import',
'in' => 'in',
'instanceof' => 'instanceof',
'int' => 'int',
'interface' => 'interface',
'long' => 'long',
'native' => 'native',
'new' => 'new',
'null' => 'null',
'package' => 'package',
'private' => 'private',
'protected' => 'protected',
'public' => 'public',
'return' => 'return',
'short' => 'short',
'static' => 'static',
'switch' => 'switch',
'synchronized' => 'synchronized',
'this' => 'this',
'throw' => 'throw',
'throws' => 'throws',
'transient' => 'transient',
'true' => 'true',
'try' => 'try',
'typeof' => 'typeof',
'var' => 'var',
'void' => 'void',
'while' => 'while',
'with' => 'with',
);
/**
* Begin processing a variable
*
* This function is utilized directly by the public methods to begin
* processing a variable and translate it to JavaScript. It will also
* call itself and set $recurring = TRUE when it encounters an array or
* object. This function will throw an exception when it encounters a
* variable type that it does not support. $recurring is not used now.
*
* Note: Detection of "simple" arrays has been disabled. All arrays are
* now converted to objects to preserve key-to-value relationships.
*
* @param mixed var
* @param boolean recurring
*/
private static function doTranslation( $var, $recurring = FALSE ) {
$var = is_object($var) ? get_object_vars($var) : $var;
if( is_array($var) ) {
$wasScalar = FALSE;
$simple = self::isSimpleArray($var);
$lb = $simple ? "[" : "{";
$rb = $simple ? "]" : "}";
$js = "{$lb}\n";
ksort($var);
}
else {
$wasScalar = TRUE;
$simple = TRUE;
$var = array($var);
$lb = "";
$rb = "";
$js = "";
}
self::$nLevel++;
$c = count($var);
$i = 0;
foreach( $var as $k => $v ) {
$left = $wasScalar ? "" : str_repeat(self::$nStr, self::$nLevel);
$left .= $simple ? "" : "'{$k}' : ";
if( !$simple && !self::validateJsPropName($k) ) {
throw new Php2JsError("'$k' is not a valid JavaScript variable name");
}
if( is_array($v) || is_object($v) ) {
$js .= $left . self::doTranslation($v, TRUE);
}
else if( is_bool($v) ) {
$js .= $left . self::doBool($v);
}
else if( is_null($v) ) {
$js .= $left . self::doNull($v);
}
else if( is_int($v) || is_float($v)) {
$js .= $left . self::doNumber($v);
}
else if( is_string($v) ) {
$js .= $left . self::doString($v);
}
else {
throw new Php2JsError("Cannot continue translation, unsupported type: " . gettype($v));
}
$i++;
if( $i < $c ) {
$js .= ",\n";
}
else {
$js .= $wasScalar ? "" : "\n";
}
}
self::$nLevel--;
$tail = str_repeat(self::$nStr, self::$nLevel) . $rb;
$js .= $wasScalar ? "" : $tail;
return $js;
}
/**
* Inline translation of a boolean to JavaScript
*
* This function accepts a boolean as input and returns the JavaScript
* representation. It is intended that this function be used for inline
* translation--it does not add newlines, semicolons, or declarations.
*
* An Exception is thrown if $var is not boolean.
*
* @param boolean var
* @return string
*/
private static function doBool( $var ) {
if( !is_bool($var) ) {
throw new Php2JsError("\$var must be a boolean");
}
$js = $var ? "true" : "false";
return $js;
}
/**
* Inline translation of NULL to JavaScript
*
* This function accepts a null type as input and returns the JavaScript
* representation. It is intended that this function be used for inline
* translation--it does not add newlines, semicolons, or declarations.
*
* An Exception is thrown if $var is not null.
*
* @param null var
* @return string
*/
private static function doNull( $var ) {
if( !is_null($var) ) {
throw new Php2JsError("\$var must be null");
}
return "null";
}
/**
* Inline translation of a numeric value JavaScript
*
* This function accepts a numeric as input and returns the JavaScript
* representation. It is intended that this function be used for inline
* translation--it does not add newlines, semicolons, or declarations.
*
* An Exception is thrown if $var is not a number.
*
* @param null var
* @return string
*/
private static function doNumber( $var ) {
if( !is_numeric($var) ) {
throw new Php2JsError("\$var is not numeric");
}
$js = "" . $var;
return $js;
}
/**
* Inline translation of a string to JavaScript
*
* This function accepts a string as input and returns the JavaScript
* representation. It is intended that this function be used for inline
* translation--it does not add newlines, semicolons, or declarations; and
* should ONLY be applied to strings that should be escaped inside quotes.
* This means don't use it on variable names, object property names, or
* associative array indices. Single quote marks are added.
*
* An exception is thrown if $var is not a string.
*
* @param string var
* @return string
*/
private static function doString( $var ) {
if( !is_string($var) ) {
throw new Php2JsError("\$var must be a string");
}
$js = "'" . self::escapeToJs($var) . "'";
return $js;
}
/**
* Validates user-supplied JavaScript variable names
*
* This function will check a string passed in via the API and verify that
* it can be used as a variable name in JavaScript. It returns FALSE if
* the specified name is not legal and otherwise returns TRUE. If $name is
* not a string, an exception is thrown.
*
* @param string var
* @return boolean
*/
private static function validateJsVarName( $name ) {
if( !is_string($name) ) {
throw new Php2JsError("\$name must be a string");
}
// cannot be a key word of javascript
if( array_key_exists($name, self::$jsKeywords) ) {
return FALSE;
}
// first letter must be a letter
// can only be made of letters, numbers, or underscore
$search = "/^[a-zA-Z]{1}[a-zA-Z0-9_]*$/";
if( !preg_match($search, $name) ) {
return FALSE;
}
return TRUE;
}
/**
* Validates user-supplied JavaScript array/object property names
*
* This function will check a string passed in via the API and verify that
* it can be used as a property name in JavaScript. It returns FALSE if
* the specified name is not legal and otherwise returns TRUE. If $name is
* not a string, an exception is thrown.
*
* @param string var
* @return boolean
*/
private static function validateJsPropName( $name ) {
if( !is_string($name) && !is_numeric($name) ) {
throw new Php2JsError("\$name must be a string");
}
// cannot be a key word of javascript
if( array_key_exists($name, self::$jsKeywords) ) {
return FALSE;
}
// first letter must be a letter
// can only be made of letters, numbers, or underscore
// UNLESS the whole thing is an integer
$s1 = "/^[a-zA-Z]{1}[a-zA-Z0-9_]*$/";
$s2 = "/^[0-9]*$/";
if( !(preg_match($s1, $name) || preg_match($s2, $name)) ) {
return FALSE;
}
return TRUE;
}
/**
* Escapes strings so they are valid in JavaScript
*
* This function accepts a PHP string as input and returns a string that will
* represent the input string in JavaScript. This function does not add
* quotation marks. An exception is thrown if the input is not a string.
*
* @param string var
*/
private static function escapeToJs( $var ) {
if( !is_string($var) ) {
throw new Php2JsError("\$var must be an array");
}
$var = str_replace(array('\\', "'"), array("\\\\", "\\'"), $var);
$var = preg_replace('#([\x00-\x1F])#e', '"\x" . sprintf("%02x", ord("\1"))', $var);
return $var;
}
/**
* Determines if a PHP array's indices are compatible with JavaScript
*
* This function will examine the indices of a PHP array and determine its
* suitability for a direct conversion to a JavaScript array. It checks
* the array keys to make sure they are all contiguous unsigned integers and
* returns the appropriate boolean value.
*
* An exception is thrown if $var is not an array.
* @param array var
* @return boolean
* @deprecated
*/
private static function isSimpleArray( $var ) {
if( !is_array($var) ) {
throw new Php2JsError("\$var must be an array");
}
$c = count($var);
for( $i = 0; $i < $c; $i++ ) {
if( !array_key_exists($i, $var) ) {
return FALSE;
}
}
return TRUE;
}
// End DocBlock template area
/**#@-*/
}
/**
* Class for throwing exceptions related to this library
*
* When any exceptions are thrown by this library, it throws an object of this
* class
*
* @package php2js
*/
class Php2JsError extends Exception {}
?>