root/fJSON.php

Revision 563, 16.5 kB (checked in by wbond, 10 months ago)

Fixed a bug in fJSON::decode() where the pure PHP version would only return one key per JSON object when returning associative arrays

LineHide Line Numbers
1 <?php
2 /**
3  * Provides encoding and decoding for JSON
4  *
5  * This class is a compatibility class for the
6  * [http://php.net/json json extension] on servers with PHP 5.0 or 5.1, or
7  * servers with the json extension compiled out.
8  *
9  * This class will handle JSON values that are not contained in an array or
10  * object - such values are not valid according to the JSON spec, but the
11  * functionality is included for compatiblity with the json extension.
12  *
13  * @copyright  Copyright (c) 2008-2009 Will Bond
14  * @author     Will Bond [wb] <will@flourishlib.com>
15  * @license    http://flourishlib.com/license
16  *
17  * @package    Flourish
18  * @link       http://flourishlib.com/fJSON
19  *
20  * @version    1.0.0b4
21  * @changes    1.0.0b4  Fixed a bug with ::decode() where JSON objects could lose all but the first key: value pair [wb, 2009-05-06]
22  * @changes    1.0.0b3  Updated the class to be consistent with PHP 5.2.9+ for encoding and decoding invalid data [wb, 2009-05-04]
23  * @changes    1.0.0b2  Changed @ error suppression operator to `error_reporting()` calls [wb, 2009-01-26]
24  * @changes    1.0.0b   The initial implementation [wb, 2008-07-12]
25  */
26 class fJSON
27 {
28     // The following constants allow for nice looking callbacks to static methods
29     const decode     = 'fJSON::decode';
30     const encode     = 'fJSON::encode';
31     const sendHeader = 'fJSON::sendHeader';
32    
33    
34     /**
35     * An abstract representation of [
36     *
37     * @internal
38     
39     * @var integer
40     */
41     const J_ARRAY_OPEN  = 0;
42    
43     /**
44     * An abstract representation of , in a JSON array
45     *
46     * @internal
47     
48     * @var integer
49     */
50     const J_ARRAY_COMMA = 1;
51    
52     /**
53     * An abstract representation of ]
54     *
55     * @internal
56     
57     * @var integer
58     */
59     const J_ARRAY_CLOSE = 2;
60    
61     /**
62     * An abstract representation of {
63     *
64     * @internal
65     
66     * @var integer
67     */
68     const J_OBJ_OPEN    = 3;
69    
70     /**
71     * An abstract representation of a JSON object key
72     *
73     * @internal
74     
75     * @var integer
76     */
77     const J_KEY         = 4;
78    
79     /**
80     * An abstract representation of :
81     *
82     * @internal
83     
84     * @var integer
85     */
86     const J_COLON       = 5;
87    
88     /**
89     * An abstract representation of , in a JSON object
90     *
91     * @internal
92     
93     * @var integer
94     */
95     const J_OBJ_COMMA   = 6;
96    
97     /**
98     * An abstract representation of }
99     *
100     * @internal
101     
102     * @var integer
103     */
104     const J_OBJ_CLOSE   = 7;
105    
106     /**
107     * An abstract representation of an integer
108     *
109     * @internal
110     
111     * @var integer
112     */
113     const J_INTEGER     = 8;
114    
115     /**
116     * An abstract representation of a floating value
117     *
118     * @internal
119     
120     * @var integer
121     */
122     const J_FLOAT       = 9;
123    
124     /**
125     * An abstract representation of a boolean true
126     *
127     * @internal
128     
129     * @var integer
130     */
131     const J_TRUE        = 10;
132    
133     /**
134     * An abstract representation of a boolean false
135     *
136     * @internal
137     
138     * @var integer
139     */
140     const J_FALSE       = 11;
141    
142     /**
143     * An abstract representation of null
144     *
145     * @internal
146     
147     * @var integer
148     */
149     const J_NULL        = 12;
150    
151     /**
152     * An abstract representation of a string
153     *
154     * @internal
155     
156     * @var integer
157     */
158     const J_STRING      = 13;
159    
160    
161     /**
162     * An array of special characters in JSON strings
163    
164     * @var array
165     */
166     static private $control_character_map = array(
167         '"'   => '\"', '\\' => '\\\\', '/'  => '\/', "\x8" => '\b',
168         "\xC" => '\f', "\n" => '\n',   "\r" => '\r', "\t"  => '\t'
169     );
170    
171     /**
172     * An array of what values are allowed after other values
173     *
174     * @internal
175     
176     * @var array
177     */
178     static private $next_values = array(
179         self::J_ARRAY_OPEN => array(
180             self::J_ARRAY_OPEN  => TRUE,
181             self::J_ARRAY_CLOSE => TRUE,
182             self::J_OBJ_OPEN    => TRUE,
183             self::J_INTEGER     => TRUE,
184             self::J_FLOAT       => TRUE,
185             self::J_TRUE        => TRUE,
186             self::J_FALSE       => TRUE,
187             self::J_NULL        => TRUE,
188             self::J_STRING      => TRUE
189         ),
190         self::J_ARRAY_COMMA => array(
191             self::J_ARRAY_OPEN  => TRUE,
192             self::J_OBJ_OPEN    => TRUE,
193             self::J_INTEGER     => TRUE,
194             self::J_FLOAT       => TRUE,
195             self::J_TRUE        => TRUE,
196             self::J_FALSE       => TRUE,
197             self::J_NULL        => TRUE,
198             self::J_STRING      => TRUE
199         ),
200         self::J_ARRAY_CLOSE => array(
201             self::J_ARRAY_CLOSE => TRUE,
202             self::J_OBJ_CLOSE   => TRUE,
203             self::J_ARRAY_COMMA => TRUE,
204             self::J_OBJ_COMMA   => TRUE
205         ),
206         self::J_OBJ_OPEN => array(
207             self::J_OBJ_CLOSE   => TRUE,
208             self::J_KEY         => TRUE
209         ),
210         self::J_KEY => array(
211             self::J_COLON       => TRUE
212         ),
213         self::J_OBJ_COMMA => array(
214             self::J_KEY         => TRUE
215         ),
216         self::J_COLON => array(
217             self::J_ARRAY_OPEN  => TRUE,
218             self::J_OBJ_OPEN    => TRUE,
219             self::J_INTEGER     => TRUE,
220             self::J_FLOAT       => TRUE,
221             self::J_TRUE        => TRUE,
222             self::J_FALSE       => TRUE,
223             self::J_NULL        => TRUE,
224             self::J_STRING      => TRUE
225         ),
226         self::J_OBJ_CLOSE => array(
227             self::J_ARRAY_CLOSE => TRUE,
228             self::J_OBJ_CLOSE   => TRUE,
229             self::J_ARRAY_COMMA => TRUE,
230             self::J_OBJ_COMMA   => TRUE
231         ),
232         self::J_INTEGER => array(
233             self::J_ARRAY_CLOSE => TRUE,
234             self::J_OBJ_CLOSE   => TRUE,
235             self::J_ARRAY_COMMA => TRUE,
236             self::J_OBJ_COMMA   => TRUE
237         ),
238         self::J_FLOAT => array(
239             self::J_ARRAY_CLOSE => TRUE,
240             self::J_OBJ_CLOSE   => TRUE,
241             self::J_ARRAY_COMMA => TRUE,
242             self::J_OBJ_COMMA   => TRUE
243         ),
244         self::J_TRUE => array(
245             self::J_ARRAY_CLOSE => TRUE,
246             self::J_OBJ_CLOSE   => TRUE,
247             self::J_ARRAY_COMMA => TRUE,
248             self::J_OBJ_COMMA   => TRUE
249         ),
250         self::J_FALSE => array(
251             self::J_ARRAY_CLOSE => TRUE,
252             self::J_OBJ_CLOSE   => TRUE,
253             self::J_ARRAY_COMMA => TRUE,
254             self::J_OBJ_COMMA   => TRUE
255         ),
256         self::J_NULL => array(
257             self::J_ARRAY_CLOSE => TRUE,
258             self::J_OBJ_CLOSE   => TRUE,
259             self::J_ARRAY_COMMA => TRUE,
260             self::J_OBJ_COMMA   => TRUE
261         ),
262         self::J_STRING => array(
263             self::J_ARRAY_CLOSE => TRUE,
264             self::J_OBJ_CLOSE   => TRUE,
265             self::J_ARRAY_COMMA => TRUE,
266             self::J_OBJ_COMMA   => TRUE
267         )
268     );
269    
270    
271     /**
272     * Decodes a JSON string into native PHP data types
273     *
274     * This function is very strict about the format of JSON. If the string is
275     * not a valid JSON string, `NULL` will be returned.
276    
277     * @param  string  $json   This should be the name of a related class
278     * @param  boolean $assoc  If this is TRUE, JSON objects will be represented as an assocative array instead of a `stdClass` object
279     * @return array|stdClass  A PHP equivalent of the JSON string
280     */
281     static public function decode($json, $assoc=FALSE)
282     {
283         if (!is_string($json) && !is_numeric($json)) {
284             return NULL;
285         }
286        
287         $json = trim($json);
288        
289         if ($json === '') {
290             return NULL;
291         }
292        
293         // If the json is an array or object, we can rely on the php function
294         if (function_exists('json_decode') && ($json[0] == '[' || $json[0] == '{' || version_compare(PHP_VERSION, '5.2.9', '>='))) {
295             return json_decode($json, $assoc);
296         }
297        
298         preg_match_all('~\[|                                     # Array begin
299                          \]|                                     # Array end
300                          {|                                         # Object begin
301                          }|                                         # Object end
302                          -?(?:0|[1-9]\d*)                        # Float
303                              (?:\.\d*(?:[eE][+\-]?\d+)?|
304                              (?:[eE][+\-]?\d+))|
305                          -?(?:0|[1-9]\d*)|                         # Integer
306                          true|                                     # True
307                          false|                                     # False
308                          null|                                     # Null
309                          ,|                                         # Member separator for arrays and objects
310                          :|                                         # Value separator for objects
311                          "(?:(?:(?!\\\\u)[^\\\\"\n\b\f\r\t]+)|   # String
312                              \\\\\\\\|
313                              \\\\/|
314                              \\\\"|
315                              \\\\b|
316                              \\\\f|
317                              \\\\n|
318                              \\\\r|
319                              \\\\t|
320                              \\\\u[0-9a-fA-F]{4})*"|
321                          \s+                                     # Whitespace
322                          ~x', $json, $matches);
323        
324         $matched_length = 0;
325         $stack          = array();
326         $last           = NULL;
327         $last_key       = NULL;
328         $output         = NULL;
329         $container      = NULL;
330        
331         if (sizeof($matches) == 1 && strlen($matches[0][0]) == strlen($json)) {
332             $match = $matches[0][0];
333             $stack = array();
334             $type  = self::getElementType($stack, self::J_ARRAY_OPEN, $match);
335             $element = self::scalarize($type, $match);
336             if ($match !== $element) {
337                 return $element;
338             }
339         }
340                            
341         if ($json[0] != '[' && $json[0] != '{') {
342             return NULL;
343         }
344        
345         foreach ($matches[0] as $match) {
346             if ($matched_length == 0) {
347                 if ($match == '[') {
348                     $output  = array();
349                     $last    = self::J_ARRAY_OPEN;
350                 } else {
351                     $output  = ($assoc) ? array() : new stdClass();
352                     $last    = self::J_OBJ_OPEN;
353                 }
354                 $stack[]   =  array($last, &$output);
355                 $container =& $output;
356                
357                 $matched_length = 1;
358            
359                 continue;
360             }
361            
362             $matched_length += strlen($match);
363            
364             // Whitespace can be skipped over
365             if (ctype_space($match)) {
366                 continue;
367             }
368            
369             $type = self::getElementType($stack, $last, $match);
370            
371             // An invalid sequence will cause parsing to stop
372             if (!isset(self::$next_values[$last][$type])) {
373                 break;
374             }
375            
376             // Decode the data values
377             $match = self::scalarize($type, $match);
378            
379             // If the element is not a value, record some info and continue
380             if ($type == self::J_COLON ||
381                   $type == self::J_OBJ_COMMA ||
382                   $type == self::J_ARRAY_COMMA ||
383                   $type == self::J_KEY) {
384                 $last = $type;
385                 if ($type == self::J_KEY) {
386                     $last_key = $match;
387                 }
388                 continue;
389             }
390            
391             // This flag is used to indicate if an array or object is being added and thus
392             // if the container reference needs to be changed to the current match
393             $ref_match = FALSE;
394            
395             // Closing an object or array
396             if ($type == self::J_OBJ_CLOSE || $type == self::J_ARRAY_CLOSE) {
397                 array_pop($stack);
398                 if (sizeof($stack) == 0) {
399                     break;
400                 }
401                 $new_container = end($stack);
402                 $container =& $new_container[1];
403                 $last = $type;
404                 continue;
405             }
406            
407             // Opening a new object or array requires some references to keep
408             // track of what the current container is
409             if ($type == self::J_OBJ_OPEN) {
410                 $match = ($assoc) ? array() : new stdClass();
411                 $ref_match = TRUE;
412             }
413             if ($type == self::J_ARRAY_OPEN) {
414                 $match = array();
415                 $ref_match = TRUE;
416             }
417            
418             if ($ref_match) {
419                 $stack[] = array($type, &$match);
420                 $stack_end = end($stack);
421             }
422            
423            
424             // Here we assign the value. This code is kind of crazy because
425             // we have to keep track of the current container by references
426             // so we can traverse back down the stack as we move out of
427             // nested arrays and objects
428             if ($last == self::J_COLON && !$assoc) {
429                 if ($last_key == '') {
430                     $last_key = '_empty_';
431                 }
432                 if ($ref_match) {
433                     $container->$last_key =& $stack_end[1];
434                     $container =& $stack_end[1];
435                 } else {
436                     $container->$last_key = $match;
437                 }
438                
439             } elseif ($last == self::J_COLON) {
440                 if ($ref_match) {
441                     $container[$last_key] =& $stack_end[1];
442                     $container =& $stack_end[1];
443                 } else {
444                     $container[$last_key] = $match;
445                 }
446                
447             } else {
448                 if ($ref_match) {
449                     $container[] =& $stack_end[1];
450                     $container =& $stack_end[1];
451                 } else {
452                     $container[] = $match;
453                 }
454             }
455            
456             if ($last == self::J_COLON) {
457                 $last_key = NULL;
458             }
459             $last = $type;
460             unset($match);
461         }
462        
463         if ($matched_length != strlen($json) || sizeof($stack) > 0) {
464             return NULL;
465         }
466        
467         return $output;
468     }
469    
470    
471     /**
472     * Encodes a PHP value into a JSON string
473     *
474     * @param  mixed  $value   The PHP value to encode
475     * @return string  The JSON string that is equivalent to the PHP value
476     */
477     static public function encode($value)
478     {
479         if (is_resource($value)) {
480             return 'null';
481         }
482        
483         if (function_exists('json_encode')) {
484             return json_encode($value);
485         }
486        
487         if (is_int($value)) {
488             return (string) $value;
489         }
490        
491         if (is_float($value)) {
492             return (string) $value;
493         }
494        
495         if (is_bool($value)) {
496             return ($value) ? 'true' : 'false';
497         }
498        
499         if (is_null($value)) {
500             return 'null';
501         }
502        
503         if (is_string($value)) {
504            
505             if (!preg_match('#^.*$#usD', $value)) {
506                 return 'null';
507             }
508            
509             $char_array = fUTF8::explode($value);
510            
511             $output = '"';
512             foreach ($char_array as $char) {
513                 if (isset(self::$control_character_map[$char])) {
514                     $output .= self::$control_character_map[$char];
515                
516                 } elseif (strlen($char) < 2) {
517                     $output .= $char;
518                
519                 } else {
520                     $output .= '\u' . substr(strtolower(fUTF8::ord($char)), 2);
521                 }
522             }
523             $output .= '"';
524            
525             return $output;
526         }
527        
528         // Detect if an array is associative, which would mean it needs to be encoded as an object
529         $is_assoc_array = FALSE;
530         if (is_array($value) && $value) {
531             $looking_for = 0;
532             foreach ($value as $key => $val) {
533                 if (!is_numeric($key) || $key != $looking_for) {
534                     $is_assoc_array = TRUE;
535                     break;
536                 }
537                 $looking_for++;
538             }
539         }
540        
541         if (is_object($value) || $is_assoc_array) {
542             $output  = '{';
543             $members = array();
544            
545             foreach ($value as $key => $val) {
546                 $members[] = self::encode((string) $key) . ':' . self::encode($val);
547             }
548            
549             $output .= join(',', $members);
550             $output .= '}';
551             return $output;
552         }
553        
554         if (is_array($value)) {
555             $output  = '[';
556             $members = array();
557            
558             foreach ($value as $key => $val) {
559                 $members[] = self::encode($val);
560             }
561            
562             $output .= join(',', $members);
563             $output .= ']';
564             return $output;
565         }
566     }
567    
568    
569     /**
570     * Determines the type of a parser JSON element
571    
572     * @param  array   &$stack   The stack of arrays/objects being parsed
573     * @param  integer $last     The type of the last element parsed
574     * @param  string  $element  The element being detected
575     * @return integer  The element type
576     */
577     static private function getElementType(&$stack, $last, $element)
578     {
579         if ($element == '[') {
580             return self::J_ARRAY_OPEN;
581         }
582        
583         if ($element == ']') {
584             return self::J_ARRAY_CLOSE;
585         }
586        
587         if ($element == '{') {
588             return self::J_OBJ_OPEN;
589         }
590        
591         if ($element == '}') {
592             return self::J_OBJ_CLOSE;
593         }
594        
595         if (ctype_digit($element)) {
596             return self::J_INTEGER;
597         }
598        
599         if (is_numeric($element)) {
600             return self::J_FLOAT;
601         }
602        
603         if ($element == 'true') {
604             return self::J_TRUE;
605         }
606        
607         if ($element == 'false') {
608             return self::J_FALSE;
609         }
610        
611         if ($element == 'null') {
612             return self::J_NULL;
613         }
614        
615         $last_stack = end($stack);
616         if ($element == ',' && $last_stack[0] == self::J_ARRAY_OPEN) {
617             return self::J_ARRAY_COMMA;
618         }
619        
620         if ($element == ',') {
621             return self::J_OBJ_COMMA;
622         }
623        
624         if ($element == ':') {
625             return self::J_COLON;
626         }
627        
628         if ($last == self::J_OBJ_OPEN || $last == self::J_OBJ_COMMA) {
629             return self::J_KEY;
630         }
631        
632         return self::J_STRING;
633     }
634    
635    
636     /**
637     * Decodes a scalar value
638    
639     * @param  integer $type     The type of the element
640     * @param  string  $element  The element to be converted to a scalar
641     * @return mixed  The scalar value or the original string of the element
642     */
643     static private function scalarize($type, $element)
644     {
645         if ($type == self::J_INTEGER) {
646             $element = (integer) $element;
647         }
648         if ($type == self::J_FLOAT) {
649             $element = (float) $element;
650         }
651         if ($type == self::J_FALSE) {
652             $element = FALSE;
653         }
654         if ($type == self::J_TRUE) {
655             $element = TRUE;
656         }
657         if ($type == self::J_NULL) {
658             $element = NULL;
659         }
660         if ($type == self::J_STRING || $type == self::J_KEY) {
661             $element = substr($element, 1, -1);
662             $element = strtr($element, array_flip(self::$control_character_map));
663             $element = preg_replace('#\\\\u([0-9a-fA-F]{4})#e', 'fUTF8::chr("U+\1")', $element);
664         }
665        
666         return $element;
667     }
668    
669    
670     /**
671     * Sets the proper `Content-Type` header for UTF-8 encoded JSON
672     *
673     * @return void
674     */
675     static public function sendHeader()
676     {
677         header('Content-Type: application/json; charset=utf-8');
678     }
679    
680    
681     /**
682     * Forces use as a static class
683     *
684     * @return fJSON
685     */
686     private function __construct() { }
687 }
688  
689  
690  
691 /**
692  * Copyright (c) 2008-2009 Will Bond <will@flourishlib.com>
693  *
694  * Permission is hereby granted, free of charge, to any person obtaining a copy
695  * of this software and associated documentation files (the "Software"), to deal
696  * in the Software without restriction, including without limitation the rights
697  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
698  * copies of the Software, and to permit persons to whom the Software is
699  * furnished to do so, subject to the following conditions:
700  *
701  * The above copyright notice and this permission notice shall be included in
702  * all copies or substantial portions of the Software.
703  *
704  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
705  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
706  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
707  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
708  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
709  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
710  * THE SOFTWARE.
711  */