You are here

class Services_JSON in Album Photos 6.2

Converts to and from JSON format.

Brief example of use:

<code> // create a new instance of Services_JSON $json = new Services_JSON();

// convert a complexe value to JSON notation, and send it to the browser $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4))); $output = $json->encode($value);

print($output); // prints: ["foo","bar",[1,2,"baz"],[3,[4]]]

// accept incoming POST data, assumed to be in JSON notation $input = file_get_contents('php://input', 1000000); $value = $json->decode($input); </code>

Hierarchy

Expanded class hierarchy of Services_JSON

File

php/json-php4.php, line 114

View source
class Services_JSON {

  /**
   * constructs a new JSON instance
   *
   * @param    int     $use    object behavior flags; combine with boolean-OR
   *
   *                           possible values:
   *                           - SERVICES_JSON_LOOSE_TYPE:  loose typing.
   *                                   "{...}" syntax creates associative arrays
   *                                   instead of objects in decode().
   *                           - SERVICES_JSON_SUPPRESS_ERRORS:  error suppression.
   *                                   Values which can't be encoded (e.g. resources)
   *                                   appear as NULL instead of throwing errors.
   *                                   By default, a deeply-nested resource will
   *                                   bubble up with an error, so all return values
   *                                   from encode() should be checked with isError()
   */
  function Services_JSON($use = 0) {
    $this->use = $use;
  }

  /**
   * convert a string from one UTF-16 char to one UTF-8 char
   *
   * Normally should be handled by mb_convert_encoding, but
   * provides a slower PHP-only method for installations
   * that lack the multibye string extension.
   *
   * @param    string  $utf16  UTF-16 character
   * @return   string  UTF-8 character
   * @access   private
   */
  function utf162utf8($utf16) {

    // oh please oh please oh please oh please oh please
    if (function_exists('mb_convert_encoding')) {
      return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
    }
    $bytes = ord($utf16[0]) << 8 | ord($utf16[1]);
    switch (true) {
      case (0x7f & $bytes) == $bytes:

        // this case should never be reached, because we are in ASCII range
        // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
        return chr(0x7f & $bytes);
      case (0x7ff & $bytes) == $bytes:

        // return a 2-byte UTF-8 character
        // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
        return chr(0xc0 | $bytes >> 6 & 0x1f) . chr(0x80 | $bytes & 0x3f);
      case (0xffff & $bytes) == $bytes:

        // return a 3-byte UTF-8 character
        // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
        return chr(0xe0 | $bytes >> 12 & 0xf) . chr(0x80 | $bytes >> 6 & 0x3f) . chr(0x80 | $bytes & 0x3f);
    }

    // ignoring UTF-32 for now, sorry
    return '';
  }

  /**
   * convert a string from one UTF-8 char to one UTF-16 char
   *
   * Normally should be handled by mb_convert_encoding, but
   * provides a slower PHP-only method for installations
   * that lack the multibye string extension.
   *
   * @param    string  $utf8   UTF-8 character
   * @return   string  UTF-16 character
   * @access   private
   */
  function utf82utf16($utf8) {

    // oh please oh please oh please oh please oh please
    if (function_exists('mb_convert_encoding')) {
      return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
    }
    switch (strlen($utf8)) {
      case 1:

        // this case should never be reached, because we are in ASCII range
        // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
        return $utf8;
      case 2:

        // return a UTF-16 character from a 2-byte UTF-8 char
        // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
        return chr(0x7 & ord($utf8[0]) >> 2) . chr(0xc0 & ord($utf8[0]) << 6 | 0x3f & ord($utf8[1]));
      case 3:

        // return a UTF-16 character from a 3-byte UTF-8 char
        // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
        return chr(0xf0 & ord($utf8[0]) << 4 | 0xf & ord($utf8[1]) >> 2) . chr(0xc0 & ord($utf8[1]) << 6 | 0x7f & ord($utf8[2]));
    }

    // ignoring UTF-32 for now, sorry
    return '';
  }

  /**
   * encodes an arbitrary variable into JSON format
   *
   * @param    mixed   $var    any number, boolean, string, array, or object to be encoded.
   *                           see argument 1 to Services_JSON() above for array-parsing behavior.
   *                           if var is a strng, note that encode() always expects it
   *                           to be in ASCII or UTF-8 format!
   *
   * @return   mixed   JSON string representation of input var or an error if a problem occurs
   * @access   public
   */
  function encode($var) {
    switch (gettype($var)) {
      case 'boolean':
        return $var ? 'true' : 'false';
      case 'NULL':
        return 'null';
      case 'integer':
        return (int) $var;
      case 'double':
      case 'float':
        return (double) $var;
      case 'string':

        // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
        $ascii = '';
        $strlen_var = strlen($var);

        /*
         * Iterate over every character in the string,
         * escaping with a slash or encoding to UTF-8 where necessary
         */
        for ($c = 0; $c < $strlen_var; ++$c) {
          $ord_var_c = ord($var[$c]);
          switch (true) {
            case $ord_var_c == 0x8:
              $ascii .= '\\b';
              break;
            case $ord_var_c == 0x9:
              $ascii .= '\\t';
              break;
            case $ord_var_c == 0xa:
              $ascii .= '\\n';
              break;
            case $ord_var_c == 0xc:
              $ascii .= '\\f';
              break;
            case $ord_var_c == 0xd:
              $ascii .= '\\r';
              break;
            case $ord_var_c == 0x22:
            case $ord_var_c == 0x2f:
            case $ord_var_c == 0x5c:

              // double quote, slash, slosh
              $ascii .= '\\' . $var[$c];
              break;
            case $ord_var_c >= 0x20 && $ord_var_c <= 0x7f:

              // characters U-00000000 - U-0000007F (same as ASCII)
              $ascii .= $var[$c];
              break;
            case ($ord_var_c & 0xe0) == 0xc0:

              // characters U-00000080 - U-000007FF, mask 110XXXXX
              // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
              $char = pack('C*', $ord_var_c, ord($var[$c + 1]));
              $c += 1;
              $utf16 = $this
                ->utf82utf16($char);
              $ascii .= sprintf('\\u%04s', bin2hex($utf16));
              break;
            case ($ord_var_c & 0xf0) == 0xe0:

              // characters U-00000800 - U-0000FFFF, mask 1110XXXX
              // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
              $char = pack('C*', $ord_var_c, ord($var[$c + 1]), ord($var[$c + 2]));
              $c += 2;
              $utf16 = $this
                ->utf82utf16($char);
              $ascii .= sprintf('\\u%04s', bin2hex($utf16));
              break;
            case ($ord_var_c & 0xf8) == 0xf0:

              // characters U-00010000 - U-001FFFFF, mask 11110XXX
              // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
              $char = pack('C*', $ord_var_c, ord($var[$c + 1]), ord($var[$c + 2]), ord($var[$c + 3]));
              $c += 3;
              $utf16 = $this
                ->utf82utf16($char);
              $ascii .= sprintf('\\u%04s', bin2hex($utf16));
              break;
            case ($ord_var_c & 0xfc) == 0xf8:

              // characters U-00200000 - U-03FFFFFF, mask 111110XX
              // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
              $char = pack('C*', $ord_var_c, ord($var[$c + 1]), ord($var[$c + 2]), ord($var[$c + 3]), ord($var[$c + 4]));
              $c += 4;
              $utf16 = $this
                ->utf82utf16($char);
              $ascii .= sprintf('\\u%04s', bin2hex($utf16));
              break;
            case ($ord_var_c & 0xfe) == 0xfc:

              // characters U-04000000 - U-7FFFFFFF, mask 1111110X
              // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
              $char = pack('C*', $ord_var_c, ord($var[$c + 1]), ord($var[$c + 2]), ord($var[$c + 3]), ord($var[$c + 4]), ord($var[$c + 5]));
              $c += 5;
              $utf16 = $this
                ->utf82utf16($char);
              $ascii .= sprintf('\\u%04s', bin2hex($utf16));
              break;
          }
        }
        return '"' . $ascii . '"';
      case 'array':

        /*
         * As per JSON spec if any array key is not an integer
         * we must treat the the whole array as an object. We
         * also try to catch a sparsely populated associative
         * array with numeric keys here because some JS engines
         * will create an array with empty indexes up to
         * max_index which can cause memory issues and because
         * the keys, which may be relevant, will be remapped
         * otherwise.
         *
         * As per the ECMA and JSON specification an object may
         * have any string as a property. Unfortunately due to
         * a hole in the ECMA specification if the key is a
         * ECMA reserved word or starts with a digit the
         * parameter is only accessible using ECMAScript's
         * bracket notation.
         */

        // treat as a JSON object
        if (is_array($var) && count($var) && array_keys($var) !== range(0, sizeof($var) - 1)) {
          $properties = array_map(array(
            $this,
            'name_value',
          ), array_keys($var), array_values($var));
          foreach ($properties as $property) {
            if (Services_JSON::isError($property)) {
              return $property;
            }
          }
          return '{' . join(',', $properties) . '}';
        }

        // treat it like a regular array
        $elements = array_map(array(
          $this,
          'encode',
        ), $var);
        foreach ($elements as $element) {
          if (Services_JSON::isError($element)) {
            return $element;
          }
        }
        return '[' . join(',', $elements) . ']';
      case 'object':
        $vars = get_object_vars($var);
        $properties = array_map(array(
          $this,
          'name_value',
        ), array_keys($vars), array_values($vars));
        foreach ($properties as $property) {
          if (Services_JSON::isError($property)) {
            return $property;
          }
        }
        return '{' . join(',', $properties) . '}';
      default:
        return $this->use & SERVICES_JSON_SUPPRESS_ERRORS ? 'null' : new Services_JSON_Error(gettype($var) . " can not be encoded as JSON string");
    }
  }

  /**
   * array-walking function for use in generating JSON-formatted name-value pairs
   *
   * @param    string  $name   name of key to use
   * @param    mixed   $value  reference to an array element to be encoded
   *
   * @return   string  JSON-formatted name-value pair, like '"name":value'
   * @access   private
   */
  function name_value($name, $value) {
    $encoded_value = $this
      ->encode($value);
    if (Services_JSON::isError($encoded_value)) {
      return $encoded_value;
    }
    return $this
      ->encode(strval($name)) . ':' . $encoded_value;
  }

  /**
   * reduce a string by removing leading and trailing comments and whitespace
   *
   * @param    $str    string      string value to strip of comments and whitespace
   *
   * @return   string  string value stripped of comments and whitespace
   * @access   private
   */
  function reduce_string($str) {
    $str = preg_replace(array(
      // eliminate single line comments in '// ...' form
      '#^\\s*//(.+)$#m',
      // eliminate multi-line comments in '/* ... */' form, at start of string
      '#^\\s*/\\*(.+)\\*/#Us',
      // eliminate multi-line comments in '/* ... */' form, at end of string
      '#/\\*(.+)\\*/\\s*$#Us',
    ), '', $str);

    // eliminate extraneous space
    return trim($str);
  }

  /**
   * decodes a JSON string into appropriate variable
   *
   * @param    string  $str    JSON-formatted string
   *
   * @return   mixed   number, boolean, string, array, or object
   *                   corresponding to given JSON input string.
   *                   See argument 1 to Services_JSON() above for object-output behavior.
   *                   Note that decode() always returns strings
   *                   in ASCII or UTF-8 format!
   * @access   public
   */
  function decode($str) {
    $str = $this
      ->reduce_string($str);
    switch (strtolower($str)) {
      case 'true':
        return true;
      case 'false':
        return false;
      case 'null':
        return null;
      default:
        $m = array();
        if (is_numeric($str)) {

          // Lookie-loo, it's a number
          // This would work on its own, but I'm trying to be
          // good about returning integers where appropriate:
          // return (float)$str;
          // Return float or int, as appropriate
          return (double) $str == (int) $str ? (int) $str : (double) $str;
        }
        elseif (preg_match('/^("|\').*(\\1)$/s', $str, $m) && $m[1] == $m[2]) {

          // STRINGS RETURNED IN UTF-8 FORMAT
          $delim = substr($str, 0, 1);
          $chrs = substr($str, 1, -1);
          $utf8 = '';
          $strlen_chrs = strlen($chrs);
          for ($c = 0; $c < $strlen_chrs; ++$c) {
            $substr_chrs_c_2 = substr($chrs, $c, 2);
            $ord_chrs_c = ord($chrs[$c]);
            switch (true) {
              case $substr_chrs_c_2 == '\\b':
                $utf8 .= chr(0x8);
                ++$c;
                break;
              case $substr_chrs_c_2 == '\\t':
                $utf8 .= chr(0x9);
                ++$c;
                break;
              case $substr_chrs_c_2 == '\\n':
                $utf8 .= chr(0xa);
                ++$c;
                break;
              case $substr_chrs_c_2 == '\\f':
                $utf8 .= chr(0xc);
                ++$c;
                break;
              case $substr_chrs_c_2 == '\\r':
                $utf8 .= chr(0xd);
                ++$c;
                break;
              case $substr_chrs_c_2 == '\\"':
              case $substr_chrs_c_2 == '\\\'':
              case $substr_chrs_c_2 == '\\\\':
              case $substr_chrs_c_2 == '\\/':
                if ($delim == '"' && $substr_chrs_c_2 != '\\\'' || $delim == "'" && $substr_chrs_c_2 != '\\"') {
                  $utf8 .= $chrs[++$c];
                }
                break;
              case preg_match('/\\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)):

                // single, escaped unicode character
                $utf16 = chr(hexdec(substr($chrs, $c + 2, 2))) . chr(hexdec(substr($chrs, $c + 4, 2)));
                $utf8 .= $this
                  ->utf162utf8($utf16);
                $c += 5;
                break;
              case $ord_chrs_c >= 0x20 && $ord_chrs_c <= 0x7f:
                $utf8 .= $chrs[$c];
                break;
              case ($ord_chrs_c & 0xe0) == 0xc0:

                // characters U-00000080 - U-000007FF, mask 110XXXXX

                //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                $utf8 .= substr($chrs, $c, 2);
                ++$c;
                break;
              case ($ord_chrs_c & 0xf0) == 0xe0:

                // characters U-00000800 - U-0000FFFF, mask 1110XXXX
                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                $utf8 .= substr($chrs, $c, 3);
                $c += 2;
                break;
              case ($ord_chrs_c & 0xf8) == 0xf0:

                // characters U-00010000 - U-001FFFFF, mask 11110XXX
                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                $utf8 .= substr($chrs, $c, 4);
                $c += 3;
                break;
              case ($ord_chrs_c & 0xfc) == 0xf8:

                // characters U-00200000 - U-03FFFFFF, mask 111110XX
                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                $utf8 .= substr($chrs, $c, 5);
                $c += 4;
                break;
              case ($ord_chrs_c & 0xfe) == 0xfc:

                // characters U-04000000 - U-7FFFFFFF, mask 1111110X
                // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
                $utf8 .= substr($chrs, $c, 6);
                $c += 5;
                break;
            }
          }
          return $utf8;
        }
        elseif (preg_match('/^\\[.*\\]$/s', $str) || preg_match('/^\\{.*\\}$/s', $str)) {

          // array, or object notation
          if ($str[0] == '[') {
            $stk = array(
              SERVICES_JSON_IN_ARR,
            );
            $arr = array();
          }
          else {
            if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
              $stk = array(
                SERVICES_JSON_IN_OBJ,
              );
              $obj = array();
            }
            else {
              $stk = array(
                SERVICES_JSON_IN_OBJ,
              );
              $obj = new stdClass();
            }
          }
          array_push($stk, array(
            'what' => SERVICES_JSON_SLICE,
            'where' => 0,
            'delim' => false,
          ));
          $chrs = substr($str, 1, -1);
          $chrs = $this
            ->reduce_string($chrs);
          if ($chrs == '') {
            if (reset($stk) == SERVICES_JSON_IN_ARR) {
              return $arr;
            }
            else {
              return $obj;
            }
          }

          //print("\nparsing {$chrs}\n");
          $strlen_chrs = strlen($chrs);
          for ($c = 0; $c <= $strlen_chrs; ++$c) {
            $top = end($stk);
            $substr_chrs_c_2 = substr($chrs, $c, 2);
            if ($c == $strlen_chrs || $chrs[$c] == ',' && $top['what'] == SERVICES_JSON_SLICE) {

              // found a comma that is not inside a string, array, etc.,
              // OR we've reached the end of the character list
              $slice = substr($chrs, $top['where'], $c - $top['where']);
              array_push($stk, array(
                'what' => SERVICES_JSON_SLICE,
                'where' => $c + 1,
                'delim' => false,
              ));

              //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
              if (reset($stk) == SERVICES_JSON_IN_ARR) {

                // we are in an array, so just push an element onto the stack
                array_push($arr, $this
                  ->decode($slice));
              }
              elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {

                // we are in an object, so figure
                // out the property name and set an
                // element in an associative array,
                // for now
                $parts = array();
                if (preg_match('/^\\s*(["\'].*[^\\\\]["\'])\\s*:\\s*(\\S.*),?$/Uis', $slice, $parts)) {

                  // "name":value pair
                  $key = $this
                    ->decode($parts[1]);
                  $val = $this
                    ->decode($parts[2]);
                  if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
                    $obj[$key] = $val;
                  }
                  else {
                    $obj->{$key} = $val;
                  }
                }
                elseif (preg_match('/^\\s*(\\w+)\\s*:\\s*(\\S.*),?$/Uis', $slice, $parts)) {

                  // name:value pair, where name is unquoted
                  $key = $parts[1];
                  $val = $this
                    ->decode($parts[2]);
                  if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
                    $obj[$key] = $val;
                  }
                  else {
                    $obj->{$key} = $val;
                  }
                }
              }
            }
            elseif (($chrs[$c] == '"' || $chrs[$c] == "'") && $top['what'] != SERVICES_JSON_IN_STR) {

              // found a quote, and we are not inside a string
              array_push($stk, array(
                'what' => SERVICES_JSON_IN_STR,
                'where' => $c,
                'delim' => $chrs[$c],
              ));

              //print("Found start of string at {$c}\n");
            }
            elseif ($chrs[$c] == $top['delim'] && $top['what'] == SERVICES_JSON_IN_STR && (strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1) {

              // found a quote, we're in a string, and it's not escaped
              // we know that it's not escaped becase there is _not_ an
              // odd number of backslashes at the end of the string so far
              array_pop($stk);

              //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n");
            }
            elseif ($chrs[$c] == '[' && in_array($top['what'], array(
              SERVICES_JSON_SLICE,
              SERVICES_JSON_IN_ARR,
              SERVICES_JSON_IN_OBJ,
            ))) {

              // found a left-bracket, and we are in an array, object, or slice
              array_push($stk, array(
                'what' => SERVICES_JSON_IN_ARR,
                'where' => $c,
                'delim' => false,
              ));

              //print("Found start of array at {$c}\n");
            }
            elseif ($chrs[$c] == ']' && $top['what'] == SERVICES_JSON_IN_ARR) {

              // found a right-bracket, and we're in an array
              array_pop($stk);

              //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
            }
            elseif ($chrs[$c] == '{' && in_array($top['what'], array(
              SERVICES_JSON_SLICE,
              SERVICES_JSON_IN_ARR,
              SERVICES_JSON_IN_OBJ,
            ))) {

              // found a left-brace, and we are in an array, object, or slice
              array_push($stk, array(
                'what' => SERVICES_JSON_IN_OBJ,
                'where' => $c,
                'delim' => false,
              ));

              //print("Found start of object at {$c}\n");
            }
            elseif ($chrs[$c] == '}' && $top['what'] == SERVICES_JSON_IN_OBJ) {

              // found a right-brace, and we're in an object
              array_pop($stk);

              //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
            }
            elseif ($substr_chrs_c_2 == '/*' && in_array($top['what'], array(
              SERVICES_JSON_SLICE,
              SERVICES_JSON_IN_ARR,
              SERVICES_JSON_IN_OBJ,
            ))) {

              // found a comment start, and we are in an array, object, or slice
              array_push($stk, array(
                'what' => SERVICES_JSON_IN_CMT,
                'where' => $c,
                'delim' => false,
              ));
              $c++;

              //print("Found start of comment at {$c}\n");
            }
            elseif ($substr_chrs_c_2 == '*/' && $top['what'] == SERVICES_JSON_IN_CMT) {

              // found a comment end, and we're in one now
              array_pop($stk);
              $c++;
              for ($i = $top['where']; $i <= $c; ++$i) {
                $chrs = substr_replace($chrs, ' ', $i, 1);
              }

              //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
            }
          }
          if (reset($stk) == SERVICES_JSON_IN_ARR) {
            return $arr;
          }
          elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
            return $obj;
          }
        }
    }
  }

  /**
   * @todo Ultimately, this should just call PEAR::isError()
   */
  function isError($data, $code = null) {
    if (class_exists('pear')) {
      return PEAR::isError($data, $code);
    }
    elseif (is_object($data) && (get_class($data) == 'services_json_error' || is_subclass_of($data, 'services_json_error'))) {
      return true;
    }
    return false;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
Services_JSON::decode function decodes a JSON string into appropriate variable
Services_JSON::encode function encodes an arbitrary variable into JSON format
Services_JSON::isError function @todo Ultimately, this should just call PEAR::isError()
Services_JSON::name_value function array-walking function for use in generating JSON-formatted name-value pairs
Services_JSON::reduce_string function reduce a string by removing leading and trailing comments and whitespace
Services_JSON::Services_JSON function constructs a new JSON instance
Services_JSON::utf162utf8 function convert a string from one UTF-16 char to one UTF-8 char
Services_JSON::utf82utf16 function convert a string from one UTF-8 char to one UTF-16 char