PageRenderTime 167ms CodeModel.GetById 81ms app.highlight 20ms RepoModel.GetById 60ms app.codeStats 1ms

/Json/Encoder.php

https://bitbucket.org/gkawka/zend-framework
PHP | 577 lines | 290 code | 75 blank | 212 comment | 41 complexity | a683f53e738ec2ace21db5cd36f22ac0 MD5 | raw file
  1<?php
  2/**
  3 * Zend Framework
  4 *
  5 * LICENSE
  6 *
  7 * This source file is subject to the new BSD license that is bundled
  8 * with this package in the file LICENSE.txt.
  9 * It is also available through the world-wide-web at this URL:
 10 * http://framework.zend.com/license/new-bsd
 11 * If you did not receive a copy of the license and are unable to
 12 * obtain it through the world-wide-web, please send an email
 13 * to license@zend.com so we can send you a copy immediately.
 14 *
 15 * @category   Zend
 16 * @package    Zend_Json
 17 * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 18 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 19 * @version    $Id: Encoder.php 24593 2012-01-05 20:35:02Z matthew $
 20 */
 21
 22/**
 23 * Encode PHP constructs to JSON
 24 *
 25 * @category   Zend
 26 * @package    Zend_Json
 27 * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 28 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 29 */
 30class Zend_Json_Encoder
 31{
 32    /**
 33     * Whether or not to check for possible cycling
 34     *
 35     * @var boolean
 36     */
 37    protected $_cycleCheck;
 38
 39    /**
 40     * Additional options used during encoding
 41     *
 42     * @var array
 43     */
 44    protected $_options = array();
 45
 46    /**
 47     * Array of visited objects; used to prevent cycling.
 48     *
 49     * @var array
 50     */
 51    protected $_visited = array();
 52
 53    /**
 54     * Constructor
 55     *
 56     * @param boolean $cycleCheck Whether or not to check for recursion when encoding
 57     * @param array $options Additional options used during encoding
 58     * @return void
 59     */
 60    protected function __construct($cycleCheck = false, $options = array())
 61    {
 62        $this->_cycleCheck = $cycleCheck;
 63        $this->_options = $options;
 64    }
 65
 66    /**
 67     * Use the JSON encoding scheme for the value specified
 68     *
 69     * @param mixed $value The value to be encoded
 70     * @param boolean $cycleCheck Whether or not to check for possible object recursion when encoding
 71     * @param array $options Additional options used during encoding
 72     * @return string  The encoded value
 73     */
 74    public static function encode($value, $cycleCheck = false, $options = array())
 75    {
 76        $encoder = new self(($cycleCheck) ? true : false, $options);
 77
 78        return $encoder->_encodeValue($value);
 79    }
 80
 81    /**
 82     * Recursive driver which determines the type of value to be encoded
 83     * and then dispatches to the appropriate method. $values are either
 84     *    - objects (returns from {@link _encodeObject()})
 85     *    - arrays (returns from {@link _encodeArray()})
 86     *    - basic datums (e.g. numbers or strings) (returns from {@link _encodeDatum()})
 87     *
 88     * @param mixed $value The value to be encoded
 89     * @return string Encoded value
 90     */
 91    protected function _encodeValue(&$value)
 92    {
 93        if (is_object($value)) {
 94            return $this->_encodeObject($value);
 95        } else if (is_array($value)) {
 96            return $this->_encodeArray($value);
 97        }
 98
 99        return $this->_encodeDatum($value);
100    }
101
102
103
104    /**
105     * Encode an object to JSON by encoding each of the public properties
106     *
107     * A special property is added to the JSON object called '__className'
108     * that contains the name of the class of $value. This is used to decode
109     * the object on the client into a specific class.
110     *
111     * @param object $value
112     * @return string
113     * @throws Zend_Json_Exception If recursive checks are enabled and the object has been serialized previously
114     */
115    protected function _encodeObject(&$value)
116    {
117        if ($this->_cycleCheck) {
118            if ($this->_wasVisited($value)) {
119
120                if (isset($this->_options['silenceCyclicalExceptions'])
121                    && $this->_options['silenceCyclicalExceptions']===true) {
122
123                    return '"* RECURSION (' . get_class($value) . ') *"';
124
125                } else {
126                    require_once 'Zend/Json/Exception.php';
127                    throw new Zend_Json_Exception(
128                        'Cycles not supported in JSON encoding, cycle introduced by '
129                        . 'class "' . get_class($value) . '"'
130                    );
131                }
132            }
133
134            $this->_visited[] = $value;
135        }
136
137        $props = '';
138        if (method_exists($value, 'toJson')) {
139            $props =',' . preg_replace("/^\{(.*)\}$/","\\1",$value->toJson());
140        } else {
141            if ($value instanceof Iterator) {
142                $propCollection = $value;
143            } else {
144                $propCollection = get_object_vars($value);
145            }
146
147            foreach ($propCollection as $name => $propValue) {
148                if (isset($propValue)) {
149                    $props .= ','
150                            . $this->_encodeString($name)
151                            . ':'
152                            . $this->_encodeValue($propValue);
153                }
154            }
155        }
156        $className = get_class($value);
157        return '{"__className":' . $this->_encodeString($className)
158                . $props . '}';
159    }
160
161
162    /**
163     * Determine if an object has been serialized already
164     *
165     * @param mixed $value
166     * @return boolean
167     */
168    protected function _wasVisited(&$value)
169    {
170        if (in_array($value, $this->_visited, true)) {
171            return true;
172        }
173
174        return false;
175    }
176
177
178    /**
179     * JSON encode an array value
180     *
181     * Recursively encodes each value of an array and returns a JSON encoded
182     * array string.
183     *
184     * Arrays are defined as integer-indexed arrays starting at index 0, where
185     * the last index is (count($array) -1); any deviation from that is
186     * considered an associative array, and will be encoded as such.
187     *
188     * @param array& $array
189     * @return string
190     */
191    protected function _encodeArray(&$array)
192    {
193        $tmpArray = array();
194
195        // Check for associative array
196        if (!empty($array) && (array_keys($array) !== range(0, count($array) - 1))) {
197            // Associative array
198            $result = '{';
199            foreach ($array as $key => $value) {
200                $key = (string) $key;
201                $tmpArray[] = $this->_encodeString($key)
202                            . ':'
203                            . $this->_encodeValue($value);
204            }
205            $result .= implode(',', $tmpArray);
206            $result .= '}';
207        } else {
208            // Indexed array
209            $result = '[';
210            $length = count($array);
211            for ($i = 0; $i < $length; $i++) {
212                $tmpArray[] = $this->_encodeValue($array[$i]);
213            }
214            $result .= implode(',', $tmpArray);
215            $result .= ']';
216        }
217
218        return $result;
219    }
220
221
222    /**
223     * JSON encode a basic data type (string, number, boolean, null)
224     *
225     * If value type is not a string, number, boolean, or null, the string
226     * 'null' is returned.
227     *
228     * @param mixed& $value
229     * @return string
230     */
231    protected function _encodeDatum(&$value)
232    {
233        $result = 'null';
234
235        if (is_int($value) || is_float($value)) {
236            $result = (string) $value;
237            $result = str_replace(",", ".", $result);
238        } elseif (is_string($value)) {
239            $result = $this->_encodeString($value);
240        } elseif (is_bool($value)) {
241            $result = $value ? 'true' : 'false';
242        }
243
244        return $result;
245    }
246
247
248    /**
249     * JSON encode a string value by escaping characters as necessary
250     *
251     * @param string& $value
252     * @return string
253     */
254    protected function _encodeString(&$string)
255    {
256        // Escape these characters with a backslash:
257        // " \ / \n \r \t \b \f
258        $search  = array('\\', "\n", "\t", "\r", "\b", "\f", '"', '/');
259        $replace = array('\\\\', '\\n', '\\t', '\\r', '\\b', '\\f', '\"', '\\/');
260        $string  = str_replace($search, $replace, $string);
261
262        // Escape certain ASCII characters:
263        // 0x08 => \b
264        // 0x0c => \f
265        $string = str_replace(array(chr(0x08), chr(0x0C)), array('\b', '\f'), $string);
266        $string = self::encodeUnicodeString($string);
267
268        return '"' . $string . '"';
269    }
270
271
272    /**
273     * Encode the constants associated with the ReflectionClass
274     * parameter. The encoding format is based on the class2 format
275     *
276     * @param ReflectionClass $cls
277     * @return string Encoded constant block in class2 format
278     */
279    private static function _encodeConstants(ReflectionClass $cls)
280    {
281        $result    = "constants : {";
282        $constants = $cls->getConstants();
283
284        $tmpArray = array();
285        if (!empty($constants)) {
286            foreach ($constants as $key => $value) {
287                $tmpArray[] = "$key: " . self::encode($value);
288            }
289
290            $result .= implode(', ', $tmpArray);
291        }
292
293        return $result . "}";
294    }
295
296
297    /**
298     * Encode the public methods of the ReflectionClass in the
299     * class2 format
300     *
301     * @param ReflectionClass $cls
302     * @return string Encoded method fragment
303     *
304     */
305    private static function _encodeMethods(ReflectionClass $cls)
306    {
307        $methods = $cls->getMethods();
308        $result = 'methods:{';
309
310        $started = false;
311        foreach ($methods as $method) {
312            if (! $method->isPublic() || !$method->isUserDefined()) {
313                continue;
314            }
315
316            if ($started) {
317                $result .= ',';
318            }
319            $started = true;
320
321            $result .= '' . $method->getName(). ':function(';
322
323            if ('__construct' != $method->getName()) {
324                $parameters  = $method->getParameters();
325                $paramCount  = count($parameters);
326                $argsStarted = false;
327
328                $argNames = "var argNames=[";
329                foreach ($parameters as $param) {
330                    if ($argsStarted) {
331                        $result .= ',';
332                    }
333
334                    $result .= $param->getName();
335
336                    if ($argsStarted) {
337                        $argNames .= ',';
338                    }
339
340                    $argNames .= '"' . $param->getName() . '"';
341
342                    $argsStarted = true;
343                }
344                $argNames .= "];";
345
346                $result .= "){"
347                         . $argNames
348                         . 'var result = ZAjaxEngine.invokeRemoteMethod('
349                         . "this, '" . $method->getName()
350                         . "',argNames,arguments);"
351                         . 'return(result);}';
352            } else {
353                $result .= "){}";
354            }
355        }
356
357        return $result . "}";
358    }
359
360
361    /**
362     * Encode the public properties of the ReflectionClass in the class2
363     * format.
364     *
365     * @param ReflectionClass $cls
366     * @return string Encode properties list
367     *
368     */
369    private static function _encodeVariables(ReflectionClass $cls)
370    {
371        $properties = $cls->getProperties();
372        $propValues = get_class_vars($cls->getName());
373        $result = "variables:{";
374        $cnt = 0;
375
376        $tmpArray = array();
377        foreach ($properties as $prop) {
378            if (! $prop->isPublic()) {
379                continue;
380            }
381
382            $tmpArray[] = $prop->getName()
383                        . ':'
384                        . self::encode($propValues[$prop->getName()]);
385        }
386        $result .= implode(',', $tmpArray);
387
388        return $result . "}";
389    }
390
391    /**
392     * Encodes the given $className into the class2 model of encoding PHP
393     * classes into JavaScript class2 classes.
394     * NOTE: Currently only public methods and variables are proxied onto
395     * the client machine
396     *
397     * @param string $className The name of the class, the class must be
398     *  instantiable using a null constructor
399     * @param string $package Optional package name appended to JavaScript
400     *  proxy class name
401     * @return string The class2 (JavaScript) encoding of the class
402     * @throws Zend_Json_Exception
403     */
404    public static function encodeClass($className, $package = '')
405    {
406        $cls = new ReflectionClass($className);
407        if (! $cls->isInstantiable()) {
408            require_once 'Zend/Json/Exception.php';
409            throw new Zend_Json_Exception("$className must be instantiable");
410        }
411
412        return "Class.create('$package$className',{"
413                . self::_encodeConstants($cls)    .","
414                . self::_encodeMethods($cls)      .","
415                . self::_encodeVariables($cls)    .'});';
416    }
417
418
419    /**
420     * Encode several classes at once
421     *
422     * Returns JSON encoded classes, using {@link encodeClass()}.
423     *
424     * @param array $classNames
425     * @param string $package
426     * @return string
427     */
428    public static function encodeClasses(array $classNames, $package = '')
429    {
430        $result = '';
431        foreach ($classNames as $className) {
432            $result .= self::encodeClass($className, $package);
433        }
434
435        return $result;
436    }
437
438    /**
439     * Encode Unicode Characters to \u0000 ASCII syntax.
440     *
441     * This algorithm was originally developed for the
442     * Solar Framework by Paul M. Jones
443     *
444     * @link   http://solarphp.com/
445     * @link   http://svn.solarphp.com/core/trunk/Solar/Json.php
446     * @param  string $value
447     * @return string
448     */
449    public static function encodeUnicodeString($value)
450    {
451        $strlen_var = strlen($value);
452        $ascii = "";
453
454        /**
455         * Iterate over every character in the string,
456         * escaping with a slash or encoding to UTF-8 where necessary
457         */
458        for($i = 0; $i < $strlen_var; $i++) {
459            $ord_var_c = ord($value[$i]);
460
461            switch (true) {
462                case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)):
463                    // characters U-00000000 - U-0000007F (same as ASCII)
464                    $ascii .= $value[$i];
465                    break;
466
467                case (($ord_var_c & 0xE0) == 0xC0):
468                    // characters U-00000080 - U-000007FF, mask 110XXXXX
469                    // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
470                    $char = pack('C*', $ord_var_c, ord($value[$i + 1]));
471                    $i += 1;
472                    $utf16 = self::_utf82utf16($char);
473                    $ascii .= sprintf('\u%04s', bin2hex($utf16));
474                    break;
475
476                case (($ord_var_c & 0xF0) == 0xE0):
477                    // characters U-00000800 - U-0000FFFF, mask 1110XXXX
478                    // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
479                    $char = pack('C*', $ord_var_c,
480                                 ord($value[$i + 1]),
481                                 ord($value[$i + 2]));
482                    $i += 2;
483                    $utf16 = self::_utf82utf16($char);
484                    $ascii .= sprintf('\u%04s', bin2hex($utf16));
485                    break;
486
487                case (($ord_var_c & 0xF8) == 0xF0):
488                    // characters U-00010000 - U-001FFFFF, mask 11110XXX
489                    // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
490                    $char = pack('C*', $ord_var_c,
491                                 ord($value[$i + 1]),
492                                 ord($value[$i + 2]),
493                                 ord($value[$i + 3]));
494                    $i += 3;
495                    $utf16 = self::_utf82utf16($char);
496                    $ascii .= sprintf('\u%04s', bin2hex($utf16));
497                    break;
498
499                case (($ord_var_c & 0xFC) == 0xF8):
500                    // characters U-00200000 - U-03FFFFFF, mask 111110XX
501                    // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
502                    $char = pack('C*', $ord_var_c,
503                                 ord($value[$i + 1]),
504                                 ord($value[$i + 2]),
505                                 ord($value[$i + 3]),
506                                 ord($value[$i + 4]));
507                    $i += 4;
508                    $utf16 = self::_utf82utf16($char);
509                    $ascii .= sprintf('\u%04s', bin2hex($utf16));
510                    break;
511
512                case (($ord_var_c & 0xFE) == 0xFC):
513                    // characters U-04000000 - U-7FFFFFFF, mask 1111110X
514                    // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
515                    $char = pack('C*', $ord_var_c,
516                                 ord($value[$i + 1]),
517                                 ord($value[$i + 2]),
518                                 ord($value[$i + 3]),
519                                 ord($value[$i + 4]),
520                                 ord($value[$i + 5]));
521                    $i += 5;
522                    $utf16 = self::_utf82utf16($char);
523                    $ascii .= sprintf('\u%04s', bin2hex($utf16));
524                    break;
525            }
526        }
527
528        return $ascii;
529     }
530
531    /**
532     * Convert a string from one UTF-8 char to one UTF-16 char.
533     *
534     * Normally should be handled by mb_convert_encoding, but
535     * provides a slower PHP-only method for installations
536     * that lack the multibye string extension.
537     *
538     * This method is from the Solar Framework by Paul M. Jones
539     *
540     * @link   http://solarphp.com
541     * @param string $utf8 UTF-8 character
542     * @return string UTF-16 character
543     */
544    protected static function _utf82utf16($utf8)
545    {
546        // Check for mb extension otherwise do by hand.
547        if( function_exists('mb_convert_encoding') ) {
548            return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
549        }
550
551        switch (strlen($utf8)) {
552            case 1:
553                // this case should never be reached, because we are in ASCII range
554                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
555                return $utf8;
556
557            case 2:
558                // return a UTF-16 character from a 2-byte UTF-8 char
559                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
560                return chr(0x07 & (ord($utf8{0}) >> 2))
561                     . chr((0xC0 & (ord($utf8{0}) << 6))
562                         | (0x3F & ord($utf8{1})));
563
564            case 3:
565                // return a UTF-16 character from a 3-byte UTF-8 char
566                // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
567                return chr((0xF0 & (ord($utf8{0}) << 4))
568                         | (0x0F & (ord($utf8{1}) >> 2)))
569                     . chr((0xC0 & (ord($utf8{1}) << 6))
570                         | (0x7F & ord($utf8{2})));
571        }
572
573        // ignoring UTF-32 for now, sorry
574        return '';
575    }
576}
577